f9821553b3
* fix(ci): fix release workflow artifact upload issue - Split artifact upload into 3 separate steps (Chrome, Firefox, Source) - Update download steps to match new artifact names - Add if-no-files-found: error for better error handling - Fix Node.js 20 compatibility issue with upload-artifact@v4 glob pattern * docs: 更新 AGENTS.md,添加测试工具和翻译文件结构说明 * docs: 更新 AGENTS.md,完善 CI 步骤和存储键名格式说明 * fix: remove unnecessary animations from StorageCleaner page - Remove entrance animations (animate-in, fade-in, zoom-in) from all components - Remove scale effect (active:scale-[0.99]) from clean button - Remove transition effects (transition-all, transition-colors) from OptionItem, StorageOptionsGrid, AutoRefreshToggle - Remove bouncing animation from error icon - Remove pulsing animation from warning banner - Keep animate-spin on button loading spinner as functional indicator * fix: remove unnecessary animations from RightClickRestorer page * fix: remove all unnecessary animations from all pages - Remove dead code animations (animate-in, fade-in, slide-in, zoom-in, shake) from tailwindcss-animate plugin (not installed) - Remove decorative transition effects (transition-all, transition-colors) from all page components - Remove scale effects (active:scale-95) from buttons - Remove bounce animations (animate-bounce) from icons - Keep functional animate-spin on loading spinners as they provide essential loading feedback Affected pages: Dashboard, Timestamp, Jwt, JsonTools, Base64Converter, HtmlToMarkdown, MarkdownToHtml, QrCode, TextStatistics * docs: 添加项目文档(copilot-instructions、编码规范、各目录 README) - 新增 .github/copilot-instructions.md:Copilot 指令文件,涵盖构建命令、CI 流水线、项目架构、关键规范 - 新增 .github/CODING_STANDARDS.md:完整的代码编写规范文档(TypeScript、React、样式、错误处理、命名、测试、Hook、i18n、存储等 12 个章节) - 新增 11 个目录的 README.md:components、components/ui、config、entrypoints、i18n、lib、pages、providers、public、src、types、utils - 更新 AGENTS.md:补充页面组件模式说明和 components/ui 目录 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(CopyButton): use shadcn buttonVariants instead of duplicated variant classes Remove hand-written variantClasses/sizeClasses that duplicated shadcn's buttonVariants. Now extends ButtonProps and imports buttonVariants from components/ui/button for consistent styling. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(ErrorBoundary): use shadcn destructive tokens instead of hardcoded red colors Replace hardcoded red-200/red-50/red-600 with border-destructive, bg-destructive/5, text-destructive. Use variant='destructive' on Button. Align error log styling with PageErrorBoundary. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(TextInputArea): replace hardcoded Chinese strings with i18n t() calls Use existing translation keys for clear success, copy success, and copy error messages instead of hardcoded Chinese text. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(QrCodePreview): use shadcn Button instead of manual button styles Replace hand-written button class strings with shadcn Button component using outline and default variants. Removes ~15 lines of duplicated Tailwind classes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(GlobalSnackbar): clarify relationship with sonner toast Add note explaining when to use GlobalSnackbar (provider context, severity levels, custom positioning) vs sonner toast (simple one-off messages). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(ImageUploader): wrap handleClearFile with useCallback Prevents unnecessary re-renders by memoizing the callback, consistent with handleFileChange which already uses useCallback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(SwitchButtonGroup): remove redundant containerPadding conditional Both branches of the ternary returned 'p-1'. Inline the constant directly into the className string. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(TopBar): add missing openExtensionPage import The function was called in handleOpenInTab but never imported from utils/chromeTabs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(TopBar): add selectedIndex boundary protection in keyboard nav Guard against out-of-bounds access when search results change during keyboard navigation. Check selectedIndex < totalItems before accessing arrays. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(RouterContainer): remove unnecessary empty-dep useMemo getEntryPointType() is a simple config getter. Replace useMemo(() => ..., []) with a direct call — the empty dep array made the memo pointless. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(i18n): add missing translation keys for error boundaries and copy messages Add keys for errorBoundary, pageErrorBoundary, router, and messages.copyEmpty in both zh and en locales. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(CopyButton): replace hardcoded Chinese strings with i18n t() calls Use useTranslation('common') for tooltip, copy empty/success/error toast messages. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(ErrorBoundary): replace hardcoded Chinese with i18n via withTranslation HOC Use react-i18next withTranslation HOC for class component to translate title, description, and refresh button text. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(PageErrorBoundary): replace hardcoded Chinese with i18n via withTranslation HOC Use react-i18next withTranslation HOC for class component to translate title, description, and retry button text. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(RouterContainer): replace hardcoded Chinese with i18n t() calls Use useTranslation('common') for 404 page title and description with entryPointType interpolation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(pages): 修复 TypeScript 错误、硬编码字符串,移除死代码 - 修复 LiveClock.tsx CopyButton size 属性类型错误 (small → sm) - 修复 ToolCard.tsx 紫色 RGB 值拼写错误 (147,51,2 purple → 147,51,232) - 替换 4 处硬编码中文为 i18n 调用 (StorageCleaner, JsonTools) - MarkdownToHtml 预览链接色改为 CSS 变量以支持暗黑模式 - 移除 Base64Converter 中已被 Base64ConverterSection 替代的死代码 (FileMode.tsx, ImageMode.tsx 及其测试文件) - 更新 README.md 移除对已删除文件的引用 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(CopyButton, LiveClock): 修改按钮大小为 'icon',移除硬编码颜色配置 * refactor(TextInputArea): 复用 CopyButton 组件替换内联复制逻辑 * fix(test): 修复 CopyButton mock 未捕获 clipboard writeText 异常 * refactor: 迁移 i18n 系统从 react-i18next 到 chrome.i18n - 移除 react-i18next、i18next 及相关依赖 - 删除旧的 i18n/ 目录和 useLazyTranslation 工具 - 新增 utils/chromeI18n.ts 类型安全 wrapper(useI18n Hook + getMessage) - 生成 public/_locales/{zh,en}/messages.json(298 个翻译 key) - 批量更新 39+ 组件文件的导入和翻译调用 - 转换翻译键格式:namespace:key → namespace_key - 修复 ErrorBoundary/PageErrorBoundary 从 withTranslation HOC 改为直接调用 getMessage - 更新 vitest.setup.ts mock 加载实际翻译文本 - 修复 11 个测试文件的断言以匹配中文翻译 - TypeScript、ESLint、547 项测试全部通过 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: 移除语言切换按钮 chrome.i18n 语言由浏览器设置决定,不支持运行时切换,移除 TopBar 中的语言切换按钮及相关逻辑。 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: 移除英文翻译,仅保留中文 删除 public/_locales/en/ 目录,清理 chromeI18n.ts 中未使用的语言切换相关导出。 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: 清理未使用的 i18n key(322 → 245) 移除 77 个未被源码引用的翻译 key,包括: - jsonFormat_* (17): 未接入的子模式标题和按钮 - buttons_* (15): 代码使用 common: 命名空间格式,直接 key 未引用 - qrCode_* (14): 未使用的页面标题和状态提示 - timestamp/common/storageCleaner/jsonDiff 等零散未用 key 保留了通过模板字面量动态引用的 key(storageCleaner_options_*、common_buttons_themeMode_*、jsonFormat_*InputPlaceholder 等)。 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: reorganize directory structure into src/ Move all source code directories into src/ for cleaner project structure: - pages/, components/, utils/, config/, providers/, types/, lib/, assets/, entrypoints/ → src/ - Use WXT srcDir config to resolve @/ alias to src/ - Update tsconfig, vitest, eslint, tailwind configs - Remove scattered README.md files from subdirectories - Update documentation (AGENTS.md, CODING_STANDARDS.md, README.md) - Fix pre-existing lint error in RouterProvider.tsx Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: remove meaningless comments Remove 50+ noise comments across the codebase: - AI-generated verbose prose (💡 emoji, '超进化', '大闸', etc.) - Comments that restate what the code obviously does - Comments about deleted code - Import-level noise comments - Overly verbose Chinese section markers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: clean up unused code and simplify imports - Remove unused imports and variables across 35 files - Simplify component logic and remove dead code - Clean up test files by removing unnecessary setup - Streamline CI workflow configuration Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: remove HTML-Markdown conversion features (htmlToMarkdown & markdownToHtml) Removed both conversion tools and all related code: - Deleted page components: HtmlToMarkdown, MarkdownToHtml - Deleted utility functions: htmlToMarkdown.ts, markdownToHtml.ts - Deleted unit tests for both utilities - Removed feature configs and PageType entries from storage.d.ts - Removed preview mode types and StorageSchema keys - Removed all i18n translation keys from messages.json - Removed vendor-markdown chunk from wxt.config.ts - Removed marked dependency from package.json - Updated feature count in tests (11 -> 9, page order 10 -> 8) - Updated README, AGENTS.md, and copilot-instructions.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: redesign Dashboard layout with search and recently used tools - Add search input at top to filter tools by name/description - Add 'recently used' section showing last 3 used tools as compact chips - Add 'all tools' section header for the tool card grid - Track tool usage via new 'app/recentlyUsedTools' storage key - Update navigateTo to record recently used tools (max 3, LRU order) - Add i18n keys: dashboard_searchPlaceholder, dashboard_recentlyUsed, dashboard_allTools Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: replace ToolCard grid with compact icon+name grid on Dashboard - Replace large ToolCard layout with dense grid (3-6 columns responsive) - Each item shows icon + tool name in a compact tile - Hover highlights icon and text with smooth transitions - Remove ToolCard import from Dashboard (component retained for reuse) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: remove redundant search input from Dashboard (header already has one) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: optimize TopBar search with Cmd+K shortcut and improved styling - Add global Cmd/Ctrl+K keyboard shortcut to focus search input - Show keyboard shortcut hint (⌘K) in search input when empty - Improve input styling: rounded-lg, softer borders, focus transitions - Improve dropdown: rounded-lg, larger shadow, better spacing - Larger result icons (h-8 w-8) with rounded-lg containers - Refined history section with uppercase label and muted icon - Search icon fades in on focus via group-focus-within Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: improve search history to show recently used tools with direct navigation - Store feature keys (PageType) in history instead of translated labels - Show icon + name + description in history items (same as search results) - Click or Enter on history item navigates directly to the tool page - Remove History icon import (no longer needed) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: useI18n fallback for empty string translations from chrome.i18n * refactor: 移除设置页面 - 删除 options 入口点目录及相关文件 - 移除 wxt.config.ts 中 options_ui 配置 - 移除 TopBar 组件中的设置按钮和 onOpenOptions prop - 移除 popup/sidepanel App.tsx 中的 handleOpenOptions - 移除相关测试中的设置页面引用 - 移除 messages.json 中设置相关翻译键 - 通过 lint、typecheck、test 验证 * docs: 更新文档,移除设置页面引用 - README.md: 移除 entrypoints/options/ 目录说明 - AGENTS.md: 更新项目结构描述,移除 options/ 入口点 * docs: add comprehensive page development standards (CODING_STANDARDS.md §11) - 新增 §11 页面开发规范(10 个子节),覆盖目录结构分4级、入口组件命名统一为 Index、业务 Hook 内部结构顺序、常量/子组件/存储键/模式切换/布局约定等规范 - 新增 §11.9 页面开发检查清单(10 步逐项确认) - 更新 AGENTS.md 和 copilot-instructions.md 同步引用新规范章节 - 修正 useLazyTranslation → useI18n 与实际代码保持一致 * refactor(pages): apply UI+Hook separation and unify component naming - Dashboard, RightClickRestorer: rename page component to Index - Jwt: extract useJwt.ts hook + JwtSection.tsx sub-component (127→76 lines) - TextStatistics: extract useTextStatistics.ts hook (63→48 lines) - JsonTools: extract useJsonTools.ts hook + constants.ts (199→109 lines) - All pages now follow CODING_STANDARDS.md §11 pattern * refactor(StorageCleaner): improve style consistency and checkbox alignment - Group storage options, auto-refresh toggle, and clean button into unified card - Remove card wrappers from StorageOptionsGrid and AutoRefreshToggle (now sections within parent card) - Align OptionItem and Select All checkboxes with consistent px-3.5 padding - Distinguish indeterminate vs checked checkbox states visually - Standardize spacing (space-y-4), add select-none to page root - Fix import path for StorageCleanerConfirm to relative ./ * fix(StorageCleaner, Base64Converter): fix layout padding and checkbox alignment - StorageCleaner: add px-3.5 to grid container to prevent content sticking to border - StorageCleaner: adjust select-all bar to pl-3.5 pr-7 for checkbox alignment with grid items - Base64Converter: add px-2 to TextMode and Base64ConverterSection for consistent spacing Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: add initial CLAUDE.md file with reference to @AGENT.md * docs: add comprehensive visual style guide Add VISUAL_STYLE_GUIDE.md to standardize UI development: - Design principles and keywords - Complete color system (CSS variables, semantic usage) - Typography hierarchy and patterns - Spacing, layout, and container specs - Border radius and shadow system - Component specs (Button, Input, Card, Dialog, etc.) - Interaction and animation guidelines - Dark mode implementation rules - Tool color palette and assignment - Code organization standards - Anti-patterns checklist - Quick reference appendix Update .gitignore to allow tracking the style guide. * docs(AGENTS): update i18n section to reflect chrome.i18n migration - Replace react-i18next docs with chrome.i18n API documentation - Update translation file paths from i18n/locales/ to public/_locales/ - Update test mock description for chromeI18n - Remove references to useLazyTranslation, vendor-i18n chunk, and snackbar Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(content): simplify content script structure - Remove messageHandler.ts (6-line pass-through facade) - Import contextMenuHandler directly from content.ts - Replace local getI18nText() with shared getMessage() from utils/chromeI18n Reduces content script files from 3 to 2 and eliminates i18n logic duplication. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(RightClickRestorer): simplify setDomain logic Replace nested if/try/catch with a single ternary expression. isUnsupportedPage() already handles invalid URLs, so new URL() is safe here. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(background): extract main world injection script Move the 62-line inline main world injection function from background.ts to a dedicated utils/rightClickInjection.ts file. This separates the RightClickRestorer's DOM patching logic from the service worker's message routing concerns. - background.ts: -62 lines, now delegates to imported function - utils/rightClickInjection.ts: new file with mainWorldInjectionScript() Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(background): simplify RELOAD_TAB with setTimeout Replace the 30-line chrome.alarms API implementation with a simple setTimeout for delayed tab reloads. Rationale: The only usage is a 1-second delay after storage cleaning. Service workers don't go idle within 1 second, so alarms' persistence benefit is unnecessary here. - Removed: alarm naming, create/clear, listener register/unregister - Added: 4-line setTimeout conditional Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(storageCleaner): extract executeScript helpers Extract two generic helpers to eliminate repetitive executeScript boilerplate: - runScript<T>(): for size/query operations (5 functions) - runCleanScript(): for cleanup operations (5 functions) Also add CLEAN_OPTION_KEYS constant shared by formatCleaningResult and isEmptyResult. - 428 lines → 379 lines (-49 lines) - Eliminates ~100 lines of duplicated try/catch/result parsing Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(base64Converter): simplify utility functions - Mark formatFileSize() as @deprecated, delegate to formatBytes - Replace manual for-loop in base64ToBytes() with Uint8Array.from() - Replace nested loops in sniffMimeFromBytes() with Array.every() 5 insertions(+), 23 deletions(-) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor: replace GlobalSnackbar with sonner toast Remove the custom 373-line GlobalSnackbar component and its Provider/Hook in favor of the already-installed sonner library. Changes: - Delete src/components/GlobalSnackbar.tsx (+ tests) - Remove SnackbarProvider from popup/App.tsx and sidepanel/App.tsx - Replace useSnackbar() calls with toast.success()/toast.error() in: - ImageUploader.tsx - useQrCode.ts - ParsePanel.tsx - LiveClock.tsx - Update test mocks to use sonner instead of GlobalSnackbar -554 lines, +43 lines (net -511 lines) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(RouterContainer): remove unnecessary useMemo and hoist constant - Replace useMemo with direct expression for animationClass - Move entryPointType to module scope (value is fixed after mount) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor: remove unnecessary useMemo calls across components Replace over-cached simple computations with direct expressions: - popup/App.tsx: getEntryPointType() and routerConfig (values are stable) - Dashboard/index.tsx: visibleSet, visibleFeatures, recentFeatures (Set creation + array filter/map are cheaper than useMemo overhead) - useTextStatistics.ts: getTextStats(text) (simple string processing) - ThemeModeProvider.tsx: contextValue object (setMode is stable via useCallback) -39 lines, +28 lines (net -11 lines) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor: unify formatBytes usage and simplify useContextMenuData 1. Remove formatSize/formatByteSize delegation functions: - storageCleaner.ts:formatSize (was just formatBytes wrapper) - textStatistics.ts:formatByteSize (was just formatBytes wrapper) - Update all callers to import formatBytes directly from @/utils/format 2. Simplify useContextMenuData hook: - Remove unnecessary useCallback wrapping - Inline checkAndConsumeData logic directly in useEffect - Eliminate callback->effect dependency cycle -69 lines, +67 lines (net -2 lines, but removes 2 indirection layers) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(storageCleaner): simplify architecture and improve type semantics - Remove RELOAD_TAB message chain, use chrome.tabs.reload() directly - Rename IndexedDB label to '站点存储' for accuracy - Introduce StorageSizeInfo type to distinguish bytes vs count - Rename totalSize to totalBytes for clarity - Merge runCleanScript into runScript to reduce duplication - Rename CleaningResult.success to overallSuccess to avoid confusion - Remove unused domain state and setDomain call Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor: replace require with import for JSON module in vitest.setup.ts * refactor: simplify Timestamp and RightClickRestorer Timestamp: - Extract shared utilities (msToUnit, dayjsFromTimestamp, ModeType) - Remove unnecessary useCallback/useMemo/React.memo - Remove extra info section (relative time, ISO 8601, UTC) - Fix missing translation key timestamp_unitS RightClickRestorer: - Remove website badge (右键已解锁 overlay) - Remove mouse penetration logic for media elements - Remove unnecessary useCallback - Remove redundant setIsLoading call Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(StorageCleanerConfirm.test.tsx): update IndexedDB label to Chinese translation --------- Co-authored-by: Ubuntu <ubuntu@localhost.localdomain> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
391 lines
10 KiB
TypeScript
391 lines
10 KiB
TypeScript
import type { CleaningResult, StorageCleanerOptions, StorageCleanResult } from '@/types/storage';
|
|
|
|
const RESTRICTED_PROTOCOLS = [
|
|
'chrome:',
|
|
'chrome-extension:',
|
|
'about:',
|
|
'edge:',
|
|
'view-source:',
|
|
'file:',
|
|
'data:',
|
|
] as const;
|
|
|
|
/** 清理选项的 key 列表(用于遍历结果) */
|
|
const CLEAN_OPTION_KEYS: (keyof StorageCleanerOptions)[] = [
|
|
'localStorage',
|
|
'sessionStorage',
|
|
'indexedDB',
|
|
'cookies',
|
|
'cacheStorage',
|
|
'serviceWorkers',
|
|
];
|
|
|
|
export async function getCurrentTab() {
|
|
// For popup pages, we need to get the active tab from the browser window that triggered the popup.
|
|
// We should ONLY care about the currently active tab in the last focused window.
|
|
// If it's a restricted URL, we return it anyway and let the caller handle the error display.
|
|
|
|
const [tab] = await chrome.tabs.query({
|
|
active: true,
|
|
lastFocusedWindow: true,
|
|
});
|
|
|
|
if (tab) {
|
|
return tab;
|
|
}
|
|
|
|
// Fallback for cases where lastFocusedWindow might not work as expected (e.g. certain sidepanel scenarios)
|
|
const [fallbackTab] = await chrome.tabs.query({
|
|
active: true,
|
|
currentWindow: true,
|
|
});
|
|
|
|
return fallbackTab;
|
|
}
|
|
|
|
export function isRestrictedUrl(url?: string): boolean {
|
|
if (!url) return true;
|
|
return RESTRICTED_PROTOCOLS.some((p) => url.startsWith(p));
|
|
}
|
|
|
|
export async function getCookieSize(url: string): Promise<number> {
|
|
try {
|
|
const cookies = await chrome.cookies.getAll({ url });
|
|
const encoder = new TextEncoder();
|
|
// 估算:名称 + 值 + 域名 + 路径 的 UTF-8 字节数
|
|
return cookies.reduce(
|
|
(acc, c) =>
|
|
acc +
|
|
encoder.encode(c.name).length +
|
|
encoder.encode(c.value).length +
|
|
encoder.encode(c.domain ?? '').length +
|
|
encoder.encode(c.path ?? '').length,
|
|
0,
|
|
);
|
|
} catch (error) {
|
|
console.error('Failed to get cookie size:', error);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 通用辅助:在指定标签页中执行脚本并返回结果
|
|
*
|
|
* @param tabId 标签页 ID
|
|
* @param func 在页面上下文中执行的函数
|
|
* @param errorLabel 错误日志前缀
|
|
* @param fallback 执行失败时的回退值
|
|
*/
|
|
async function runScript<T>(
|
|
tabId: number,
|
|
func: () => T | Promise<T>,
|
|
errorLabel: string,
|
|
fallback: T,
|
|
): Promise<T> {
|
|
try {
|
|
const [result] = await chrome.scripting.executeScript({ target: { tabId }, func });
|
|
return (result?.result as T) ?? fallback;
|
|
} catch (error) {
|
|
console.error(`Failed to ${errorLabel}:`, error);
|
|
return fallback;
|
|
}
|
|
}
|
|
|
|
export async function getLocalStorageSize(tabId: number): Promise<number> {
|
|
return runScript(
|
|
tabId,
|
|
() => {
|
|
try {
|
|
const encoder = new TextEncoder();
|
|
return Object.entries(localStorage).reduce(
|
|
(acc, [k, v]) => acc + encoder.encode(k).length + encoder.encode(v).length,
|
|
0,
|
|
);
|
|
} catch {
|
|
return 0;
|
|
}
|
|
},
|
|
'get LocalStorage size',
|
|
0,
|
|
);
|
|
}
|
|
|
|
export async function getSessionStorageSize(tabId: number): Promise<number> {
|
|
return runScript(
|
|
tabId,
|
|
() => {
|
|
try {
|
|
const encoder = new TextEncoder();
|
|
return Object.entries(sessionStorage).reduce(
|
|
(acc, [k, v]) => acc + encoder.encode(k).length + encoder.encode(v).length,
|
|
0,
|
|
);
|
|
} catch {
|
|
return 0;
|
|
}
|
|
},
|
|
'get SessionStorage size',
|
|
0,
|
|
);
|
|
}
|
|
|
|
export async function getOriginStorageEstimate(tabId: number): Promise<number> {
|
|
return runScript(
|
|
tabId,
|
|
async () => {
|
|
try {
|
|
if (navigator.storage && navigator.storage.estimate) {
|
|
const estimate = await navigator.storage.estimate();
|
|
return estimate.usage || 0;
|
|
}
|
|
return 0;
|
|
} catch {
|
|
return 0;
|
|
}
|
|
},
|
|
'get origin storage estimate',
|
|
0,
|
|
);
|
|
}
|
|
|
|
export async function getCacheStorageSize(tabId: number): Promise<number> {
|
|
return runScript(
|
|
tabId,
|
|
async () => {
|
|
try {
|
|
if ('caches' in window) {
|
|
const keys = await caches.keys();
|
|
return keys.length;
|
|
}
|
|
return 0;
|
|
} catch {
|
|
return 0;
|
|
}
|
|
},
|
|
'get CacheStorage size',
|
|
0,
|
|
);
|
|
}
|
|
|
|
export async function getServiceWorkerCount(tabId: number): Promise<number> {
|
|
return runScript(
|
|
tabId,
|
|
async () => {
|
|
try {
|
|
if ('serviceWorker' in navigator) {
|
|
const regs = await navigator.serviceWorker.getRegistrations();
|
|
return regs.length;
|
|
}
|
|
return 0;
|
|
} catch {
|
|
return 0;
|
|
}
|
|
},
|
|
'get ServiceWorker count',
|
|
0,
|
|
);
|
|
}
|
|
|
|
export async function clearCookies(url: string): Promise<StorageCleanResult> {
|
|
try {
|
|
const cookies = await chrome.cookies.getAll({ url });
|
|
for (const cookie of cookies) {
|
|
const protocol = cookie.secure ? 'https:' : 'http:';
|
|
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,
|
|
storeId: cookie.storeId,
|
|
});
|
|
}
|
|
return { success: true, count: cookies.length };
|
|
} catch (error) {
|
|
return { success: false, error: String(error) };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 通用辅助:执行清理脚本并解析结果
|
|
*
|
|
* @param tabId 标签页 ID
|
|
* @param func 在页面上下文中执行的清理函数
|
|
* @param errorLabel 错误日志前缀
|
|
*/
|
|
async function runCleanScript(
|
|
tabId: number,
|
|
func: () => { count: number } | Promise<{ count: number }>,
|
|
errorLabel: string,
|
|
): Promise<StorageCleanResult> {
|
|
const raw = await runScript(tabId, func, errorLabel, { count: 0 });
|
|
if (raw && typeof raw === 'object' && 'count' in raw) {
|
|
return { success: true, count: raw.count };
|
|
}
|
|
return { success: false, error: 'No result returned' };
|
|
}
|
|
|
|
export async function injectClearLocalStorage(tabId: number): Promise<StorageCleanResult> {
|
|
return runCleanScript(
|
|
tabId,
|
|
() => {
|
|
const count = localStorage.length;
|
|
localStorage.clear();
|
|
return { count };
|
|
},
|
|
'clear LocalStorage',
|
|
);
|
|
}
|
|
|
|
export async function injectClearSessionStorage(tabId: number): Promise<StorageCleanResult> {
|
|
return runCleanScript(
|
|
tabId,
|
|
() => {
|
|
const count = sessionStorage.length;
|
|
sessionStorage.clear();
|
|
return { count };
|
|
},
|
|
'clear SessionStorage',
|
|
);
|
|
}
|
|
|
|
export async function injectClearIndexedDB(tabId: number): Promise<StorageCleanResult> {
|
|
return runCleanScript(
|
|
tabId,
|
|
async () => {
|
|
if (typeof indexedDB.databases !== 'function') {
|
|
return { count: 0 };
|
|
}
|
|
const databases = await indexedDB.databases();
|
|
let count = 0;
|
|
for (const db of databases) {
|
|
if (!db.name) continue;
|
|
const dbName = db.name;
|
|
try {
|
|
await new Promise<void>((resolve, reject) => {
|
|
const deleteReq = indexedDB.deleteDatabase(dbName);
|
|
const timeout = setTimeout(() => {
|
|
console.warn('IndexedDB delete timeout:', dbName);
|
|
resolve();
|
|
}, 5000);
|
|
deleteReq.onblocked = () => {
|
|
console.warn('IndexedDB delete blocked:', dbName);
|
|
clearTimeout(timeout);
|
|
resolve();
|
|
};
|
|
deleteReq.onsuccess = () => {
|
|
clearTimeout(timeout);
|
|
resolve();
|
|
};
|
|
deleteReq.onerror = () => {
|
|
clearTimeout(timeout);
|
|
reject(new Error(`Failed to delete ${dbName}`));
|
|
};
|
|
});
|
|
count++;
|
|
} catch (e) {
|
|
console.error('Delete DB error:', e);
|
|
}
|
|
}
|
|
return { count };
|
|
},
|
|
'clear IndexedDB',
|
|
);
|
|
}
|
|
|
|
export async function injectClearCacheStorage(tabId: number): Promise<StorageCleanResult> {
|
|
return runCleanScript(
|
|
tabId,
|
|
async () => {
|
|
if ('caches' in window) {
|
|
const cacheNames = await caches.keys();
|
|
for (const name of cacheNames) {
|
|
await caches.delete(name);
|
|
}
|
|
return { count: cacheNames.length };
|
|
}
|
|
return { count: 0 };
|
|
},
|
|
'clear CacheStorage',
|
|
);
|
|
}
|
|
|
|
export async function injectUnregisterServiceWorkers(tabId: number): Promise<StorageCleanResult> {
|
|
return runCleanScript(
|
|
tabId,
|
|
async () => {
|
|
if ('serviceWorker' in navigator) {
|
|
const registrations = await navigator.serviceWorker.getRegistrations();
|
|
for (const registration of registrations) {
|
|
await registration.unregister();
|
|
}
|
|
return { count: registrations.length };
|
|
}
|
|
return { count: 0 };
|
|
},
|
|
'unregister ServiceWorkers',
|
|
);
|
|
}
|
|
|
|
export async function clearStorage(
|
|
tabId: number,
|
|
url: string,
|
|
options: StorageCleanerOptions,
|
|
): Promise<CleaningResult> {
|
|
const result: CleaningResult = { overallSuccess: true };
|
|
|
|
if (options.localStorage) {
|
|
result.localStorage = await injectClearLocalStorage(tabId);
|
|
}
|
|
if (options.sessionStorage) {
|
|
result.sessionStorage = await injectClearSessionStorage(tabId);
|
|
}
|
|
if (options.indexedDB) {
|
|
result.indexedDB = await injectClearIndexedDB(tabId);
|
|
}
|
|
if (options.cookies) {
|
|
result.cookies = await clearCookies(url);
|
|
}
|
|
if (options.cacheStorage) {
|
|
result.cacheStorage = await injectClearCacheStorage(tabId);
|
|
}
|
|
if (options.serviceWorkers) {
|
|
result.serviceWorkers = await injectUnregisterServiceWorkers(tabId);
|
|
}
|
|
|
|
const failures = Object.values(result).filter(
|
|
(r): r is StorageCleanResult => r?.success === false,
|
|
);
|
|
if (failures.length > 0) {
|
|
result.overallSuccess = false;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
export function formatCleaningResult(
|
|
result: CleaningResult,
|
|
t: (key: string, options?: Record<string, unknown>) => string,
|
|
): string {
|
|
const parts: string[] = [];
|
|
|
|
for (const key of CLEAN_OPTION_KEYS) {
|
|
const r = result[key];
|
|
if (r?.success && r.count > 0) {
|
|
parts.push(`${r.count} ${t(`storageCleaner:options.${key}`)}`);
|
|
}
|
|
}
|
|
|
|
if (parts.length === 0) {
|
|
return t('storageCleaner:noDataToClean');
|
|
}
|
|
|
|
return t('storageCleaner:cleanedSummary', { items: parts.join(', ') });
|
|
}
|
|
|
|
export function isEmptyResult(result: CleaningResult): boolean {
|
|
const values = Object.values(result).filter(
|
|
(r): r is StorageCleanResult => r?.success === true && r.count > 0,
|
|
);
|
|
return values.length === 0;
|
|
}
|