diff --git a/.gitignore b/.gitignore index 994530a..befe77c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ **/node_modules/** /.idea/ -trae-usage-monitor-*.vsix +*.vsix out/ trae-usage-server/ .vscode-test/ diff --git a/.vscode/settings.json b/.vscode/settings.json index 57c3a17..23805f5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,52 @@ { - "idf.pythonInstallPath": "D:\\tools\\Espressif\\tools\\idf-python\\3.11.2\\python.exe" -} \ No newline at end of file + "idf.pythonInstallPath": "D:\\tools\\Espressif\\tools\\idf-python\\3.11.2\\python.exe", + "projectColors.mainColor": "#f40b93", + "window.title": "TraeUsage", + "workbench.colorCustomizations": { + "statusBarItem.warningBackground": "#f40b93", + "statusBarItem.warningForeground": "#ffffff", + "statusBarItem.warningHoverBackground": "#f40b93", + "statusBarItem.warningHoverForeground": "#ffffff90", + "statusBarItem.remoteBackground": "#ff18a0", + "statusBarItem.remoteForeground": "#ffffff", + "statusBarItem.remoteHoverBackground": "#ff25ad", + "statusBarItem.remoteHoverForeground": "#ffffff90", + "statusBar.background": "#f40b93", + "statusBar.foreground": "#ffffff", + "statusBar.border": "#f40b93", + "statusBar.debuggingBackground": "#f40b93", + "statusBar.debuggingForeground": "#ffffff", + "statusBar.debuggingBorder": "#f40b93", + "statusBar.noFolderBackground": "#f40b93", + "statusBar.noFolderForeground": "#ffffff", + "statusBar.noFolderBorder": "#f40b93", + "statusBar.prominentBackground": "#f40b93", + "statusBar.prominentForeground": "#ffffff", + "statusBar.prominentHoverBackground": "#f40b93", + "statusBar.prominentHoverForeground": "#ffffff90", + "focusBorder": "#f40b9399", + "progressBar.background": "#f40b93", + "textLink.foreground": "#ff4bd3", + "textLink.activeForeground": "#ff58e0", + "selection.background": "#e70086", + "list.highlightForeground": "#f40b93", + "list.focusAndSelectionOutline": "#f40b9399", + "button.background": "#f40b93", + "button.foreground": "#ffffff", + "button.hoverBackground": "#ff18a0", + "tab.activeBorderTop": "#ff18a0", + "pickerGroup.foreground": "#ff18a0", + "list.activeSelectionBackground": "#f40b934d", + "panelTitle.activeBorder": "#ff18a0", + "activityBar.activeBorder": "#f40b93", + "activityBarBadge.foreground": "#ffffff", + "activityBarBadge.background": "#f40b93" + }, + "projectColors.name": "TraeUsage", + "projectColors.isActivityBarColored": false, + "projectColors.isTitleBarColored": false, + "projectColors.isStatusBarColored": true, + "projectColors.isProjectNameColored": true, + "projectColors.isActiveItemsColored": true, + "projectColors.setWindowTitle": true +} diff --git a/README.md b/README.md index 9af32b2..d2d4fc0 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Trae Usage Monitor -[English](README.en.md) +由于原项目出现本人无法使用是问题,并发现有其他用户也遇到了相同问题,因此本人 fork 了原项目,本意是为了帮助优先,但是反馈问题和提交 pr,等待好久无人回应,所以单开一个版本。 -一个VSCode扩展,用于实时监控Trae AI的使用量统计。 +一个 VSCode 扩展,用于实时监控 Trae AI 的使用量统计。 ### 状态栏 @@ -23,42 +23,45 @@ 功能截图 -### 2. 获取Session ID +### 2. 获取 Session ID **使用浏览器扩展** -**Chrome浏览器:** -1. 安装Chrome扩展:[Trae Usage Token Extractor](https://chromewebstore.google.com/detail/edkpaodbjadikhahggapfilgmfijjhei?utm_source=item-share-cb) +**Chrome 浏览器:** -**Edge浏览器:** -1. 安装Edge扩展:[Trae Usage Token Extractor](https://microsoftedge.microsoft.com/addons/detail/trae-usage-token-extracto/leopdblngeedggognlgokdlfpiojalji) +1. 安装 Chrome 扩展:[Trae Usage Token Extractor](https://chromewebstore.google.com/detail/edkpaodbjadikhahggapfilgmfijjhei?utm_source=item-share-cb) + +**Edge 浏览器:** + +1. 安装 Edge 扩展:[Trae Usage Token Extractor](https://microsoftedge.microsoft.com/addons/detail/trae-usage-token-extracto/leopdblngeedggognlgokdlfpiojalji) **使用步骤:** -1. 安装后通过通知或TraeUsage窗口设置跳转安装Chrome扩展 -2. 安装后在浏览器点击Chrome扩展图标,点击跳转按钮到Usage页面 -3. 登录并浏览usage页面,自动获取Session ID -4. 点击Chrome扩展图标,自动复制Session ID至粘贴板 -5. 返回Trae,Trae Usage扩展会自动识别粘贴板并配置Session ID -6. Ctrl+Shift+P 打开命令面板,输入TraeUsage: Collect Usage Details + +1. 安装后通过通知或 TraeUsage 窗口设置跳转安装 Chrome 扩展 +2. 安装后在浏览器点击 Chrome 扩展图标,点击跳转按钮到 Usage 页面 +3. 登录并浏览 usage 页面,自动获取 Session ID +4. 点击 Chrome 扩展图标,自动复制 Session ID 至粘贴板 +5. 返回 Trae,Trae Usage 扩展会自动识别粘贴板并配置 Session ID +6. Ctrl+Shift+P 打开命令面板,输入 TraeUsage: Collect Usage Details ### 3. 查看使用量 -配置完成后,在VSCode左侧的资源管理器面板中会出现 "Trae Usage" 视图,显示: +配置完成后,在 VSCode 左侧的资源管理器面板中会出现 "Trae Usage" 视图,显示: - ⚡ Premium Fast Request:快速请求的使用量和剩余配额 -- 🐌 Premium Slow Request:慢速请求的使用量和剩余配额 +- 🐌 Premium Slow Request:慢速请求的使用量和剩余配额 - 🔧 Auto Completion:自动补全的使用量和剩余配额 - 🚀 Advanced Model:高级模型的使用量和剩余配额 - ## 反馈与支持 -如果您在使用过程中遇到问题或有功能建议,欢迎访问我们的GitHub项目页面: +如果您在使用过程中遇到问题或有功能建议,欢迎访问我们的 GitHub 项目页面: -🔗 **项目地址**:[https://github.com/whyuds/TraeUsage](https://github.com/whyuds/TraeUsage) +🔗 **原项目地址**:[https://github.com/whyuds/TraeUsage](https://github.com/whyuds/TraeUsage) +🔗 **当前项目地址**:[https://github.com/mtpupil/TraeUsage](https://github.com/mtpupil/TraeUsage) -💬 **问题反馈**:如有问题请在GitHub上提交[Issues](https://github.com/whyuds/TraeUsage/issues) +💬 **问题反馈**:如有问题请在 GitHub 上提交[Issues](https://github.com/mtpupil/TraeUsage/issues) ## 许可证 -MIT License \ No newline at end of file +MIT License diff --git a/package-lock.json b/package-lock.json index 2606887..f3bc43e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "trae-usage-monitor", - "version": "1.3.1-SNAPSHOT", + "version": "1.3.4-SNAPSHOT", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "trae-usage-monitor", - "version": "1.3.1-SNAPSHOT", + "version": "1.3.4-SNAPSHOT", "license": "MIT", "dependencies": { "axios": "^1.6.0" diff --git a/package.json b/package.json index 5653864..cbd5285 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { - "name": "trae-usage-monitor", - "displayName": "Trae Usage", + "name": "trae-usage-reborn", + "displayName": "Trae Usage Reborn", "description": "Monitor Trae AI usage statistics in real-time", - "version": "1.3.3", - "publisher": "whyuds", + "version": "1.3.4", + "publisher": "MTpupil", "repository": { "type": "git", - "url": "https://github.com/whyuds/TraeUsage.git" + "url": "https://github.com/mtpupil/TraeUsage.git" }, "license": "MIT", "engines": { @@ -15,6 +15,51 @@ "categories": [ "Other" ], + "keywords": [ + "Trae", + "trae", + "Trae AI", + "AI", + "ai", + "人工智能", + "Usage", + "usage", + "使用量", + "用量", + "用量统计", + "配额", + "额度", + "Monitor", + "monitor", + "监控", + "统计", + "仪表板", + "看板", + "Token", + "token", + "令牌", + "JWT", + "Session", + "session", + "Session ID", + "会话", + "Cloud-IDE", + "Cloud IDE", + "云IDE", + "VSCode Extension", + "extension", + "扩展", + "插件", + "Usage Details", + "使用详情", + "Entitlement", + "权益", + "订阅", + "套餐", + "木瞳", + "mtpupil", + "MTpupil" + ], "icon": "img/logo_256.png", "activationEvents": [ "onStartupFinished" @@ -48,7 +93,6 @@ "icon": "$(output)" } ], - "configuration": { "title": "%configuration.title%", "properties": { @@ -107,4 +151,4 @@ "dependencies": { "axios": "^1.6.0" } -} \ No newline at end of file +} diff --git a/src/apiService.ts b/src/apiService.ts index c2dda72..51727c0 100644 --- a/src/apiService.ts +++ b/src/apiService.ts @@ -1,17 +1,17 @@ -import * as vscode from 'vscode'; -import axios from 'axios'; -import { logWithTime } from './utils'; -import { TokenResponse } from './extension'; -import { t } from './i18n'; -import { UsageDetailResponse } from './types'; +import * as vscode from "vscode"; +import axios from "axios"; +import { logWithTime } from "./utils"; +import { TokenResponse } from "./extension"; +import { t } from "./i18n"; +import { UsageDetailResponse } from "./types"; // 常量定义 -const DEFAULT_HOST = 'https://api-sg-central.trae.ai'; -const FALLBACK_HOST = 'https://api-us-east.trae.ai'; +const DEFAULT_HOST = "https://api-us-east.trae.ai"; +const FALLBACK_HOST = "https://api-sg-central.trae.ai"; const API_TIMEOUT = 3000; const MAX_RETRY_COUNT = 5; const RETRY_DELAY = 1000; -const TOKEN_ERROR_CODE = '20310'; +const TOKEN_ERROR_CODE = "20310"; /** * 统一的API服务类,管理GetUserToken接口调用 @@ -39,34 +39,44 @@ export class ApiService { * @param isManualRefresh 是否为手动刷新 * @returns Promise */ - public async getTokenFromSession(sessionId: string, retryCount = 0, isManualRefresh = false): Promise { + public async getTokenFromSession( + sessionId: string, + retryCount = 0, + isManualRefresh = false + ): Promise { // 检查缓存 if (this.cachedToken && this.cachedSessionId === sessionId) { return this.cachedToken; } const currentHost = this.getHost(); - + try { const response = await axios.post( `${currentHost}/cloudide/api/v3/common/GetUserToken`, {}, { headers: { - 'Cookie': `X-Cloudide-Session=${sessionId}`, - 'Host': new URL(currentHost).hostname, - 'Content-Type': 'application/json' + Cookie: `X-Cloudide-Session=${sessionId}`, + Host: new URL(currentHost).hostname, + "Content-Type": "application/json", }, - timeout: API_TIMEOUT + timeout: API_TIMEOUT, } ); - logWithTime('更新Token'); + logWithTime("更新Token"); this.cachedToken = response.data.Result.Token; this.cachedSessionId = sessionId; return this.cachedToken; } catch (error) { - return this.handleTokenError(error, sessionId, retryCount, currentHost, isManualRefresh); + return this.handleTokenError( + error, + sessionId, + retryCount, + currentHost, + isManualRefresh + ); } } @@ -76,24 +86,31 @@ export class ApiService { * @param maxRetries 最大重试次数 * @returns Promise */ - public async getTokenWithRetry(sessionId: string, maxRetries: number = MAX_RETRY_COUNT): Promise { - return this.apiRequestWithRetry(async () => { - const currentHost = this.getHost(); - const response = await axios.post( - `${currentHost}/cloudide/api/v3/common/GetUserToken`, - {}, - { - headers: { - 'Cookie': `X-Cloudide-Session=${sessionId}`, - 'Host': new URL(currentHost).hostname, - 'Content-Type': 'application/json' - }, - timeout: API_TIMEOUT - } - ); + public async getTokenWithRetry( + sessionId: string, + maxRetries: number = MAX_RETRY_COUNT + ): Promise { + return this.apiRequestWithRetry( + async () => { + const currentHost = this.getHost(); + const response = await axios.post( + `${currentHost}/cloudide/api/v3/common/GetUserToken`, + {}, + { + headers: { + Cookie: `X-Cloudide-Session=${sessionId}`, + Host: new URL(currentHost).hostname, + "Content-Type": "application/json", + }, + timeout: API_TIMEOUT, + } + ); - return response.data.Result.Token; - }, '获取认证Token', maxRetries); + return response.data.Result.Token; + }, + "获取认证Token", + maxRetries + ); } /** @@ -110,74 +127,85 @@ export class ApiService { * 处理Token获取错误 */ private async handleTokenError( - error: any, - sessionId: string, - retryCount: number, + error: any, + sessionId: string, + retryCount: number, currentHost: string, isManualRefresh: boolean = false ): Promise { - logWithTime(`获取Token失败 (尝试 ${retryCount + 1}/${MAX_RETRY_COUNT}): ${error.code}, ${error.message}`); - + logWithTime( + `获取Token失败 (尝试 ${retryCount + 1}/${MAX_RETRY_COUNT}): ${ + error.code + }, ${error.message}` + ); + // 处理401认证失败情况 if (error.response?.status === 401) { - logWithTime('检测到401认证失败,可能是sessionId无效或已过期'); + logWithTime("检测到401认证失败,可能是sessionId无效或已过期"); if (isManualRefresh) { - vscode.window.showErrorMessage( - '认证失败:Session ID可能无效或已过期,请更新Session ID', - '更新Session ID' - ).then(selection => { - if (selection === '更新Session ID') { - vscode.commands.executeCommand('traeUsage.updateSession'); - } - }); + vscode.window + .showErrorMessage( + "认证失败:Session ID可能无效或已过期,请更新Session ID", + "更新Session ID" + ) + .then((selection) => { + if (selection === "更新Session ID") { + vscode.commands.executeCommand("traeUsage.updateSession"); + } + }); } else { // 自动刷新时显示错误提示,但不阻塞流程 - vscode.window.showErrorMessage('Trae Usage: 认证失败,请手动更新Session ID'); + vscode.window.showErrorMessage( + "Trae Usage: 认证失败,请手动更新Session ID" + ); } return null; } - + // 核心修改:处理Token错误(支持双向切换主机) if (this.isTokenError(error)) { if (!this.hasSwitchedHost) { // 未切换过主机:切换到另一个主机(默认 ↔ 备用互切) - const otherHost = currentHost === DEFAULT_HOST ? FALLBACK_HOST : DEFAULT_HOST; - logWithTime(`检测到错误代码${TOKEN_ERROR_CODE},尝试切换到备用主机: ${otherHost}`); + const otherHost = + currentHost === DEFAULT_HOST ? FALLBACK_HOST : DEFAULT_HOST; + logWithTime( + `检测到错误代码${TOKEN_ERROR_CODE},尝试切换到备用主机: ${otherHost}` + ); await this.setHost(otherHost); // 切换到另一个主机 this.hasSwitchedHost = true; // 标记为已切换 return this.getTokenFromSession(sessionId, 0); // 重置重试次数,重试获取Token } else { // 已切换过主机仍失败:通知用户无法获取Token if (isManualRefresh) { - vscode.window.showErrorMessage(t('messages.cannotGetToken')); + vscode.window.showErrorMessage(t("messages.cannotGetToken")); } else { - vscode.window.showErrorMessage('Trae Usage: 无法获取认证Token,请检查网络连接或手动更新Session ID'); + vscode.window.showErrorMessage( + "Trae Usage: 无法获取认证Token,请检查网络连接或手动更新Session ID" + ); } return null; } } - + // 原有可重试错误逻辑(不变) if (this.isRetryableError(error) && retryCount < MAX_RETRY_COUNT) { logWithTime(`Token获取失败,将在1秒后进行第${retryCount + 1}次重试`); - await new Promise(resolve => setTimeout(resolve, RETRY_DELAY)); + await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY)); return this.getTokenFromSession(sessionId, retryCount + 1); } - + // 重试后网络仍有问题(不变) if (this.isRetryableError(error) && retryCount >= MAX_RETRY_COUNT) { if (isManualRefresh) { - vscode.window.showErrorMessage(t('messages.networkUnstable')); + vscode.window.showErrorMessage(t("messages.networkUnstable")); } else { - vscode.window.showErrorMessage('Trae Usage: 网络不稳定,请稍后重试'); + vscode.window.showErrorMessage("Trae Usage: 网络不稳定,请稍后重试"); } } - + return null; } - - /** * 带重试机制的通用API请求函数 */ @@ -187,7 +215,7 @@ export class ApiService { maxRetries: number = MAX_RETRY_COUNT ): Promise { let lastError: any; - + for (let attempt = 1; attempt <= maxRetries; attempt++) { try { const result = await requestFn(); @@ -197,37 +225,46 @@ export class ApiService { return result; } catch (error) { lastError = error; - logWithTime(`${operationName} 第${attempt}次尝试失败: ${String(error)}`); - + logWithTime( + `${operationName} 第${attempt}次尝试失败: ${String(error)}` + ); + if (attempt < maxRetries) { const delay = RETRY_DELAY * attempt; // 递增延迟 logWithTime(`等待${delay}ms后进行第${attempt + 1}次重试`); - await new Promise(resolve => setTimeout(resolve, delay)); + await new Promise((resolve) => setTimeout(resolve, delay)); } } } - - throw new Error(`${operationName} 在${maxRetries}次重试后仍然失败: ${String(lastError)}`); + + throw new Error( + `${operationName} 在${maxRetries}次重试后仍然失败: ${String(lastError)}` + ); } /** * 检查是否是Token错误 */ private isTokenError(error: any): boolean { - return error?.response?.data?.ResponseMetadata?.Error?.Code === TOKEN_ERROR_CODE; + return ( + error?.response?.data?.ResponseMetadata?.Error?.Code === TOKEN_ERROR_CODE + ); } /** * 检查是否是可重试的错误 */ public isRetryableError(error: any): boolean { - return error && ( - error.code === 'ECONNABORTED' || - error.message?.includes('timeout') || - error.code === 'ENOTFOUND' || - error.code === 'ECONNRESET' || - error.message?.includes('Failed to establish a socket connection to proxies') || - error.message?.includes('proxy') + return ( + error && + (error.code === "ECONNABORTED" || + error.message?.includes("timeout") || + error.code === "ENOTFOUND" || + error.code === "ECONNRESET" || + error.message?.includes( + "Failed to establish a socket connection to proxies" + ) || + error.message?.includes("proxy")) ); } @@ -237,15 +274,19 @@ export class ApiService { * @param operationName 操作名称 * @param showUserMessage 是否显示用户消息 */ - public handleApiError(error: any, operationName: string, showUserMessage: boolean = false): void { + public handleApiError( + error: any, + operationName: string, + showUserMessage: boolean = false + ): void { const errorMessage = `${operationName}失败: ${String(error)}`; logWithTime(errorMessage); - + if (showUserMessage) { if (this.isRetryableError(error)) { - vscode.window.showErrorMessage(t('messages.networkUnstable')); + vscode.window.showErrorMessage(t("messages.networkUnstable")); } else if (this.isTokenError(error)) { - vscode.window.showErrorMessage(t('messages.cannotGetToken')); + vscode.window.showErrorMessage(t("messages.cannotGetToken")); } else { vscode.window.showErrorMessage(`${operationName}失败,请稍后重试`); } @@ -270,10 +311,16 @@ export class ApiService { if (response?.code === 1001) { logWithTime(`${operationName}: Token已失效(code: 1001)`); this.clearCache(); - vscode.window.showErrorMessage(t('messages.tokenExpired')); + vscode.window.showErrorMessage(t("messages.tokenExpired")); } else if (response?.code) { - logWithTime(`${operationName}: API返回错误码 ${response.code}, 消息: ${response.message || 'Unknown error'}`); - vscode.window.showErrorMessage(`${operationName}失败: ${response.message || 'Unknown error'}`); + logWithTime( + `${operationName}: API返回错误码 ${response.code}, 消息: ${ + response.message || "Unknown error" + }` + ); + vscode.window.showErrorMessage( + `${operationName}失败: ${response.message || "Unknown error"}` + ); } } @@ -314,16 +361,16 @@ export class ApiService { {}, { headers: { - 'authorization': `Cloud-IDE-JWT ${authToken}`, - 'Host': new URL(currentHost).hostname, - 'Content-Type': 'application/json' + authorization: `Cloud-IDE-JWT ${authToken}`, + Host: new URL(currentHost).hostname, + "Content-Type": "application/json", }, - timeout: API_TIMEOUT + timeout: API_TIMEOUT, } ); return response.data; - }, '获取用户权益列表'); + }, "获取用户权益列表"); return result; } catch (error) { @@ -337,19 +384,21 @@ export class ApiService { * @param authToken 认证Token * @returns Promise<{ start_time: number; end_time: number } | null> */ - public async getSubscriptionTimeRange(authToken: string): Promise<{ start_time: number; end_time: number } | null> { + public async getSubscriptionTimeRange( + authToken: string + ): Promise<{ start_time: number; end_time: number } | null> { try { const result = await this.getUserEntitlementList(authToken); - + if (result?.user_entitlement_pack_list?.length > 0) { const pack = result.user_entitlement_pack_list[0]; return { start_time: pack.entitlement_base_info.start_time, - end_time: pack.entitlement_base_info.end_time + end_time: pack.entitlement_base_info.end_time, }; } - - throw new Error('No entitlement pack found'); + + throw new Error("No entitlement pack found"); } catch (error) { logWithTime(`获取订阅时间范围失败: ${error}`); return null; @@ -380,12 +429,12 @@ export class ApiService { start_time, end_time, page_size: pageSize, - page_num: pageNum + page_num: pageNum, }; const headers = { - 'authorization': `Cloud-IDE-JWT ${authToken}`, - 'Host': new URL(currentHost).hostname, - 'Content-Type': 'application/json' + authorization: `Cloud-IDE-JWT ${authToken}`, + Host: new URL(currentHost).hostname, + "Content-Type": "application/json", }; logWithTime(`请求第${pageNum}页使用详情数据: ${url}`); @@ -395,7 +444,7 @@ export class ApiService { requestBody, { headers, - timeout: 10000 + timeout: 10000, } ); @@ -415,4 +464,4 @@ export class ApiService { */ export function getApiService(): ApiService { return ApiService.getInstance(); -} \ No newline at end of file +} diff --git a/src/dashboardGenerator.ts b/src/dashboardGenerator.ts index e62566b..7f4877c 100644 --- a/src/dashboardGenerator.ts +++ b/src/dashboardGenerator.ts @@ -1,9 +1,16 @@ -import * as vscode from 'vscode'; -import * as os from 'os'; -import { StoredUsageData, UsageDetailItem, UsageSummary, ModelStats, ModeStats, DailyStats } from './types'; -import { logWithTime, formatTimestamp } from './utils'; - -const USAGE_DATA_FILE = 'usage_data.json'; +import * as vscode from "vscode"; +import * as os from "os"; +import { + StoredUsageData, + UsageDetailItem, + UsageSummary, + ModelStats, + ModeStats, + DailyStats, +} from "./types"; +import { logWithTime, formatTimestamp } from "./utils"; + +const USAGE_DATA_FILE = "usage_data.json"; export class UsageDashboardGenerator { private context: vscode.ExtensionContext; @@ -18,42 +25,53 @@ export class UsageDashboardGenerator { const rawData = await this.loadUsageData(); if (!rawData || Object.keys(rawData.usage_details).length === 0) { const choice = await vscode.window.showWarningMessage( - 'No usage data found, please collect data first', - 'Collect Now' + "未找到使用数据,请先收集数据", + "立即收集" ); - if (choice === 'Collect Now') { - vscode.commands.executeCommand('traeUsage.collectUsageDetails'); + if (choice === "立即收集") { + vscode.commands.executeCommand("traeUsage.collectUsageDetails"); } return; } await this.generateAndShowDashboard(rawData); } catch (error) { - logWithTime(`Failed to display dashboard: ${error}`); - vscode.window.showErrorMessage(`Dashboard error: ${error?.toString() || 'Unknown error'}`); + logWithTime(`显示看板失败: ${error}`); + vscode.window.showErrorMessage( + `看板错误: ${error?.toString() || "Unknown error"}` + ); } } private async loadUsageData(): Promise { - const dataPath = vscode.Uri.joinPath(this.context.globalStorageUri, USAGE_DATA_FILE); - + const dataPath = vscode.Uri.joinPath( + this.context.globalStorageUri, + USAGE_DATA_FILE + ); + try { const fileContent = await vscode.workspace.fs.readFile(dataPath); const jsonData = JSON.parse(fileContent.toString()); return jsonData as StoredUsageData; } catch (error) { - logWithTime(`Failed to read usage data file: ${error}`); + logWithTime(`读取使用数据文件失败: ${error}`); return null; } } - private filterUsageDetails(usageDetails: UsageDetailItem[], startDate?: string, endDate?: string): UsageDetailItem[] { + private filterUsageDetails( + usageDetails: UsageDetailItem[], + startDate?: string, + endDate?: string + ): UsageDetailItem[] { if (!startDate && !endDate) { return usageDetails; } - return usageDetails.filter(item => { - const itemDate = new Date(item.usage_time * 1000).toISOString().split('T')[0]; + return usageDetails.filter((item) => { + const itemDate = new Date(item.usage_time * 1000) + .toISOString() + .split("T")[0]; if (startDate && itemDate < startDate) return false; if (endDate && itemDate > endDate) return false; return true; @@ -67,10 +85,10 @@ export class UsageDashboardGenerator { total_sessions: usageDetails.length, model_stats: {}, mode_stats: {}, - daily_stats: {} + daily_stats: {}, }; - usageDetails.forEach(item => { + usageDetails.forEach((item) => { summary.total_amount += item.amount_float; summary.total_cost += item.cost_money_float; @@ -84,7 +102,7 @@ export class UsageDashboardGenerator { input_tokens: 0, output_tokens: 0, cache_read_tokens: 0, - cache_write_tokens: 0 + cache_write_tokens: 0, }; } const modelStats = summary.model_stats[modelName]; @@ -97,7 +115,7 @@ export class UsageDashboardGenerator { modelStats.cache_write_tokens += item.extra_info.cache_write_token; // Mode statistics - const mode = item.use_max_mode ? 'Max' : 'Normal'; + const mode = item.use_max_mode ? "Max" : "Normal"; if (!summary.mode_stats[mode]) { summary.mode_stats[mode] = { count: 0, amount: 0, cost: 0 }; } @@ -106,9 +124,14 @@ export class UsageDashboardGenerator { summary.mode_stats[mode].cost += item.cost_money_float; // Daily statistics - const date = new Date(item.usage_time * 1000).toISOString().split('T')[0]; + const date = new Date(item.usage_time * 1000).toISOString().split("T")[0]; if (!summary.daily_stats[date]) { - summary.daily_stats[date] = { count: 0, amount: 0, cost: 0, models: [] }; + summary.daily_stats[date] = { + count: 0, + amount: 0, + cost: 0, + models: [], + }; } summary.daily_stats[date].count++; summary.daily_stats[date].amount += item.amount_float; @@ -121,106 +144,151 @@ export class UsageDashboardGenerator { return summary; } - private async generateAndShowDashboard(rawData: StoredUsageData): Promise { + private async generateAndShowDashboard( + rawData: StoredUsageData + ): Promise { const allUsageDetails = Object.values(rawData.usage_details); const initialSummary = this.generateSummary(allUsageDetails); - + if (this.panel) { this.panel.dispose(); } - + this.panel = vscode.window.createWebviewPanel( - 'traeUsageDashboard', - 'Trae Usage Statistics', + "traeUsageDashboard", + "Trae 使用统计看板", vscode.ViewColumn.One, { enableScripts: true, - retainContextWhenHidden: true + retainContextWhenHidden: true, } ); // Listen for messages from webview this.panel.webview.onDidReceiveMessage(async (message) => { switch (message.command) { - case 'filter': - const filteredDetails = this.filterUsageDetails(allUsageDetails, message.startDate, message.endDate); + case "filter": + const filteredDetails = this.filterUsageDetails( + allUsageDetails, + message.startDate, + message.endDate + ); const filteredSummary = this.generateSummary(filteredDetails); - + this.panel?.webview.postMessage({ - command: 'updateData', + command: "updateData", summary: filteredSummary, - details: filteredDetails + details: filteredDetails, }); break; - - case 'export': - const exportDetails = this.filterUsageDetails(allUsageDetails, message.startDate, message.endDate); - await this.exportData(exportDetails, message.startDate, message.endDate); + + case "export": + const exportDetails = this.filterUsageDetails( + allUsageDetails, + message.startDate, + message.endDate + ); + await this.exportData( + exportDetails, + message.startDate, + message.endDate + ); break; } }); - this.panel.webview.html = this.generateDashboardHTML(rawData, initialSummary, allUsageDetails); + this.panel.webview.html = this.generateDashboardHTML( + rawData, + initialSummary, + allUsageDetails + ); } - private async exportData(filteredDetails: UsageDetailItem[], startDate?: string, endDate?: string): Promise { + private async exportData( + filteredDetails: UsageDetailItem[], + startDate?: string, + endDate?: string + ): Promise { try { const csvContent = this.generateCSV(filteredDetails); - const dateRange = startDate && endDate ? `_${startDate}_to_${endDate}` : ''; - const fileName = `trae_usage_export${dateRange}_${new Date().toISOString().split('T')[0]}.csv`; - + const dateRange = + startDate && endDate ? `_${startDate}_to_${endDate}` : ""; + const fileName = `trae_usage_export${dateRange}_${ + new Date().toISOString().split("T")[0] + }.csv`; + const uri = await vscode.window.showSaveDialog({ defaultUri: vscode.Uri.file(fileName), filters: { - 'CSV Files': ['csv'] - } + "CSV 文件": ["csv"], + }, }); if (uri) { - await vscode.workspace.fs.writeFile(uri, Buffer.from(csvContent, 'utf8')); - vscode.window.showInformationMessage(`Data exported to: ${uri.fsPath}`); + await vscode.workspace.fs.writeFile( + uri, + Buffer.from(csvContent, "utf8") + ); + vscode.window.showInformationMessage(`数据已导出: ${uri.fsPath}`); } } catch (error) { - vscode.window.showErrorMessage(`Export failed: ${error}`); + vscode.window.showErrorMessage(`导出失败: ${error}`); } } private generateCSV(details: UsageDetailItem[]): string { const headers = [ - 'Time', 'Model', 'Mode', 'Usage', 'Cost', 'Input Tokens', 'Output Tokens', 'Cache Read Tokens', 'Cache Write Tokens', 'Session ID' + "Time", + "Model", + "Mode", + "Usage", + "Cost", + "Input Tokens", + "Output Tokens", + "Cache Read Tokens", + "Cache Write Tokens", + "Session ID", ]; - - const rows = details.map(item => [ - new Date((item.usage_time || 0) * 1000).toLocaleString('en-US'), - item.model_name || '', - item.use_max_mode ? 'Max' : 'Normal', + + const rows = details.map((item) => [ + new Date((item.usage_time || 0) * 1000).toLocaleString("en-US"), + item.model_name || "", + item.use_max_mode ? "Max" : "Normal", (item.amount_float || 0).toString(), (item.cost_money_float || 0).toString(), (item.extra_info?.input_token || 0).toString(), (item.extra_info?.output_token || 0).toString(), (item.extra_info?.cache_read_token || 0).toString(), (item.extra_info?.cache_write_token || 0).toString(), - item.session_id || '' + item.session_id || "", ]); - return [headers, ...rows].map(row => row.join(',')).join('\n'); + return [headers, ...rows].map((row) => row.join(",")).join("\n"); } - private generateDashboardHTML(rawData: StoredUsageData, summary: UsageSummary, allUsageDetails: UsageDetailItem[]): string { - const timeRange = `${formatTimestamp(rawData.start_time)} - ${formatTimestamp(rawData.end_time)}`; - + private generateDashboardHTML( + rawData: StoredUsageData, + summary: UsageSummary, + allUsageDetails: UsageDetailItem[] + ): string { + const timeRange = `${formatTimestamp( + rawData.start_time + )} - ${formatTimestamp(rawData.end_time)}`; + // Get date range for filter - const dates = allUsageDetails.map(item => new Date(item.usage_time * 1000).toISOString().split('T')[0]); - const minDate = Math.min(...dates.map(d => new Date(d).getTime())); - const maxDate = Math.max(...dates.map(d => new Date(d).getTime())); - + const dates = allUsageDetails.map( + (item) => new Date(item.usage_time * 1000).toISOString().split("T")[0] + ); + const minDate = Math.min(...dates.map((d) => new Date(d).getTime())); + const maxDate = Math.max(...dates.map((d) => new Date(d).getTime())); + return ` - Trae Usage Statistics + Trae 使用统计看板