refactor(platform-detection): 统一平台检测逻辑并添加分享链接识别

将 content script 和 popup 中的平台检测逻辑统一迁移到 platform-detector 工具模块
添加夸克网盘分享链接检测功能,并在检测到分享链接时禁用扩展
更新相关测试用例确保检测逻辑正确性
这个提交包含在:
Zain 2025-12-18 22:57:18 +08:00
父节点 9e45a7e51e
当前提交 f9f93cca59
共有 6 个文件被更改,包括 349 次插入 和 27 次删除

查看文件

@ -1,6 +1,6 @@
{
"name": "cloud-drive-renamer",
"version": "0.2.0",
"version": "0.2.1",
"description": "批量重命名工具,支持夸克网盘(阿里云盘和百度网盘开发中)",
"type": "module",
"scripts": {

查看文件

@ -9,6 +9,7 @@ import { storage } from '../utils/storage';
import { PlatformUsageStats, STORAGE_KEYS } from '../types/stats';
import { I18nService } from '../utils/i18n';
import type { LanguageChangeMessage } from '../types/i18n';
import { detectPlatformFromUrl, isQuarkShareLink } from '../utils/platform-detector';
/**
* Content Script
@ -33,18 +34,20 @@ let initRetryCount = 0;
const MAX_INIT_RETRIES = 3;
// 检测当前平台
function detectPlatform(): PlatformName | null {
// 使用统一的平台检测工具函数
export function detectPlatform(): PlatformName | null {
const url = window.location.href;
const pathname = window.location.pathname;
if (url.includes('pan.quark.cn')) {
return 'quark';
} else if (url.includes('www.aliyundrive.com')) {
return 'aliyun';
} else if (url.includes('pan.baidu.com')) {
return 'baidu';
// 使用统一的检测逻辑
const platform = detectPlatformFromUrl(url, pathname);
// 如果是分享链接,记录日志 (提升日志级别为 warn,因为这是需要用户关注的情况)
if (url.includes('pan.quark.cn') && isQuarkShareLink(pathname)) {
logger.warn('Quark share link detected, extension disabled for share pages');
}
return null;
return platform;
}
// 初始化

查看文件

@ -5,19 +5,14 @@ import { PlatformUsageStats, STORAGE_KEYS } from '../types/stats';
import { APP_VERSION_WITH_PREFIX } from '../shared/version';
import { I18nService } from '../utils/i18n';
import { LanguageChangeMessage } from '../types/i18n';
import { detectPlatformFromUrl } from '../utils/platform-detector';
/**
* Popup脚本
*/
// 平台URL到平台标识的映射
// 注意:只包含真正实现了content script和adapter的平台
const PLATFORM_URL_MAP: Record<string, PlatformName> = {
'pan.quark.cn': 'quark',
// 未来支持的平台在此添加
// 'www.aliyundrive.com': 'aliyun',
// 'pan.baidu.com': 'baidu',
};
// 注意: 平台检测逻辑已迁移到 src/utils/platform-detector.ts
// 使用统一的 detectPlatformFromUrl() 函数
/**
* 获取平台显示名称(动态翻译)
@ -126,6 +121,7 @@ async function initPopup() {
}
// 检测平台
// 使用统一的检测逻辑 (移除旧的本地实现)
const platformKey = detectPlatformFromUrl(currentTab.url);
if (platformKey) {
// 使用统一的函数获取翻译
@ -151,15 +147,8 @@ async function initPopup() {
bindEvents();
}
// 检测平台
function detectPlatformFromUrl(url: string): PlatformName | null {
for (const [urlPattern, platformKey] of Object.entries(PLATFORM_URL_MAP)) {
if (url.includes(urlPattern)) {
return platformKey;
}
}
return null;
}
// 注意: detectPlatformFromUrl() 已迁移到 src/utils/platform-detector.ts
// 这里不再需要本地实现
// 更新平台名称显示
function updatePlatformName(name: string) {

查看文件

@ -10,7 +10,7 @@
* 1. Edit version in package.json
* 2. Run: npm run sync:version (or automatically via prebuild/predev)
*
* Generated at: 2025-12-18T13:53:59.891Z
* Generated at: 2025-12-18T14:55:23.819Z
*/
/**

查看文件

@ -0,0 +1,75 @@
/**
* 平台检测工具模块
* 统一 content script 和 popup 的平台检测逻辑
*/
import { PlatformName } from '../types/platform';
/**
* 夸克网盘分享链接路径模式
* 所有包含这些模式的路径都被识别为分享链接页面
*/
export const QUARK_SHARE_LINK_PATTERNS = ['/s/'] as const;
/**
* 检测URL是否为夸克网盘分享链接
* @param pathname - URL路径部分
* @returns 是否为分享链接
*/
export function isQuarkShareLink(pathname: string | null | undefined): boolean {
// 防御性检查: 确保 pathname 存在且为字符串
if (!pathname || typeof pathname !== 'string') {
return false;
}
// 检查路径是否包含任何分享链接模式
return QUARK_SHARE_LINK_PATTERNS.some(pattern => pathname.includes(pattern));
}
/**
* 从URL检测平台类型
* @param url - 完整URL
* @param pathname - URL路径部分 (可选, 用于分享链接检测)
* @returns 平台名称或null
*/
export function detectPlatformFromUrl(url: string, pathname?: string): PlatformName | null {
// 夸克网盘
if (url.includes('pan.quark.cn')) {
// 使用统一的分享链接检测函数
// 如果未提供pathname,尝试从URL解析
const pathToCheck = pathname ?? extractPathnameFromUrl(url);
if (isQuarkShareLink(pathToCheck)) {
return null; // 分享链接页面不支持
}
return 'quark';
}
// 阿里云盘 (未来支持)
if (url.includes('www.aliyundrive.com')) {
return 'aliyun';
}
// 百度网盘 (未来支持)
if (url.includes('pan.baidu.com')) {
return 'baidu';
}
return null;
}
/**
* 从完整URL中提取pathname部分
* @param url - 完整URL
* @returns pathname字符串
*/
function extractPathnameFromUrl(url: string): string {
try {
const urlObj = new URL(url);
return urlObj.pathname;
} catch {
// URL解析失败,返回空字符串
return '';
}
}

查看文件

@ -0,0 +1,255 @@
/**
* 平台检测单元测试
* 验证统一的平台检测工具函数正确识别个人网盘和分享链接
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { detectPlatformFromUrl, isQuarkShareLink, QUARK_SHARE_LINK_PATTERNS } from '../../src/utils/platform-detector';
import { detectPlatform } from '../../src/content/index';
import { logger } from '../../src/utils/logger';
// Mock logger 模块
vi.mock('../../src/utils/logger', () => ({
logger: {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
}
}));
describe('Platform Detection - Unified Utils', () => {
beforeEach(() => {
// 重置所有 mock
vi.clearAllMocks();
});
describe('isQuarkShareLink() - Share Link Pattern Detection', () => {
it('should detect basic share link pattern', () => {
expect(isQuarkShareLink('/s/abc123xyz')).toBe(true);
});
it('should detect share link with trailing slash', () => {
expect(isQuarkShareLink('/s/abc123xyz/')).toBe(true);
});
it('should detect share link with nested path', () => {
expect(isQuarkShareLink('/s/share/folder')).toBe(true);
});
it('should return false for personal storage path', () => {
expect(isQuarkShareLink('/list')).toBe(false);
expect(isQuarkShareLink('/list/all')).toBe(false);
expect(isQuarkShareLink('/settings')).toBe(false);
});
it('should handle null pathname defensively', () => {
expect(isQuarkShareLink(null)).toBe(false);
});
it('should handle undefined pathname defensively', () => {
expect(isQuarkShareLink(undefined)).toBe(false);
});
it('should handle empty string', () => {
expect(isQuarkShareLink('')).toBe(false);
});
it('should use configurable patterns from constant', () => {
// 验证函数使用了导出的常量
expect(QUARK_SHARE_LINK_PATTERNS).toContain('/s/');
});
});
describe('detectPlatformFromUrl() - Unified Detection Logic', () => {
describe('Quark Drive - Personal Storage', () => {
it('should detect personal file list page', () => {
const result = detectPlatformFromUrl(
'https://pan.quark.cn/list#/list/all',
'/list'
);
expect(result).toBe('quark');
});
it('should detect personal folder page', () => {
const result = detectPlatformFromUrl(
'https://pan.quark.cn/list#/list/folder/12345',
'/list'
);
expect(result).toBe('quark');
});
it('should detect personal recent files page', () => {
const result = detectPlatformFromUrl(
'https://pan.quark.cn/list#/recent',
'/list'
);
expect(result).toBe('quark');
});
it('should detect settings page', () => {
const result = detectPlatformFromUrl(
'https://pan.quark.cn/settings',
'/settings'
);
expect(result).toBe('quark');
});
});
describe('Quark Drive - Share Links (Should Return null)', () => {
it('should return null for basic share link', () => {
const result = detectPlatformFromUrl(
'https://pan.quark.cn/s/abc123xyz',
'/s/abc123xyz'
);
expect(result).toBeNull();
});
it('should return null for share link with password', () => {
const result = detectPlatformFromUrl(
'https://pan.quark.cn/s/abc123xyz?pwd=1234',
'/s/abc123xyz'
);
expect(result).toBeNull();
});
it('should return null for share link with hash fragment', () => {
const result = detectPlatformFromUrl(
'https://pan.quark.cn/s/xyz789#/folder',
'/s/xyz789'
);
expect(result).toBeNull();
});
it('should return null for share link with query parameters', () => {
const result = detectPlatformFromUrl(
'https://pan.quark.cn/s/shared123?from=mobile&source=qr',
'/s/shared123'
);
expect(result).toBeNull();
});
it('should handle pathname with trailing slash', () => {
const result = detectPlatformFromUrl(
'https://pan.quark.cn/s/test123/',
'/s/test123/'
);
expect(result).toBeNull();
});
it('should handle nested share path', () => {
const result = detectPlatformFromUrl(
'https://pan.quark.cn/s/share/folder',
'/s/share/folder'
);
expect(result).toBeNull();
});
});
describe('Auto pathname extraction (when pathname not provided)', () => {
it('should extract pathname from URL for personal storage', () => {
const result = detectPlatformFromUrl('https://pan.quark.cn/list#/list/all');
expect(result).toBe('quark');
});
it('should extract pathname from URL for share link', () => {
const result = detectPlatformFromUrl('https://pan.quark.cn/s/abc123xyz');
expect(result).toBeNull();
});
});
describe('Other Platforms - Unchanged Behavior', () => {
it('should detect Aliyun Drive', () => {
const result = detectPlatformFromUrl(
'https://www.aliyundrive.com/drive/file/all',
'/drive/file/all'
);
expect(result).toBe('aliyun');
});
it('should detect Baidu Drive', () => {
const result = detectPlatformFromUrl(
'https://pan.baidu.com/disk/home',
'/disk/home'
);
expect(result).toBe('baidu');
});
it('should return null for unsupported platform', () => {
const result = detectPlatformFromUrl(
'https://example.com/',
'/'
);
expect(result).toBeNull();
});
});
describe('Edge Cases - Robustness', () => {
it('should handle malformed URL gracefully', () => {
const result = detectPlatformFromUrl('not-a-valid-url');
expect(result).toBeNull();
});
it('should handle empty pathname', () => {
const result = detectPlatformFromUrl('https://pan.quark.cn/', '');
expect(result).toBe('quark');
});
});
});
describe('detectPlatform() - Content Script Integration', () => {
beforeEach(() => {
// 重置 window.location mock
delete (window as any).location;
});
it('should detect personal storage and not log warning', () => {
Object.defineProperty(window, 'location', {
value: {
href: 'https://pan.quark.cn/list#/list/all',
pathname: '/list'
},
writable: true,
configurable: true
});
const result = detectPlatform();
expect(result).toBe('quark');
expect(logger.warn).not.toHaveBeenCalled();
});
it('should detect share link and log warning', () => {
Object.defineProperty(window, 'location', {
value: {
href: 'https://pan.quark.cn/s/abc123xyz',
pathname: '/s/abc123xyz'
},
writable: true,
configurable: true
});
const result = detectPlatform();
expect(result).toBeNull();
expect(logger.warn).toHaveBeenCalledWith(
'Quark share link detected, extension disabled for share pages'
);
});
it('should return null for unsupported platform without warning', () => {
Object.defineProperty(window, 'location', {
value: {
href: 'https://example.com/',
pathname: '/'
},
writable: true,
configurable: true
});
const result = detectPlatform();
expect(result).toBeNull();
expect(logger.warn).not.toHaveBeenCalled();
});
});
});