From 7167a437639cdb7ca16bdd8e1e910a5396d7e6ff Mon Sep 17 00:00:00 2001 From: LingandRX <56020800+LingandRX@users.noreply.github.com> Date: Wed, 20 May 2026 19:55:49 +0800 Subject: [PATCH] Develop (#25) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 核心功能新增 工具套件扩展 JSON 工具(差异比较、格式化、转 YAML/TOML、压缩) Base64 转换器(文本/文件/图像编解码) Markdown ↔ HTML 双向转换 JWT 解析器 文本统计工具 二维码生成/解析(替换库以减小体积) 表单工具(已移除) 表单映射、智能填充、高亮定位功能已移除 架构与工程改进 技术栈升级 引入 @webext-core/messaging 重构消息通信 添加 @dnd-kit 支持功能列表拖拽排序 引入 i18next 实现中英文国际化 使用 MUI 主题系统替代硬编码样式,支持暗色模式 代码质量 统一组件导出为默认导出 提取公共组件(TextInputArea、SwitchButtonGroup、PageHeader、DecodeResultPaper) 使用 useStorageState 钩子统一管理存储状态 添加 PageErrorBoundary 页面级错误边界 实现组件懒加载优化首屏性能 关键重构 路由系统重构 — 支持独立标签页模式,合并路由与功能配置 状态管理优化 — 存储状态增加快照机制、验证器和防抖处理 剪贴板与打印工具 — 统一为 async/await 形式,修复重复触发问题 存储清理器 — 使用 TextEncoder 准确计算 UTF-8 字节数,支持 TB 单位 测试与 CI 测试覆盖 — 为新增工具、公共组件、边界场景补充单元测试 CI 精简 — 移除 Firefox 测试,仅保留 Chrome;修正类型检查命令 --- .claude/settings.local.json | 19 +++++++++ AGENTS.md | 2 +- pages/Base64Converter/FileMode.tsx | 13 +------ pages/Base64Converter/ImageMode.tsx | 13 +------ pages/Base64Converter/TextMode.tsx | 2 +- pages/HtmlToMarkdown/index.tsx | 6 +-- pages/Jwt/index.tsx | 3 -- pages/MarkdownToHtml/index.tsx | 2 +- utils/__tests__/base64Converter.test.ts | 18 +++++++++ utils/__tests__/storageCleaner.test.ts | 52 ++++++++++++++++++++++++- utils/base64Converter.ts | 6 +-- utils/storageCleaner.ts | 3 +- 12 files changed, 101 insertions(+), 38 deletions(-) create mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..c8c774c --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,19 @@ +{ + "permissions": { + "allow": [ + "Bash(git add:*)", + "Bash(git commit:*)", + "Bash(npx vitest:*)", + "Bash(git rm:*)", + "Bash(git stash:*)", + "Bash(git reset:*)", + "Bash(git checkout:*)", + "Bash(npx tsc:*)", + "Bash(npx eslint:*)", + "Bash(npm run:*)", + "Bash(npm test:*)", + "Bash(pnpm typecheck *)", + "Bash(pnpm test *)" + ] + } +} diff --git a/AGENTS.md b/AGENTS.md index e2769b1..e1b4011 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,7 +16,7 @@ This file provides guidance to AI agents (such as Gemini, Codex, etc.) when work - `npm run build:firefox` - 构建 Firefox 浏览器的生产版本 - `npm run zip` - 打包 Chrome 扩展为 ZIP 文件 - `npm run zip:firefox` - 打包 Firefox 扩展为 ZIP 文件 -- `npm run compile` - 执行 TypeScript 类型检查(`tsc --noEmit`) +- `npm run typecheck` - 执行 TypeScript 类型检查(`tsc --noEmit`) - `npm run lint` - 运行 ESLint 静态代码检查 ### 测试 diff --git a/pages/Base64Converter/FileMode.tsx b/pages/Base64Converter/FileMode.tsx index 15d6ef2..1713b51 100644 --- a/pages/Base64Converter/FileMode.tsx +++ b/pages/Base64Converter/FileMode.tsx @@ -272,17 +272,8 @@ export default function FileMode() { {t('base64Output')} - {}} - /> - {}} - /> + + - {}} - /> - {}} - /> + + {outputLabel} - {}} /> + {t('charCount', { count: result.markdownLength })} - {}} - size="small" - /> + diff --git a/pages/Jwt/index.tsx b/pages/Jwt/index.tsx index 4d432a2..65e56b8 100644 --- a/pages/Jwt/index.tsx +++ b/pages/Jwt/index.tsx @@ -11,7 +11,6 @@ import { useTranslation } from 'react-i18next'; interface SectionProps { title: string; content: unknown; - raw: string; color: string; } @@ -99,13 +98,11 @@ export default function Index() {
{t('charCount', { count: result.htmlLength })} - {}} size="small" /> + diff --git a/utils/__tests__/base64Converter.test.ts b/utils/__tests__/base64Converter.test.ts index 37a7ef9..b252238 100644 --- a/utils/__tests__/base64Converter.test.ts +++ b/utils/__tests__/base64Converter.test.ts @@ -77,6 +77,11 @@ describe('base64ToText', () => { expect(result).toBe('hello'); }); + it('应该处理带参数(如 charset)的 data URI 前缀', () => { + const result = base64ToText('data:text/plain;charset=utf-8;base64,aGVsbG8='); + expect(result).toBe('hello'); + }); + it('应该剥离带空白的 data URI 前缀', () => { const result = base64ToText(' data:text/plain;base64,aGVsbG8= '); expect(result).toBe('hello'); @@ -168,6 +173,13 @@ describe('extractMimeTypeFromDataUri', () => { expect(extractMimeTypeFromDataUri('data:text/plain;base64,SGVsbG8=')).toBe('text/plain'); }); + it('应该从带参数的 data URI 中提取 MIME 类型', () => { + expect(extractMimeTypeFromDataUri('data:text/plain;charset=utf-8;base64,SGVsbG8=')).toBe( + 'text/plain', + ); + expect(extractMimeTypeFromDataUri('data:image/svg+xml;base64,PHN2Zw==')).toBe('image/svg+xml'); + }); + it('应该对无效的 data URI 返回默认类型', () => { expect(extractMimeTypeFromDataUri('invalid')).toBe('application/octet-stream'); }); @@ -333,6 +345,12 @@ describe('base64ToBlob', () => { expect(result.rawBase64).toBe('iVBORw0KGgo='); }); + it('应该处理带参数的 data URI 前缀', () => { + const result = base64ToBlob('data:text/plain;charset=utf-8;base64,aGVsbG8='); + expect(result.rawBase64).toBe('aGVsbG8='); + expect(result.mimeType).toBe('text/plain'); + }); + it('blob 大小应该等于解码后的字节数', () => { const result = base64ToBlob('aGVsbG8='); expect(result.blob.size).toBe(5); diff --git a/utils/__tests__/storageCleaner.test.ts b/utils/__tests__/storageCleaner.test.ts index cde4975..4b2d6c2 100644 --- a/utils/__tests__/storageCleaner.test.ts +++ b/utils/__tests__/storageCleaner.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { formatSize, isRestrictedUrl } from '@/utils/storageCleaner'; +import { clearCookies, formatSize, isRestrictedUrl } from '@/utils/storageCleaner'; describe('storageCleaner utils', () => { describe('isRestrictedUrl', () => { @@ -80,4 +80,54 @@ describe('storageCleaner utils', () => { expect(formatSize(1025)).toBe('1.0 KB'); }); }); + + describe('clearCookies', () => { + it('should strip leading dot from cookie domain when removing cookies', async () => { + const mockCookies = [ + { name: 'session', domain: '.example.com', path: '/', secure: true, storeId: '0' }, + { name: 'auth', domain: '.example.com', path: '/api', secure: false, storeId: '0' }, + ]; + (chrome.cookies.getAll as any).mockResolvedValue(mockCookies); + (chrome.cookies.remove as any).mockResolvedValue(undefined); + + const result = await clearCookies('https://example.com'); + + expect(result).toEqual({ success: true, count: 2 }); + expect(chrome.cookies.remove).toHaveBeenCalledWith({ + url: 'https://example.com/', + name: 'session', + storeId: '0', + }); + expect(chrome.cookies.remove).toHaveBeenCalledWith({ + url: 'http://example.com/api', + name: 'auth', + storeId: '0', + }); + }); + + it('should handle domains without leading dot', async () => { + const mockCookies = [ + { name: 'pref', domain: 'sub.example.com', path: '/path', secure: true, storeId: '0' }, + ]; + (chrome.cookies.getAll as any).mockResolvedValue(mockCookies); + (chrome.cookies.remove as any).mockResolvedValue(undefined); + + const result = await clearCookies('https://sub.example.com'); + + expect(result).toEqual({ success: true, count: 1 }); + expect(chrome.cookies.remove).toHaveBeenCalledWith({ + url: 'https://sub.example.com/path', + name: 'pref', + storeId: '0', + }); + }); + + it('should return error when operation fails', async () => { + (chrome.cookies.getAll as any).mockRejectedValue(new Error('Permission denied')); + + const result = await clearCookies('https://example.com'); + + expect(result).toEqual({ success: false, error: 'Error: Permission denied' }); + }); + }); }); diff --git a/utils/base64Converter.ts b/utils/base64Converter.ts index 0f56be6..7d1828e 100644 --- a/utils/base64Converter.ts +++ b/utils/base64Converter.ts @@ -78,8 +78,8 @@ export function textToBase64(text: string): TextToBase64Result { }; } -/** 匹配 data URI 的 base64 前缀,如 "data:image/png;base64," */ -const DATA_URI_BASE64_PREFIX = /^data:[^;,]+;base64,/i; +/** 匹配 data URI 的 base64 前缀,如 "data:image/png;base64,"(允许中间含参数) */ +const DATA_URI_BASE64_PREFIX = /^data:[^,]+;base64,/i; /** * 将 Base64 字符串解码为文本 @@ -208,7 +208,7 @@ export function fileToBase64(file: File): Promise { * @returns MIME 类型 */ export function extractMimeTypeFromDataUri(dataUri: string): string { - const match = dataUri.match(/^data:([^;]+);base64,/); + const match = dataUri.match(/^data:([^;,]+)[^,]*;base64,/); return match ? match[1] : 'application/octet-stream'; } diff --git a/utils/storageCleaner.ts b/utils/storageCleaner.ts index c1d4201..514d2e3 100644 --- a/utils/storageCleaner.ts +++ b/utils/storageCleaner.ts @@ -192,7 +192,8 @@ export async function clearCookies(url: string): Promise { const cookies = await chrome.cookies.getAll({ url }); for (const cookie of cookies) { const protocol = cookie.secure ? 'https:' : 'http:'; - const cookieUrl = `${protocol}//${cookie.domain}${cookie.path}`; + const domain = cookie.domain.startsWith('.') ? cookie.domain.slice(1) : cookie.domain; + const cookieUrl = `${protocol}//${domain}${cookie.path}`; await chrome.cookies.remove({ url: cookieUrl, name: cookie.name,