From d5eb882dfe1b4ea333aa01a3ac915c46dc12fbe9 Mon Sep 17 00:00:00 2001 From: Sirius Date: Thu, 6 Aug 2026 12:01:17 +0800 Subject: [PATCH] =?UTF-8?q?token=E7=BB=9F=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- src/index.ts | 2 ++ src/renderer.ts | 45 ++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 41b6f2b..fa8693e 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ 原因是 v1.10.0 没有公开嵌入 API,交互式 TUI 没有独立的结构化最终结果通道。headless 的最后一行提供稳定 `finalText`,能保证工具参数、工具结果、状态、stderr、思考事件和 ANSI 内容不会混入 API 回复。 -终端会显示:请求开始、模型、turn 状态、工具事件、工具参数、工具结果、stderr、最终回答和耗时。终端不会显示原始 Ink TUI、动画、键盘快捷键、人工权限批准、人工问题回答和详细内部思考文本。 +终端会显示:请求开始、模型、turn 状态、工具事件、工具参数、工具结果、stderr、最终回答和耗时。每个 turn 结束时还会显示本轮和服务启动以来的累计 input/output/total token;服务退出时再输出一次累计,重启后从零开始。终端不会显示原始 Ink TUI、动画、键盘快捷键、人工权限批准、人工问题回答和详细内部思考文本。 本机 v1.10.0 实测表明,headless 即使设为 `auto-accept`,shell 仍会被拒绝。默认 `dangerously_skip_permissions: true`,因此实际传入 `--yolo`,让文件写入和命令工具能继续执行。它会绕过所有权限确认,只应对可信工作目录使用。若要只读,把 `dangerously_skip_permissions` 改为 `false`,同时把 `permission_mode` 改成 `plan`。 diff --git a/src/index.ts b/src/index.ts index e90a9ae..ab2057f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ #!/usr/bin/env node import path from "node:path"; import { loadConfig } from "./config.js"; +import { printCumulativeUsage } from "./renderer.js"; import { createServer } from "./server.js"; function configPathFromArgs(args: string[]): string { @@ -23,6 +24,7 @@ async function main(): Promise { } await bridge.app.listen({ host: config.host, port: config.port }); + process.once("exit", printCumulativeUsage); process.stdout.write(`Command Code OpenAI Bridge 已启动\n`); process.stdout.write(`API: http://${config.host}:${config.port}/v1\n`); process.stdout.write(`工作目录: ${config.resolvedWorkingDirectory}\n`); diff --git a/src/renderer.ts b/src/renderer.ts index db16738..cbd1869 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -1,4 +1,5 @@ import { inspect } from "node:util"; +import type { CommandUsage } from "./openai.js"; const tty = process.stdout.isTTY; const color = (code: number, text: string) => tty ? `\u001b[${code}m${text}\u001b[0m` : text; @@ -6,6 +7,40 @@ const cyan = (text: string) => color(36, text); const green = (text: string) => color(32, text); const yellow = (text: string) => color(33, text); const dim = (text: string) => color(2, text); +const tokenNumber = new Intl.NumberFormat("zh-CN"); +const cumulativeUsage: Required = { inputTokens: 0, outputTokens: 0 }; +let cumulativeUsagePrinted = false; + +function tokenCount(value: unknown): number | undefined { + return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 + ? value + : undefined; +} + +function readUsage(value: unknown): Required | undefined { + if (typeof value !== "object" || value === null) return undefined; + const usage = value as Record; + const inputTokens = tokenCount(usage.inputTokens); + const outputTokens = tokenCount(usage.outputTokens); + if (inputTokens === undefined && outputTokens === undefined) return undefined; + return { + inputTokens: inputTokens ?? 0, + outputTokens: outputTokens ?? 0, + }; +} + +function formatUsage(usage: Required): string { + const totalTokens = usage.inputTokens + usage.outputTokens; + return `输入 ${tokenNumber.format(usage.inputTokens)} · 输出 ${tokenNumber.format(usage.outputTokens)} · 合计 ${tokenNumber.format(totalTokens)}`; +} + +export function printCumulativeUsage(): void { + if (cumulativeUsagePrinted) return; + cumulativeUsagePrinted = true; + process.stdout.write(`\n${cyan("━━━━━━━━ Token 累计 ━━━━━━━━")}\n`); + process.stdout.write(`${formatUsage(cumulativeUsage)}\n`); + process.stdout.write(`${cyan("━━━━━━━━━━━━━━━━━━━━━━━━━━")}\n`); +} function details(event: Record): string { const copy = { ...event }; @@ -61,9 +96,17 @@ export class TerminalRenderer { case "model_request_end": process.stdout.write(`\n${dim(`模型请求结束 · ${String(event.stopReason ?? "unknown")}`)}\n`); return; - case "turn_end": + case "turn_end": { + const usage = readUsage(event.usage); + if (usage) { + cumulativeUsage.inputTokens += usage.inputTokens; + cumulativeUsage.outputTokens += usage.outputTokens; + } process.stdout.write(`${dim(`Turn ${String(event.turnNumber ?? "?")} 结束`)}\n`); + process.stdout.write(`${dim("Token 本轮")} · ${usage ? formatUsage(usage) : "不可用"}\n`); + process.stdout.write(`${dim("Token 累计")} · ${formatUsage(cumulativeUsage)}\n`); return; + } default: if (type.includes("tool") || type.includes("permission") || type.includes("question")) { process.stdout.write(`${yellow(`\n[${type}]`)}\n${details(event)}\n`);