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 使用统计看板