feat(usage-stats): 添加平台使用统计功能

添加 usage-stats 模块用于记录各平台操作成功和失败次数
在文件选择面板执行操作后更新使用统计数据
添加相关单元测试确保功能正确性
这个提交包含在:
Zain 2025-12-30 09:45:37 +08:00
父节点 9209ee2d8a
当前提交 104407fe6b
共有 6 个文件被更改,包括 154 次插入 和 2 次删除

查看文件

@ -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();

查看文件

@ -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
*/
/**

查看文件

@ -22,7 +22,7 @@ export class StorageManager {
async get<T>(key: string): Promise<T | null> {
try {
const result = await chrome.storage.local.get(key);
return result[key] || null;
return (result as Record<string, T | undefined>)[key] ?? null;
} catch (error) {
console.error('[CDR] Failed to read from chrome.storage:', error);
// 降级到localStorage

44
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<void> {
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<PlatformUsageStats>(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);
}
}

查看文件

@ -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<any>(key);
expect(result).toBe(value);
}
});
it('应该在数据不存在时返回null', async () => {
const key = 'non-existent-key';

查看文件

@ -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();
});
});