diff --git a/package.json b/package.json index 0baefdd..28e0e35 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cloud-drive-renamer", - "version": "0.2.0", + "version": "0.2.1", "description": "批量重命名工具,支持夸克网盘(阿里云盘和百度网盘开发中)", "type": "module", "scripts": { diff --git a/src/content/index.ts b/src/content/index.ts index 60a2864..e80d065 100644 --- a/src/content/index.ts +++ b/src/content/index.ts @@ -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; } // 初始化 diff --git a/src/popup/popup.ts b/src/popup/popup.ts index f0c3e79..dcf1f4d 100644 --- a/src/popup/popup.ts +++ b/src/popup/popup.ts @@ -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 = { - '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) { diff --git a/src/shared/version.ts b/src/shared/version.ts index f22259e..aebb12c 100644 --- a/src/shared/version.ts +++ b/src/shared/version.ts @@ -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 */ /** diff --git a/src/utils/platform-detector.ts b/src/utils/platform-detector.ts new file mode 100644 index 0000000..d6fbeaf --- /dev/null +++ b/src/utils/platform-detector.ts @@ -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 ''; + } +} diff --git a/tests/unit/platform-detection.test.ts b/tests/unit/platform-detection.test.ts new file mode 100644 index 0000000..fd2c3bb --- /dev/null +++ b/tests/unit/platform-detection.test.ts @@ -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(); + }); + }); +});