From 9cf9b51d08d33a2d4c1e5812c3f1543ca294614e Mon Sep 17 00:00:00 2001 From: mtpupil <970632312@qq.com> Date: Wed, 3 Dec 2025 09:59:23 +0800 Subject: [PATCH 1/3] =?UTF-8?q?refactor(apiService):=20=E4=BC=98=E5=8C=96A?= =?UTF-8?q?PI=E6=9C=8D=E5=8A=A1=E9=94=99=E8=AF=AF=E5=A4=84=E7=90=86?= =?UTF-8?q?=E5=8F=8A=E4=B8=BB=E6=9C=BA=E5=88=87=E6=8D=A2=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修改默认主机和备用主机的配置顺序 --- src/apiService.ts | 247 ++++++++++++++---------- src/extension.ts | 471 +++++++++++++++++++++++++++------------------- 2 files changed, 424 insertions(+), 294 deletions(-) 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/extension.ts b/src/extension.ts index 155d128..67c6a9c 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,11 +1,16 @@ -import * as vscode from 'vscode'; -import * as os from 'os'; -import * as cp from 'child_process'; -import { initializeI18n, t } from './i18n'; -import { UsageDetailCollector } from './usageCollector'; -import { UsageDashboardGenerator } from './dashboardGenerator'; -import { disposeOutputChannel, getOutputChannel, logWithTime, formatTimestamp } from './utils'; -import { getApiService } from './apiService'; +import * as vscode from "vscode"; +import * as os from "os"; +import * as cp from "child_process"; +import { initializeI18n, t } from "./i18n"; +import { UsageDetailCollector } from "./usageCollector"; +import { UsageDashboardGenerator } from "./dashboardGenerator"; +import { + disposeOutputChannel, + getOutputChannel, + logWithTime, + formatTimestamp, +} from "./utils"; +import { getApiService } from "./apiService"; // ==================== 类型定义 ==================== interface UsageData { @@ -75,7 +80,7 @@ export interface TokenResponse { }; } -type BrowserType = 'chrome' | 'edge' | 'unknown'; +type BrowserType = "chrome" | "edge" | "unknown"; // ==================== 常量定义 ==================== // 常量定义 @@ -86,46 +91,46 @@ const RETRY_DELAY = 1000; // ==================== 浏览器检测 ==================== async function detectDefaultBrowser(): Promise { const platform = os.platform(); - + try { const command = getBrowserDetectionCommand(platform); - if (!command) return 'unknown'; - + if (!command) return "unknown"; + return new Promise((resolve) => { cp.exec(command, (error, stdout) => { if (error) { logWithTime(`检测浏览器失败: ${error.message}`); - resolve('unknown'); + resolve("unknown"); return; } - + const browserType = parseBrowserOutput(stdout.toLowerCase()); resolve(browserType); }); }); } catch (error) { logWithTime(`检测浏览器异常: ${error}`); - return 'unknown'; + return "unknown"; } } function getBrowserDetectionCommand(platform: string): string | null { switch (platform) { - case 'win32': + case "win32": return 'reg query "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\http\\UserChoice" /v ProgId'; - case 'darwin': + case "darwin": return 'defaults read com.apple.LaunchServices/com.apple.launchservices.secure LSHandlers | grep -A 2 -B 2 "LSHandlerURLScheme.*http"'; - case 'linux': - return 'xdg-settings get default-web-browser'; + case "linux": + return "xdg-settings get default-web-browser"; default: return null; } } function parseBrowserOutput(output: string): BrowserType { - if (output.includes('chrome')) return 'chrome'; - if (output.includes('edge') || output.includes('msedge')) return 'edge'; - return 'unknown'; + if (output.includes("chrome")) return "chrome"; + if (output.includes("edge") || output.includes("msedge")) return "edge"; + return "unknown"; } // ==================== 主类 ==================== @@ -139,7 +144,7 @@ export class TraeUsageProvider { private clickCount = 0; private isRefreshing = false; private isManualRefresh = false; - private isAuthFailed = false; // 新增:标识认证失败状态 + private isAuthFailed = false; // 新增:标识认证失败状态 private usageDetailCollector: UsageDetailCollector; private usageDashboardGenerator: UsageDashboardGenerator; @@ -147,7 +152,7 @@ export class TraeUsageProvider { this.statusBarItem = this.createStatusBarItem(); this.usageDetailCollector = new UsageDetailCollector(context); this.usageDashboardGenerator = new UsageDashboardGenerator(context); - + this.initialize(); } @@ -165,8 +170,11 @@ export class TraeUsageProvider { } private createStatusBarItem(): vscode.StatusBarItem { - const item = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100); - item.command = 'traeUsage.handleStatusBarClick'; + const item = vscode.window.createStatusBarItem( + vscode.StatusBarAlignment.Left, + 100 + ); + item.command = "traeUsage.handleStatusBarClick"; item.show(); return item; } @@ -180,7 +188,7 @@ export class TraeUsageProvider { } else { this.updateStatusBar(); } - + this.startAutoRefresh(); this.fetchUsageData(); } @@ -188,12 +196,12 @@ export class TraeUsageProvider { // ==================== 点击处理 ==================== handleStatusBarClick(): void { if (this.isRefreshing) return; - + this.clickCount++; - + if (this.clickTimer) { this.clearClickTimer(); - vscode.commands.executeCommand('traeUsage.updateSession'); + vscode.commands.executeCommand("traeUsage.updateSession"); } else { this.clickTimer = setTimeout(() => { if (this.clickCount === 1) { @@ -216,7 +224,7 @@ export class TraeUsageProvider { refresh(): void { this.isManualRefresh = true; this.isRefreshing = true; - this.isAuthFailed = false; // 清除认证失败状态 + this.isAuthFailed = false; // 清除认证失败状态 this.setLoadingState(); this.clearCache(); this.fetchUsageData(); @@ -225,8 +233,8 @@ export class TraeUsageProvider { } private setLoadingState(): void { - this.statusBarItem.text = t('statusBar.loading'); - this.statusBarItem.tooltip = t('statusBar.refreshing'); + this.statusBarItem.text = t("statusBar.loading"); + this.statusBarItem.tooltip = t("statusBar.refreshing"); this.statusBarItem.color = undefined; } @@ -264,33 +272,42 @@ export class TraeUsageProvider { } private showNotConfiguredStatus(): void { - this.statusBarItem.text = t('statusBar.notConfigured'); + this.statusBarItem.text = t("statusBar.notConfigured"); this.statusBarItem.color = undefined; - this.statusBarItem.tooltip = `${t('statusBar.clickToConfigureSession')}\n\n${t('statusBar.clickInstructions')}`; + this.statusBarItem.tooltip = `${t( + "statusBar.clickToConfigureSession" + )}\n\n${t("statusBar.clickInstructions")}`; } private showAuthFailedStatus(): void { - this.statusBarItem.text = '⚠️ 认证失败'; - this.statusBarItem.color = '#ff6b6b'; // 红色提示 - this.statusBarItem.tooltip = `认证失败:Session ID可能无效或已过期\n请点击状态栏重新配置Session ID\n\n${t('statusBar.clickInstructions')}`; + this.statusBarItem.text = "⚠️ 认证失败"; + this.statusBarItem.color = "#ff6b6b"; // 红色提示 + this.statusBarItem.tooltip = `认证失败:Session ID可能无效或已过期\n请点击状态栏重新配置Session ID\n\n${t( + "statusBar.clickInstructions" + )}`; } private showUsageStatus(stats: UsageStats): void { const { totalUsage, totalLimit } = stats; const remaining = totalLimit - totalUsage; - + // 只保留一位小数 const remainingFormatted = remaining.toFixed(1); - - this.statusBarItem.text = `⚡ Fast: ${totalUsage}/${totalLimit} (${t('statusBar.remaining', { remaining: remainingFormatted })})`; + + this.statusBarItem.text = `⚡ Fast: ${totalUsage}/${totalLimit} (${t( + "statusBar.remaining", + { remaining: remainingFormatted } + )})`; this.statusBarItem.color = undefined; this.statusBarItem.tooltip = this.buildDetailedTooltip(); } private showNoActiveSubscriptionStatus(): void { - this.statusBarItem.text = t('statusBar.noActiveSubscription'); + this.statusBarItem.text = t("statusBar.noActiveSubscription"); this.statusBarItem.color = undefined; - this.statusBarItem.tooltip = `${t('statusBar.noActiveSubscriptionTooltip')}\n\n${t('statusBar.clickInstructions')}`; + this.statusBarItem.tooltip = `${t( + "statusBar.noActiveSubscriptionTooltip" + )}\n\n${t("statusBar.clickInstructions")}`; } // ==================== 使用量统计 ==================== @@ -307,10 +324,11 @@ export class TraeUsageProvider { return { totalUsage, totalLimit, hasValidPacks }; } - this.usageData.user_entitlement_pack_list.forEach(pack => { + this.usageData.user_entitlement_pack_list.forEach((pack) => { const usage = pack.usage.premium_model_fast_amount; - const limit = pack.entitlement_base_info.quota.premium_model_fast_request_limit; - + const limit = + pack.entitlement_base_info.quota.premium_model_fast_request_limit; + if (limit > 0) { totalUsage += usage; totalLimit += limit; @@ -327,16 +345,23 @@ export class TraeUsageProvider { } // 可测试的静态方法:根据数据构建 tooltip - public static buildTooltipFromData(usageData: ApiResponse | null, currentTime?: Date): string { + public static buildTooltipFromData( + usageData: ApiResponse | null, + currentTime?: Date + ): string { if (!usageData || usageData.code === 1001) { - return `${t('statusBar.clickToConfigureSession')}\n\n${t('statusBar.clickInstructions')}`; + return `${t("statusBar.clickToConfigureSession")}\n\n${t( + "statusBar.clickInstructions" + )}`; } const sections: string[] = []; - const validPacks = TraeUsageProvider.getValidPacks(usageData.user_entitlement_pack_list); + const validPacks = TraeUsageProvider.getValidPacks( + usageData.user_entitlement_pack_list + ); if (validPacks.length === 0) { - sections.push(t('tooltip.noValidPacks')); + sections.push(t("tooltip.noValidPacks")); } else { const packSections = TraeUsageProvider.buildPackSections(validPacks); sections.push(...packSections); @@ -344,44 +369,51 @@ export class TraeUsageProvider { // 添加更新时间 const timeSection = TraeUsageProvider.buildTimeSection(currentTime); - sections.push(''); + sections.push(""); sections.push(timeSection); - return sections.join('\n'); + return sections.join("\n"); } // 获取有效的订阅包 public static getValidPacks(packList: EntitlementPack[]): EntitlementPack[] { - return packList.filter(pack => TraeUsageProvider.hasValidUsageData(pack)); + return packList.filter((pack) => TraeUsageProvider.hasValidUsageData(pack)); } // 构建订阅包信息段落 public static buildPackSections(validPacks: EntitlementPack[]): string[] { const sections: string[] = []; - + validPacks.forEach((pack, index) => { const { usage, entitlement_base_info } = pack; const { quota } = entitlement_base_info; - + // 获取订阅类型标识 const subscriptionType = TraeUsageProvider.getSubscriptionTypeLabel(pack); - + // Premium Fast Request使用情况(带进度条) const fastUsed = usage.premium_model_fast_amount; const fastLimit = quota.premium_model_fast_request_limit; - + if (fastLimit > 0) { - const progressInfo = TraeUsageProvider.buildProgressBar(fastUsed, fastLimit); - + const progressInfo = TraeUsageProvider.buildProgressBar( + fastUsed, + fastLimit + ); + // Add subscription summary header - const header = `${subscriptionType} (${fastUsed}/${fastLimit}) Expire: ${formatTimestamp(entitlement_base_info.end_time)}`; + const header = `${subscriptionType} (${fastUsed}/${fastLimit}) Expire: ${formatTimestamp( + entitlement_base_info.end_time + )}`; sections.push(header); - - sections.push(`[${progressInfo.progressBar}] ${progressInfo.percentage}%`); - + + sections.push( + `[${progressInfo.progressBar}] ${progressInfo.percentage}%` + ); + // 如果不是最后一个订阅,添加分隔线 if (index < validPacks.length - 1) { - sections.push(''); + sections.push(""); } } }); @@ -390,69 +422,85 @@ export class TraeUsageProvider { } // 构建进度条 - public static buildProgressBar(used: number, limit: number): { progressBar: string; percentage: number } { + public static buildProgressBar( + used: number, + limit: number + ): { progressBar: string; percentage: number } { const percentage = Math.round((used / limit) * 100); const progressBarLength = 25; const filledLength = Math.round((used / limit) * progressBarLength); - const progressBar = '█'.repeat(filledLength) + '░'.repeat(progressBarLength - filledLength); - + const progressBar = + "█".repeat(filledLength) + "░".repeat(progressBarLength - filledLength); + return { progressBar, percentage }; } // 构建时间信息段落 public static buildTimeSection(currentTime?: Date): string { const now = currentTime || new Date(); - const updateTime = now.toLocaleString('zh-CN', { - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', - hour12: false - }).replace(/\/(\d{2})\/(\d{2})/, '$1/$2').replace(/, /, ' '); - - return `${' '.repeat(50)}🕐 ${updateTime}`; + const updateTime = now + .toLocaleString("zh-CN", { + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + hour12: false, + }) + .replace(/\/(\d{2})\/(\d{2})/, "$1/$2") + .replace(/, /, " "); + + return `${" ".repeat(50)}🕐 ${updateTime}`; } // 检查订阅包是否有有效的使用数据 public static hasValidUsageData(pack: EntitlementPack): boolean { const { quota } = pack.entitlement_base_info; - return quota.premium_model_fast_request_limit > 0 || - quota.premium_model_slow_request_limit > 0 || - quota.auto_completion_limit > 0 || - quota.advanced_model_request_limit > 0; + return ( + quota.premium_model_fast_request_limit > 0 || + quota.premium_model_slow_request_limit > 0 || + quota.auto_completion_limit > 0 || + quota.advanced_model_request_limit > 0 + ); } // 获取订阅类型标签 public static getSubscriptionTypeLabel(pack: EntitlementPack): string { const { entitlement_base_info } = pack; - + // 根据product_type判断订阅类型 if (entitlement_base_info.product_type !== undefined) { const productType = entitlement_base_info.product_type; switch (productType) { case 1: - return 'Pro Plan'; + return "Pro Plan"; case 2: - return 'Extra Package'; + return "Extra Package"; default: - return 'Unknown'; + return "Unknown"; } } - + // 如果没有product_type,根据其他特征判断 const { quota } = entitlement_base_info; if (quota.premium_model_fast_request_limit === -1) { - return 'Unlimited'; + return "Unlimited"; } else if (quota.premium_model_fast_request_limit > 1000) { - return 'Premium'; + return "Premium"; } else { - return 'Basic'; + return "Basic"; } } // ==================== API 调用 ==================== - private async getTokenFromSession(sessionId: string, retryCount = 0): Promise { - return this.apiService.getTokenFromSession(sessionId, retryCount, this.isManualRefresh); + private async getTokenFromSession( + sessionId: string, + retryCount = 0 + ): Promise { + return this.apiService.getTokenFromSession( + sessionId, + retryCount, + this.isManualRefresh + ); } async fetchUsageData(retryCount = 0): Promise { @@ -477,38 +525,36 @@ export class TraeUsageProvider { } private getSessionId(): string | undefined { - const config = vscode.workspace.getConfiguration('traeUsage'); - return config.get('sessionId'); + const config = vscode.workspace.getConfiguration("traeUsage"); + return config.get("sessionId"); } - - private async callUsageApi(authToken: string) { return this.apiService.getUserEntitlementList(authToken); } private async handleApiResponse(data: ApiResponse): Promise { this.usageData = data; - this.isAuthFailed = false; // 清除认证失败状态 - logWithTime('更新使用量数据'); - + this.isAuthFailed = false; // 清除认证失败状态 + logWithTime("更新使用量数据"); + // 使用apiService的统一错误处理 if (!this.apiService.isApiResponseSuccess(data)) { - this.apiService.handleApiResponseError(data, '获取使用量数据'); + this.apiService.handleApiResponseError(data, "获取使用量数据"); if (data?.code === 1001) { this.handleTokenExpired(); } } - this.updateStatusBar(); this.resetRefreshState(); + this.updateStatusBar(); } private handleTokenExpired(): void { - logWithTime('Token已失效(code: 1001),清除缓存'); - this.isAuthFailed = true; // 设置认证失败状态 + logWithTime("Token已失效(code: 1001),清除缓存"); + this.isAuthFailed = true; // 设置认证失败状态 this.clearCache(); - + if (this.isManualRefresh) { this.showAuthExpiredMessage(); } @@ -521,7 +567,7 @@ export class TraeUsageProvider { // ==================== 错误处理 ==================== private handleNoSessionId(): void { - this.isAuthFailed = false; // 清除认证失败状态 + this.isAuthFailed = false; // 清除认证失败状态 if (this.isManualRefresh) { this.showSetSessionMessage(); this.resetRefreshState(); @@ -531,10 +577,10 @@ export class TraeUsageProvider { } private handleNoToken(): void { - this.isAuthFailed = true; // 设置认证失败状态 + this.isAuthFailed = true; // 设置认证失败状态 this.resetRefreshState(); this.updateStatusBar(); - + if (this.isManualRefresh) { // 手动刷新时显示更新Session对话框 showUpdateSessionDialog(); @@ -543,33 +589,39 @@ export class TraeUsageProvider { } private handleFetchError(error: any, retryCount: number): void { - logWithTime(`获取使用量数据失败 (尝试 ${retryCount + 1}/${MAX_RETRY_COUNT}): ${error}`); - + logWithTime( + `获取使用量数据失败 (尝试 ${retryCount + 1}/${MAX_RETRY_COUNT}): ${error}` + ); + // 处理401认证失败情况 if (error.response?.status === 401) { this.isAuthFailed = true; this.resetRefreshState(); this.updateStatusBar(); - + if (this.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; } - + if (this.isManualRefresh) { if (this.apiService.isRetryableError(error)) { - vscode.window.showErrorMessage(t('messages.networkUnstable')); + vscode.window.showErrorMessage(t("messages.networkUnstable")); } else { this.showFetchErrorMessage(error); } @@ -577,11 +629,11 @@ export class TraeUsageProvider { this.updateStatusBar(); return; } - + if (retryCount < MAX_RETRY_COUNT) { this.scheduleRetry(retryCount); } else { - logWithTime('API调用失败,已达到最大重试次数,停止重试'); + logWithTime("API调用失败,已达到最大重试次数,停止重试"); // 达到最大重试次数后,恢复状态栏状态 this.resetRefreshState(); this.updateStatusBar(); @@ -597,41 +649,49 @@ export class TraeUsageProvider { // ==================== 消息显示 ==================== private showSetSessionMessage(): void { - vscode.window.showWarningMessage( - t('messages.pleaseSetSessionId'), - t('messages.setSessionId') - ).then(selection => { - if (selection === t('messages.setSessionId')) { - vscode.commands.executeCommand('traeUsage.updateSession'); - } - }); + vscode.window + .showWarningMessage( + t("messages.pleaseSetSessionId"), + t("messages.setSessionId") + ) + .then((selection) => { + if (selection === t("messages.setSessionId")) { + vscode.commands.executeCommand("traeUsage.updateSession"); + } + }); } private showTokenErrorMessage(): void { - vscode.window.showErrorMessage( - t('messages.cannotGetToken'), - t('messages.updateSessionId') - ).then(selection => { - if (selection === t('messages.updateSessionId')) { - vscode.commands.executeCommand('traeUsage.updateSession'); - } - }); + vscode.window + .showErrorMessage( + t("messages.cannotGetToken"), + t("messages.updateSessionId") + ) + .then((selection) => { + if (selection === t("messages.updateSessionId")) { + vscode.commands.executeCommand("traeUsage.updateSession"); + } + }); } private showAuthExpiredMessage(): void { - vscode.window.showErrorMessage( - t('messages.authenticationExpired'), - t('messages.updateSessionId') - ).then(selection => { - if (selection === t('messages.updateSessionId')) { - vscode.commands.executeCommand('traeUsage.updateSession'); - } - }); + vscode.window + .showErrorMessage( + t("messages.authenticationExpired"), + t("messages.updateSessionId") + ) + .then((selection) => { + if (selection === t("messages.updateSessionId")) { + vscode.commands.executeCommand("traeUsage.updateSession"); + } + }); } private showFetchErrorMessage(error: any): void { vscode.window.showErrorMessage( - t('messages.getUsageDataFailed', { error: error?.toString() || 'Unknown error' }) + t("messages.getUsageDataFailed", { + error: error?.toString() || "Unknown error", + }) ); } @@ -639,10 +699,10 @@ export class TraeUsageProvider { public startAutoRefresh(): void { this.clearRefreshTimer(); - const config = vscode.workspace.getConfiguration('traeUsage'); - const intervalSeconds = config.get('refreshInterval', 300); + const config = vscode.workspace.getConfiguration("traeUsage"); + const intervalSeconds = config.get("refreshInterval", 300); const intervalMilliseconds = intervalSeconds * 1000; - + const maxInterval = 2147483647; const safeInterval = Math.min(intervalMilliseconds, maxInterval); @@ -680,7 +740,7 @@ class ClipboardMonitor { try { const clipboardText = await vscode.env.clipboard.readText(); const sessionMatch = clipboardText.match(/X-Cloudide-Session=([^\s;]+)/); - + if (sessionMatch?.[1]) { await this.handleSessionDetected(sessionMatch[1]); } @@ -690,9 +750,9 @@ class ClipboardMonitor { } private async handleSessionDetected(sessionId: string): Promise { - const config = vscode.workspace.getConfiguration('traeUsage'); - const currentSessionId = config.get('sessionId'); - + const config = vscode.workspace.getConfiguration("traeUsage"); + const currentSessionId = config.get("sessionId"); + if (sessionId !== currentSessionId) { await this.promptUpdateSession(sessionId, config); this.lastNotifiedSessionId = null; @@ -702,24 +762,35 @@ class ClipboardMonitor { } } - private async promptUpdateSession(sessionId: string, config: vscode.WorkspaceConfiguration): Promise { + private async promptUpdateSession( + sessionId: string, + config: vscode.WorkspaceConfiguration + ): Promise { const choice = await vscode.window.showInformationMessage( - t('messages.clipboardSessionDetected', { sessionId: sessionId.substring(0, 20) }), - t('messages.confirmUpdate'), - t('messages.cancel') + t("messages.clipboardSessionDetected", { + sessionId: sessionId.substring(0, 20), + }), + t("messages.confirmUpdate"), + t("messages.cancel") ); - - if (choice === t('messages.confirmUpdate')) { - await config.update('sessionId', sessionId, vscode.ConfigurationTarget.Global); + + if (choice === t("messages.confirmUpdate")) { + await config.update( + "sessionId", + sessionId, + vscode.ConfigurationTarget.Global + ); await getApiService().resetToDefaultHost(); - vscode.window.showInformationMessage(t('messages.sessionIdAutoUpdated')); - vscode.commands.executeCommand('traeUsage.refresh'); + vscode.window.showInformationMessage(t("messages.sessionIdAutoUpdated")); + vscode.commands.executeCommand("traeUsage.refresh"); } } private notifySameSession(sessionId: string): void { vscode.window.showInformationMessage( - t('messages.sameSessionIdDetected', { sessionId: sessionId.substring(0, 20) }) + t("messages.sameSessionIdDetected", { + sessionId: sessionId.substring(0, 20), + }) ); } } @@ -727,53 +798,62 @@ class ClipboardMonitor { // ==================== 扩展激活 ==================== export function activate(context: vscode.ExtensionContext) { initializeI18n(); - + const provider = new TraeUsageProvider(context); const clipboardMonitor = new ClipboardMonitor(); registerCommands(context, provider); registerListeners(context, provider, clipboardMonitor); - + context.subscriptions.push(provider); } -function registerCommands(context: vscode.ExtensionContext, provider: TraeUsageProvider): void { +function registerCommands( + context: vscode.ExtensionContext, + provider: TraeUsageProvider +): void { const commands = [ - vscode.commands.registerCommand('traeUsage.handleStatusBarClick', () => { + vscode.commands.registerCommand("traeUsage.handleStatusBarClick", () => { provider.handleStatusBarClick(); }), - vscode.commands.registerCommand('traeUsage.refresh', () => { + vscode.commands.registerCommand("traeUsage.refresh", () => { provider.refresh(); }), - vscode.commands.registerCommand('traeUsage.updateSession', async () => { + vscode.commands.registerCommand("traeUsage.updateSession", async () => { await showUpdateSessionDialog(); }), - vscode.commands.registerCommand('traeUsage.collectUsageDetails', () => { + vscode.commands.registerCommand("traeUsage.collectUsageDetails", () => { provider.collectUsageDetails(); }), - vscode.commands.registerCommand('traeUsage.showUsageDashboard', () => { + vscode.commands.registerCommand("traeUsage.showUsageDashboard", () => { provider.showUsageDashboard(); }), - vscode.commands.registerCommand('traeUsage.showOutput', () => { + vscode.commands.registerCommand("traeUsage.showOutput", () => { provider.showOutput(); - }) + }), ]; - + context.subscriptions.push(...commands); } -function registerListeners(context: vscode.ExtensionContext, provider: TraeUsageProvider, clipboardMonitor: ClipboardMonitor): void { - const windowStateListener = vscode.window.onDidChangeWindowState(async (e) => { - if (e.focused) { - setTimeout(() => clipboardMonitor.checkForSession(), 500); +function registerListeners( + context: vscode.ExtensionContext, + provider: TraeUsageProvider, + clipboardMonitor: ClipboardMonitor +): void { + const windowStateListener = vscode.window.onDidChangeWindowState( + async (e) => { + if (e.focused) { + setTimeout(() => clipboardMonitor.checkForSession(), 500); + } } - }); + ); - const configListener = vscode.workspace.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('traeUsage.refreshInterval')) { + const configListener = vscode.workspace.onDidChangeConfiguration((e) => { + if (e.affectsConfiguration("traeUsage.refreshInterval")) { provider.startAutoRefresh(); } - if (e.affectsConfiguration('traeUsage.language')) { + if (e.affectsConfiguration("traeUsage.language")) { initializeI18n(); provider.fetchUsageData(); } @@ -785,26 +865,28 @@ function registerListeners(context: vscode.ExtensionContext, provider: TraeUsage async function showUpdateSessionDialog(): Promise { const defaultBrowser = await detectDefaultBrowser(); logWithTime(`更新Session时检测到默认浏览器: ${defaultBrowser}`); - + const extensionUrl = getBrowserExtensionUrl(defaultBrowser); - + const choice = await vscode.window.showInformationMessage( - t('messages.sessionConfigurationMessage'), - t('messages.visitOfficialUsagePage'), - t('messages.installBrowserExtension') + t("messages.sessionConfigurationMessage"), + t("messages.visitOfficialUsagePage"), + t("messages.installBrowserExtension") ); - - if (choice === t('messages.visitOfficialUsagePage')) { - vscode.env.openExternal(vscode.Uri.parse('https://www.trae.ai/account-setting#usage')); - } else if (choice === t('messages.installBrowserExtension')) { + + if (choice === t("messages.visitOfficialUsagePage")) { + vscode.env.openExternal( + vscode.Uri.parse("https://www.trae.ai/account-setting#usage") + ); + } else if (choice === t("messages.installBrowserExtension")) { vscode.env.openExternal(vscode.Uri.parse(extensionUrl)); } } function getBrowserExtensionUrl(browserType: BrowserType): string { - return browserType === 'edge' - ? 'https://microsoftedge.microsoft.com/addons/detail/trae-usage-token-extracto/leopdblngeedggognlgokdlfpiojalji' - : 'https://chromewebstore.google.com/detail/edkpaodbjadikhahggapfilgmfijjhei'; + return browserType === "edge" + ? "https://microsoftedge.microsoft.com/addons/detail/trae-usage-token-extracto/leopdblngeedggognlgokdlfpiojalji" + : "https://chromewebstore.google.com/detail/edkpaodbjadikhahggapfilgmfijjhei"; } // ==================== 类型定义补充 ==================== @@ -815,4 +897,3 @@ interface UsageStats { } export function deactivate() {} - From 6e00f278bb914bbfd89de05d4a0ead91557f908f Mon Sep 17 00:00:00 2001 From: mtpupil <970632312@qq.com> Date: Wed, 10 Dec 2025 09:00:58 +0800 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E9=85=8D=E7=BD=AE=E5=92=8C=E5=9B=BD=E9=99=85=E5=8C=96?= =?UTF-8?q?=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 更新 package.json 版本号和显示名称 添加多语言关键词支持 完善 vscode 主题颜色配置 国际化仪表盘界面和错误提示 优化 README 文档说明 --- .vscode/settings.json | 53 +++++- README.md | 43 +++-- package.json | 52 +++++- src/dashboardGenerator.ts | 377 ++++++++++++++++++++++++-------------- 4 files changed, 360 insertions(+), 165 deletions(-) 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.json b/package.json index 5653864..15d375a 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "trae-usage-monitor", - "displayName": "Trae Usage", + "displayName": "Trae Usage Reborn", "description": "Monitor Trae AI usage statistics in real-time", - "version": "1.3.3", + "version": "1.3.4-SNAPSHOT", "publisher": "whyuds", "repository": { "type": "git", @@ -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/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 使用统计看板