diff --git a/src/content/components/file-selector-panel.ts b/src/content/components/file-selector-panel.ts index 04547c2..9e87a5a 100644 --- a/src/content/components/file-selector-panel.ts +++ b/src/content/components/file-selector-panel.ts @@ -9,6 +9,7 @@ import { BatchResults, ProgressEvent } from '../../types/core'; import { I18nService } from '../../utils/i18n'; import { parseFileName } from '../../utils/helpers'; import { logger } from '../../utils/logger'; +import { recordUsageStatsDelta } from '../../utils/usage-stats'; import './config-panel'; import './file-list-panel'; import './preview-panel'; @@ -490,6 +491,11 @@ export class FileSelectorPanel extends LitElement { this.executionResults = results; this.executorState = executor.getState(); + await recordUsageStatsDelta(this.adapter.platform, { + success: results.success.length, + failed: results.failed.length, + }); + this.applyExecutionResults(results); void this.syncAfterRename(); } catch (error) { @@ -697,6 +703,11 @@ export class FileSelectorPanel extends LitElement { this.executionResults = results; this.executorState = executor.getState(); + await recordUsageStatsDelta(this.adapter.platform, { + success: results.success.length, + failed: results.failed.length, + }); + this.applyExecutionResults(results); this.updateSummaryProgress(); void this.syncAfterRename(); diff --git a/src/shared/version.ts b/src/shared/version.ts index 85fc775..666f0cf 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-29T10:57:35.368Z + * Generated at: 2025-12-29T11:20:31.860Z */ /** diff --git a/src/utils/storage.ts b/src/utils/storage.ts index 98223fa..b027166 100644 --- a/src/utils/storage.ts +++ b/src/utils/storage.ts @@ -22,7 +22,7 @@ export class StorageManager { async get(key: string): Promise { try { const result = await chrome.storage.local.get(key); - return result[key] || null; + return (result as Record)[key] ?? null; } catch (error) { console.error('[CDR] Failed to read from chrome.storage:', error); // 降级到localStorage diff --git a/src/utils/usage-stats.ts b/src/utils/usage-stats.ts new file mode 100644 index 0000000..c252d61 --- /dev/null +++ b/src/utils/usage-stats.ts @@ -0,0 +1,44 @@ +import type { PlatformName } from '../types/platform'; +import type { PlatformUsageStats } from '../types/stats'; +import { STORAGE_KEYS } from '../types/stats'; +import { logger } from './logger'; +import { storage } from './storage'; + +function getUsageStatsKey(platform: PlatformName): string { + return STORAGE_KEYS.USAGE_STATS_PREFIX + platform; +} + +function normalizeDelta(value: number): number { + if (!Number.isFinite(value)) return 0; + return Math.max(0, Math.trunc(value)); +} + +export async function recordUsageStatsDelta( + platform: PlatformName, + delta: { success: number; failed: number } +): Promise { + const successDelta = normalizeDelta(delta.success); + const failedDelta = normalizeDelta(delta.failed); + + if (successDelta === 0 && failedDelta === 0) { + return; + } + + const storageKey = getUsageStatsKey(platform); + + try { + const current = await storage.get(storageKey); + + const next: PlatformUsageStats = { + platform, + successCount: (current?.successCount ?? 0) + successDelta, + failedCount: (current?.failedCount ?? 0) + failedDelta, + lastUpdated: Date.now(), + }; + + await storage.set(storageKey, next); + } catch (error) { + logger.error(`[UsageStats] Failed to update stats for ${platform}`, error as Error); + } +} + diff --git a/tests/unit/storage.test.ts b/tests/unit/storage.test.ts index eb7ab0a..65d1d05 100644 --- a/tests/unit/storage.test.ts +++ b/tests/unit/storage.test.ts @@ -111,6 +111,20 @@ describe('StorageManager', () => { expect(result).toEqual(value); }); + it('应该正确读取 falsy 值 (false/0/空字符串)', async () => { + const cases = [ + { key: 'bool-false', value: false }, + { key: 'number-zero', value: 0 }, + { key: 'empty-string', value: '' }, + ] as const; + + for (const { key, value } of cases) { + mockChromeStorage.get.mockResolvedValueOnce({ [key]: value }); + const result = await storageManager.get(key); + expect(result).toBe(value); + } + }); + it('应该在数据不存在时返回null', async () => { const key = 'non-existent-key'; diff --git a/tests/unit/usage-stats.test.ts b/tests/unit/usage-stats.test.ts new file mode 100644 index 0000000..a2f3572 --- /dev/null +++ b/tests/unit/usage-stats.test.ts @@ -0,0 +1,83 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { recordUsageStatsDelta } from '../../src/utils/usage-stats'; +import { STORAGE_KEYS } from '../../src/types/stats'; + +describe('Usage stats', () => { + const platform = 'quark' as const; + const storageKey = STORAGE_KEYS.USAGE_STATS_PREFIX + platform; + + let mockChromeStorage: any; + + beforeEach(() => { + mockChromeStorage = { + get: vi.fn(() => Promise.resolve({})), + set: vi.fn(() => Promise.resolve()), + }; + + global.chrome = { + storage: { + local: mockChromeStorage, + }, + } as any; + + global.localStorage = { + getItem: vi.fn(), + setItem: vi.fn(), + removeItem: vi.fn(), + clear: vi.fn(), + key: vi.fn(), + length: 0, + } as any; + + vi.spyOn(Date, 'now').mockReturnValue(1700000000000); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should create stats for a platform when missing', async () => { + mockChromeStorage.get.mockResolvedValueOnce({}); + + await recordUsageStatsDelta(platform, { success: 2, failed: 1 }); + + expect(mockChromeStorage.get).toHaveBeenCalledWith(storageKey); + expect(mockChromeStorage.set).toHaveBeenCalledWith({ + [storageKey]: { + platform, + successCount: 2, + failedCount: 1, + lastUpdated: 1700000000000, + }, + }); + }); + + it('should accumulate stats when existing value present', async () => { + mockChromeStorage.get.mockResolvedValueOnce({ + [storageKey]: { + platform, + successCount: 5, + failedCount: 2, + lastUpdated: 1, + }, + }); + + await recordUsageStatsDelta(platform, { success: 3, failed: 4 }); + + expect(mockChromeStorage.set).toHaveBeenCalledWith({ + [storageKey]: { + platform, + successCount: 8, + failedCount: 6, + lastUpdated: 1700000000000, + }, + }); + }); + + it('should no-op when delta is zero', async () => { + await recordUsageStatsDelta(platform, { success: 0, failed: 0 }); + expect(mockChromeStorage.get).not.toHaveBeenCalled(); + expect(mockChromeStorage.set).not.toHaveBeenCalled(); + }); +}); +