From 1e4a9375df4b6c007fe6d2e47fb67dc1e5e623f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=A8=E9=9C=96=E9=93=83?= Date: Fri, 20 Mar 2026 00:37:20 +0800 Subject: [PATCH] docs: add storage cleaner implementation plan - Add TypeScript types task - Add cookies permission task - Create storage cleaning utilities task - Create StorageCleanerPage component task - Update App.tsx with tab switching task - Build and test task - Add testing checklist and rollback plan Co-Authored-By: Claude Opus 4.6 --- .../plans/2026-03-20-storage-cleaner-plan.md | 854 ++++++++++++++++++ 1 file changed, 854 insertions(+) create mode 100644 docs/superpowers/plans/2026-03-20-storage-cleaner-plan.md diff --git a/docs/superpowers/plans/2026-03-20-storage-cleaner-plan.md b/docs/superpowers/plans/2026-03-20-storage-cleaner-plan.md new file mode 100644 index 0000000..7f9e5cd --- /dev/null +++ b/docs/superpowers/plans/2026-03-20-storage-cleaner-plan.md @@ -0,0 +1,854 @@ +# Storage Cleaner Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagentation-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a storage cleaner feature to the browser extension popup that allows users to clear localStorage, sessionStorage, IndexedDB, Cookies, Cache Storage, andress Workers for the current page. + +**Architecture:** Add new StorageCleanerPage component with tab switching in the popup, using chrome.cookies API for cookies and script injection for other storage types. User preferences are persisted using Chrome Storage. + +**Tech Stack:** React 19 + TypeScript, Material UI, Chrome Extension APIs + +--- + +## File Structure + +``` +entrypoints/popup/ +├── App.tsx (modify: add tab switching) +└── pages/ + ├── TimestampPage.tsx (no change) + └── StorageCleanerPage.tsx (create: new storage cleaner page) + +types/ +└── storage.d.ts (modify: add storage cleaner types) + +utils/ +└── storageCleaner.ts (create: storage cleaning utilities) + +wxt.config.ts (modify: add cookies permission) +``` + +--- + +## Task 1: Add TypeScript Types for Storage Cleaner + +**Files:** + +- Modify: `types/storage.d.ts` + +- [ ] **Step 1: Add storage cleaner types to StorageSchema and interfaces** + +```typescript +export interface StorageSchema { + 'app/lastRoute': string; + 'app/theme': string; + 'storageCleaner/preferences': StorageCleanerPreferences; +} + +export interface StorageCleanerPreferences { + autoRefresh: boolean; + selectedTypes: StorageCleanerOptions; +} + +export interface StorageCleanerOptions { + localStorage: boolean; + sessionStorage: boolean; + indexedDB: boolean; + cookies: boolean; + cacheStorage: boolean; + serviceWorkers: boolean; +} + +export type StorageCleanResult = + | { + success: true; + count: number; + } + | { + success: false; + error: string; + }; + +export interface CleaningResult { + success: boolean; + error?: string; + localStorage?: StorageCleanResult; + sessionStorage?: StorageCleanResult; + indexedDB?: StorageCleanResult; + cookies?: StorageCleanResult; + cacheStorage?: StorageCleanResult; + serviceWorkers?: StorageCleanResult; +} +``` + +- [ ] **Step 2: Commit TypeScript types** + +```bash +git add types/storage.d.ts +git commit -m "feat: add TypeScript types for storage cleaner" +``` + +--- + +## Task 2: Add Cookies Permission to Manifest + +**Files:** + +- Modify: `wxt.config.ts:10-18` + +- [ ] **Step 1: Add 'cookies' permission to manifest** + +```typescript +permissions: [ + 'storage', + 'unlimitedStorage', + 'clipboardWrite', + 'activeTab', + 'scripting', + 'tabs', + 'debugger', + 'cookies', // Add this line +], +``` + +- [ ] **Step 2: Test build to ensure manifest is valid** + +Run: `npm run compile` +Expected: No TypeScript errors + +- [ ] **Step 3: Commit manifest changes** + +```bash +git add wxt.config.ts +git commit -m "feat: add cookies permission to manifest" +``` + +--- + +## Task 3: Create Storage Cleaning Utilities + +**Files:** + +- Create: `utils/storageCleaner.ts` + +- [ ] **Step 1: Create storage cleaning utility file with helper functions** + +```typescript +import type { StorageCleanerOptions, CleaningResult, StorageCleanResult } from 'types/storage'; + +const RESTRICTED_PROTOCOLS = [ + 'chrome:', + 'chrome-extension:', + 'about:', + 'edge:', + 'view-source:', + 'file:', + 'data:', +] as const; + +export async function getCurrentTab() { + const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); + return tab; +} + +export function isRestrictedUrl(url?: string): boolean { + if (!url) return true; + return RESTRICTED_PROTOCOLS.some((p) => url.startsWith(p)); +} + +export async function clearCookies(url: string): Promise { + try { + const cookies = await chrome.cookies.getAll({ url }); + for (const cookie of cookies) { + await chrome.cookies.remove({ + url, + name: cookie.name, + storeId: cookie.storeId, + }); + } + return { success: true, count: cookies.length }; + } catch (error) { + return { success: false, error: String(error) }; + } +} + +export async function injectClearLocalStorage(tabId: number): Promise { + try { + const result = await chrome.scripting.executeScript<{ count: number }>({ + target: { tabId }, + func: () => { + const count = localStorage.length; + localStorage.clear(); + return { count }; + }, + }); + if (result.result) { + return { success: true, count: result.result.count }; + } + return { success: false, error: 'No result returned' }; + } catch (error) { + return { success: false, error: String(error) }; + } +} + +export async function injectClearSessionStorage(tabId: number): Promise { + try { + const result = await chrome.scripting.executeScript<{ count: number }>({ + target: { tabId }, + func: () => { + const count = sessionStorage.length; + sessionStorage.clear(); + return { count }; + }, + }); + if (result.result) { + return { success: true, count: result.result.count }; + } + return { success: false, error: 'No result returned' }; + } catch (error) { + return { success: false, error: String(error) }; + } +} + +export async function injectClearIndexedDB(tabId: number): Promise { + try { + const result = await chrome.scripting.executeScript<{ count: number } | { error: string }>({ + target: { tabId }, + func: () => { + if (typeof indexedDB.databases === 'function') { + return indexedDB.databases().then(async (databases) => { + let count = 0; + for (const db of databases) { + await new Promise((resolve, reject) => { + const deleteReq = indexedDB.deleteDatabase(db.name); + deleteReq.onblocked = () => { + console.warn('IndexedDB delete blocked:', db.name); + }; + deleteReq.onsuccess = () => resolve(); + deleteReq.onerror = () => reject(); + }); + count++; + } + return { count }; + }); + } + return { error: 'databases_api_unavailable' }; + }, + }); + if (result.result) { + if ('error' in result.result) { + return { success: false, error: result.result.error }; + } + return { success: true, count: result.result.count }; + } + return { success: false, error: 'No result returned' }; + } catch (error) { + return { success: false, error: String(error) }; + } +} + +export async function injectClearCacheStorage(tabId: number): Promise { + try { + const result = await chrome.scripting.executeScript<{ count: number }>({ + target: { tabId }, + func: 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 }; + }, + }); + if (result.result) { + return { success: true, count: result.result.count }; + } + return { success: false, error: 'No result returned' }; + } catch (error) { + return { success: false, error: String(error) }; + } +} + +export async function injectUnregisterServiceWorkers(tabId: number): Promise { + try { + const result = await chrome.scripting.executeScript<{ count: number }>({ + target: { tabId }, + func: 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 }; + }, + }); + if (result.result) { + return { success: true, count: result.result.count }; + } + return { success: false, error: 'No result returned' }; + } catch (error) { + return { success: false, error: String(error) }; + } +} + +export async function clearStorage( + tabId: number, + url: string, + options: StorageCleanerOptions, +): Promise { + const result: CleaningResult = { success: 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); + } + + // Check if any operation failed + const failures = Object.values(result).filter( + (r): r is StorageCleanResult => r?.success === false, + ); + + if (failures.length > 0) { + result.success = false; + result.error = '部分清理失败'; + } + + return result; +} + +export function formatCleaningResult(result: CleaningResult): string { + const parts: string[] = []; + + if (result.localStorage?.success) { + parts.push(`${result.localStorage.count} 个 localStorage`); + } + if (result.sessionStorage?.success) { + parts.push(`${result.sessionStorage.count} 个 sessionStorage`); + } + if (result.indexedDB?.success) { + parts.push(`${result.indexedDB.count} 个 IndexedDB`); + } + if (result.cookies?.success) { + parts.push(`${result.cookies.count} 个 Cookies`); + } + if (result.cacheStorage?.success) { + parts.push(`${result.cacheStorage.count} 个 Cache`); + } + if (result.serviceWorkers?.success) { + parts.push(`${result.serviceWorkers.count} 个 Service Workers`); + } + + if (parts.length === 0) { + return '该页面没有可清理的存储数据'; + } + + return `清理了 ${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; +} +``` + +- [ ] **Step 2: Commit storage cleaning utilities** + +```bash +git add utils/storageCleaner.ts +git commit -m "feat: add storage cleaning utility functions" +``` + +--- + +## Task 4: Create StorageCleanerPage Component + +**Files:** + +- Create: `entrypoints/popup/pages/StorageCleanerPage.tsx` + +- [ ] **Step 1: Create StorageCleanerPage component with UI and logic** + +```typescript +import { useState, useEffect, useCallback } from 'react'; +import { + Paper, + Typography, + Box, + Checkbox, + Button, + FormControlLabel, + Alert, + Snackbar, +} from '@mui/material'; +import RefreshIcon from '@mui/icons-material/Refresh'; +import WarningIcon from '@mui/icons-material/Warning'; +import { storageUtil } from '@/utils/chromeStorage'; +import type { StorageCleanerOptions, CleaningResult, StorageCleanerPreferences } from 'types/storage'; +import { + getCurrentTab, + isRestrictedUrl, + clearStorage, + formatCleaningResult, + isEmptyResult, +} from '@/utils/storageCleaner'; + +const DEFAULT_OPTIONS: StorageCleanerOptions = { + localStorage: true, + sessionStorage: true, + indexedDB: true, + cookies: true, + cacheStorage: true, + serviceWorkers: true, +}; + +const DEFAULT_PREFERENCES: StorageCleanerPreferences = { + autoRefresh: true, + selectedTypes: DEFAULT_OPTIONS, +}; + +export default function StorageCleanerPage() { + const [domain, setDomain] = useState(''); + const [error, setError] = useState(''); + const [options, setOptions] = useState(DEFAULT_OPTIONS); + const [autoRefresh, setAutoRefresh] = useState(true); + const [loading, setLoading] = useState(false); + const [result, setResult] = useState(null); + const [showConfirm, setShowConfirm] = useState(false); + const [snackbar, setSnackbar] = useState<{ open: boolean; message: string }>({ + open: false, + message: '', + }); + + // Load tab info and user preferences + useEffect(() => { + const loadInfo = async () => { + const tab = await getCurrentTab(); + + if (!tab || !tab.url) { + setError('无法获取当前标签页'); + return; + } + + if (isRestrictedUrl(tab.url)) { + setError('存储清理功能不支持此页面'); + return; + } + + setDomain(new URL(tab.url).hostname); + + // Load user preferences + const prefs = await storageUtil.get( + 'storageCleaner/preferences', + DEFAULT_PREFERENCES, + ); + setAutoRefresh(prefs.autoRefresh); + setOptions(prefs.selectedTypes); + }; + + loadInfo(); + }, []); + + const handleOptionChange = useCallback((key: keyof StorageCleanerOptions) => { + setOptions((prev) => ({ ...prev, [key]: !prev[key] })); + }, []); + + const handleClean = useCallback(async () => { + const tab = await getCurrentTab(); + + if (!tab || !tab.id || !tab.url) { + setSnackbar({ open: true, message: '无法获取当前标签页' }); + return; + } + + setLoading(true); + + try { + const cleaningResult = await clearStorage(tab.id, tab.url, options); + setResult(cleaningResult); + + // Save user preferences + await storageUtil.set('storageCleaner/preferences', { + autoRefresh, + selectedTypes: options, + }); + + // Auto refresh if enabled + if (autoRefresh && cleaningResult.success) { + setSnackbar({ open: true, message: '页面即将刷新,Popup 将关闭' }); + setTimeout(() => { + chrome.tabs.reload(tab.id); + }, 1500); + } + } catch (err) { + setSnackbar({ open: true, message: `清理失败: ${String(err)}` }); + } finally { + setLoading(false); + setShowConfirm(false); + } + }, [options, autoRefresh]); + + const handleRefresh = useCallback(async () => { + const tab = await getCurrentTab(); + if (tab?.id) { + setSnackbar({ open: true, message: '页面即将刷新,Popup 将关闭' }); + setTimeout(() => { + chrome.tabs.reload(tab.id); + }, 1500); + } + }, []); + + if (error) { + return ( + + }> + {error} + + + ); + } + + return ( + + {/* Header */} + + + 存储清理 + + + 当前页面: {domain || '加载中...'} + + + + {/* Storage Type Options */} + + + 选择要清理的存储类型: + + + handleOptionChange('localStorage')} + /> + } + label="localStorage" + /> + handleOptionChange('sessionStorage')} + /> + } + label="sessionStorage" + /> + handleOptionChange('indexedDB')} + /> + } + label="IndexedDB" + /> + handleOptionChange('cookies')} + /> + } + label="Cookies" + /> + handleOptionChange('cacheStorage')} + /> + } + label="Cache Storage" + /> + handleOptionChange('serviceWorkers')} + /> + } + label="Service Workers" + /> + + + + {/* Auto Refresh Option */} + + setAutoRefresh(e.target.checked)} + /> + } + label="清理完成后自动刷新页面" + /> + + + {/* Action Buttons */} + + + + + {/* Result Display */} + {result && ( + + + {result.success ? formatCleaningResult(result) : result.error || '清理失败'} + + {!autoRefresh && result.success && ( + + )} + + )} + + {/* Confirmation Dialog */} + {showConfirm && ( + + 确认清理 + + 确定要清理选中的存储数据吗?此操作不可撤销。 + + + + + + + )} + + {/* Snackbar */} + setSnackbar({ ...snackbar, open: false })} + > + + {snackbar.message} + + + + ); +} +``` + +- [ ] **Step 2: Commit StorageCleanerPage component** + +```bash +git add entrypoints/popup/pages/StorageCleanerPage.tsx +git commit -m "feat: add StorageCleanerPage component" +``` + +--- + +## Task 5: Update App.tsx with Tab Switching + +**Files:** + +- Modify: `entrypoints/popup/App.tsx` + +- [ ] **Step 1: Add tab switching logic to App.tsx** + +```typescript +import { useState } from 'react'; +import { Box } from '@mui/material'; +import TimestampPage from './pages/TimestampPage'; +import StorageCleanerPage from './pages/StorageCleanerPage'; +import './App.css'; + +type PageType = 'timestamp' | 'storageCleaner'; + +function App() { + const [currentPage, setCurrentPage] = useState('timestamp'); + + return ( +
+ + + + + {currentPage === 'timestamp' && } + {currentPage === 'storageCleaner' && } +
+ ); +} + +export default App; +``` + +- [ ] **Step 2: Run type check** + +Run: `npm run compile` +Expected: No TypeScript errors + +- [ ] **Step 3: Commit App.tsx changes** + +```bash +git add entrypoints/popup/App.tsx +git commit -m "feat: add tab switching to App component" +``` + +--- + +## Task 6: Build and Test + +**Files:** + +- No file changes + +- [ ] **Step 1: Build the extension** + +Run: `npm run build` +Expected: Build succeeds with no errors + +- [ ] **Step 2: Run lint check** + +Run: `npm run lint` +Expected: No linting errors + +- [ ] **Step 3: Load extension in Chrome for manual testing** + +Instructions: + +1. Open Chrome and navigate to `chrome://extensions/` +2. Enable Developer Mode +3. Click "Load unpacked" +4. Select `.output/chrome-mv3` directory +5. Test on a regular web page (e.g., example.com) + +- [ ] **Step 4: Commit successful implementation** + +```bash +git commit --allow-empty -m "feat: complete storage cleaner feature implementation" +``` + +--- + +## Testing Checklist + +After implementation, verify: + +- [ ] Tab switching works between timestamp and storage cleaner +- [ ] Current domain displays correctly +- [ ] All storage type checkboxes toggle correctly +- [ ] Auto refresh checkbox persists across sessions +- [ ] Clear confirmation dialog appears +- [ ] localStorage clears successfully +- [ ] sessionStorage clears successfully +- [ ] IndexedDB clears successfully (or shows error if unavailable) +- [ ] Cookies clear successfully +- [ ] Cache Storage clears successfully +- [ ] Service Workers unregister successfully +- [ ] Result message displays correctly +- [ ] Empty state shows friendly message +- [ ] Auto refresh works +- [ ] Manual refresh button appears when auto-refresh is off +- [ ] Restricted pages show error message +- [ ] Snackbar notifications appear correctly + +--- + +## Rollback Plan + +If issues occur during testing: + +1. Revert to before implementation: + + ```bash + git reset --hard + ``` + +2. Or revert specific files: + ```bash + git checkout HEAD -- types/storage.d.ts wxt.config.ts utils/storageCleaner.ts entrypoints/popup/App.tsx entrypoints/popup/pages/StorageCleanerPage.tsx + ``` + +--- + +## Notes + +- The popup closes automatically when the page is refreshed - this is expected behavior +- IndexedDB.databases() may not be available in all browser versions; the fallback handles this +- Chrome Cookies API requires explicit permission, which is added to the manifest +- User preferences are persisted using the existing chromeStorage.ts utility