coding-tool-x 3.5.11 → 3.5.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +11 -1
  3. package/dist/web/assets/{Analytics-Bw-0r4Ys.js → Analytics-B9Shk-uq.js} +1 -1
  4. package/dist/web/assets/{ConfigTemplates-C6QRIeDj.js → ConfigTemplates-Dlkn-_zW.js} +1 -1
  5. package/dist/web/assets/Home-AGf32LvJ.js +1 -0
  6. package/dist/web/assets/Home-BeGkC4bb.css +1 -0
  7. package/dist/web/assets/{PluginManager-Ce-mws6Q.js → PluginManager-BnAjHN4u.js} +1 -1
  8. package/dist/web/assets/{ProjectList-9Z7FFCIT.js → ProjectList-7YV_zk9L.js} +1 -1
  9. package/dist/web/assets/{SessionList-CyETQcs8.js → SessionList-D0un-UKS.js} +1 -1
  10. package/dist/web/assets/{SkillManager-DSUcP1se.js → SkillManager-DhYBmbtd.js} +1 -1
  11. package/dist/web/assets/{WorkspaceManager-Cvbc11bJ.js → WorkspaceManager-JOZIPAGo.js} +1 -1
  12. package/dist/web/assets/icons-QFXzEgqM.js +1 -0
  13. package/dist/web/assets/index-DM3yF1Oo.js +2 -0
  14. package/dist/web/assets/{index-BS7aplYp.css → index-dzKgnRhx.css} +1 -1
  15. package/dist/web/index.html +4 -4
  16. package/package.json +2 -1
  17. package/src/commands/daemon.js +6 -2
  18. package/src/commands/ui.js +8 -3
  19. package/src/index.js +2 -0
  20. package/src/server/api/hooks.js +18 -1
  21. package/src/server/codex-proxy-server.js +22 -6
  22. package/src/server/gemini-proxy-server.js +3 -1
  23. package/src/server/index.js +37 -5
  24. package/src/server/opencode-proxy-server.js +39 -14
  25. package/src/server/proxy-server.js +3 -1
  26. package/src/server/services/base/proxy-utils.js +41 -0
  27. package/src/server/services/base/response-usage-parser.js +137 -59
  28. package/src/server/services/codex-statistics-service.js +1 -0
  29. package/src/server/services/gemini-statistics-service.js +1 -0
  30. package/src/server/services/https-cert.js +202 -0
  31. package/src/server/services/notification-hooks.js +138 -6
  32. package/src/server/services/opencode-gateway-converter.js +3 -0
  33. package/src/server/services/opencode-statistics-service.js +1 -0
  34. package/src/server/services/proxy-log-helper.js +44 -12
  35. package/src/server/services/statistics-service.js +73 -28
  36. package/src/server/services/web-build.js +172 -0
  37. package/src/server/websocket-server.js +18 -1
  38. package/dist/web/assets/Home-BQxQ1LhR.css +0 -1
  39. package/dist/web/assets/Home-B_Z9ythw.js +0 -1
  40. package/dist/web/assets/icons-lKCTySl7.js +0 -1
  41. package/dist/web/assets/index-CTeg1xG-.js +0 -2
@@ -4,15 +4,15 @@
4
4
  <meta charset="UTF-8">
5
5
  <link rel="icon" href="/favicon.ico">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
- <title>CC-TOOL - ClaudeCode增强工作助手</title>
8
- <script type="module" crossorigin src="/assets/index-CTeg1xG-.js"></script>
7
+ <title>coding-tool-x · AI 编程工作台</title>
8
+ <script type="module" crossorigin src="/assets/index-DM3yF1Oo.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="/assets/markdown-BNJfEgCA.js">
10
10
  <link rel="modulepreload" crossorigin href="/assets/vue-vendor-B5RqjEjQ.js">
11
11
  <link rel="modulepreload" crossorigin href="/assets/naive-ui-DM897mIP.js">
12
- <link rel="modulepreload" crossorigin href="/assets/icons-lKCTySl7.js">
12
+ <link rel="modulepreload" crossorigin href="/assets/icons-QFXzEgqM.js">
13
13
  <link rel="modulepreload" crossorigin href="/assets/vendors-DQA4Hw9v.js">
14
14
  <link rel="stylesheet" crossorigin href="/assets/markdown-BfC0goYb.css">
15
- <link rel="stylesheet" crossorigin href="/assets/index-BS7aplYp.css">
15
+ <link rel="stylesheet" crossorigin href="/assets/index-dzKgnRhx.css">
16
16
  </head>
17
17
  <body>
18
18
  <div id="app"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coding-tool-x",
3
- "version": "3.5.11",
3
+ "version": "3.5.13",
4
4
  "description": "Vibe Coding 增强工作助手 - 智能会话管理、动态渠道切换、全局搜索、实时监控",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -19,6 +19,7 @@
19
19
  "test:windows": "node scripts/test-windows-regression.js",
20
20
  "benchmark:codex": "node scripts/benchmark-codex-loading.js",
21
21
  "build:web": "cd src/web && npm run build",
22
+ "prepack": "npm run build:web",
22
23
  "dev:web": "cd src/web && npm run dev",
23
24
  "dev:server": "nodemon"
24
25
  },
@@ -281,10 +281,14 @@ async function handleStart() {
281
281
 
282
282
  // 检查是否启用 LAN 访问 (--host 标志)
283
283
  const enableHost = process.argv.includes('--host');
284
+ const enableHttps = process.argv.includes('--https');
284
285
  const pmArgs = ['ui', '--daemon'];
285
286
  if (enableHost) {
286
287
  pmArgs.push('--host');
287
288
  }
289
+ if (enableHttps) {
290
+ pmArgs.push('--https');
291
+ }
288
292
  require('fs').mkdirSync(PATHS.logs, { recursive: true });
289
293
 
290
294
  // 启动 PM2 进程
@@ -335,10 +339,10 @@ async function handleStart() {
335
339
  }
336
340
 
337
341
  console.log(chalk.green('\n[OK] Coding-Tool 服务已启动(后台运行)\n'));
338
- console.log(chalk.gray(`Web UI: http://localhost:${port}`));
342
+ console.log(chalk.gray(`Web UI: ${enableHttps ? 'https' : 'http'}://localhost:${port}`));
339
343
  printPortToolIssue(readyState.degradedPortCheckIssue);
340
344
  if (enableHost) {
341
- console.log(chalk.yellow(`[WARN] LAN 访问已启用 (http://<your-ip>:${port})`));
345
+ console.log(chalk.yellow(`[WARN] LAN 访问已启用 (${enableHttps ? 'https' : 'http'}://<your-ip>:${port})`));
342
346
  }
343
347
  console.log(chalk.gray('\n可以安全关闭此终端窗口'));
344
348
  console.log(chalk.gray('\n常用命令:'));
@@ -10,7 +10,9 @@ async function handleUI() {
10
10
 
11
11
  // 检查是否启用 LAN 访问 (--host 标志)
12
12
  const enableHost = process.argv.includes('--host');
13
+ const enableHttps = process.argv.includes('--https');
13
14
  const host = enableHost ? '0.0.0.0' : '127.0.0.1';
15
+ const protocol = enableHttps ? 'https' : 'http';
14
16
 
15
17
  if (!isDaemon) {
16
18
  console.clear();
@@ -18,15 +20,18 @@ async function handleUI() {
18
20
  if (enableHost) {
19
21
  console.log(chalk.yellow('[WARN] LAN 访问已启用 (--host)\n'));
20
22
  }
23
+ if (enableHttps) {
24
+ console.log(chalk.yellow('[LOCK] 已启用本地 HTTPS 模式 (--https)\n'));
25
+ }
21
26
  }
22
27
 
23
28
  // 从配置加载端口
24
29
  const config = loadConfig();
25
30
  const port = config.ports?.webUI || 19999;
26
- const url = `http://localhost:${port}`;
31
+ const url = `${protocol}://localhost:${port}`;
27
32
 
28
33
  try {
29
- await startServer(port, host);
34
+ await startServer(port, host, { useHttps: enableHttps });
30
35
 
31
36
  // 自动打开浏览器(仅非 daemon 模式)
32
37
  if (!isDaemon) {
@@ -87,7 +92,7 @@ async function handleUI() {
87
92
  console.log(chalk.gray('按 Ctrl+C 停止服务器'));
88
93
  } else {
89
94
  // Daemon 模式:保持运行
90
- console.log(chalk.green(`[OK] Coding-Tool 服务已在后台启动 (端口: ${port})`));
95
+ console.log(chalk.green(`[OK] Coding-Tool 服务已在后台启动 (${protocol.toUpperCase()} 端口: ${port})`));
91
96
  }
92
97
 
93
98
  } catch (error) {
package/src/index.js CHANGED
@@ -45,8 +45,10 @@ function showHelp() {
45
45
 
46
46
  console.log(chalk.yellow('[UI] UI 管理:'));
47
47
  console.log(' ctx ui 前台启动 Web UI(仅本地访问)');
48
+ console.log(' ctx ui --https 前台启动 Web UI(本地 HTTPS)');
48
49
  console.log(' ctx ui --host 前台启动 Web UI(允许 LAN 访问)');
49
50
  console.log(' ctx ui start 后台启动 Web UI');
51
+ console.log(' ctx ui start --https 后台启动 Web UI(本地 HTTPS)');
50
52
  console.log(' ctx ui start --host 后台启动 Web UI(允许 LAN 访问)');
51
53
  console.log(' ctx ui stop 停止 Web UI');
52
54
  console.log(' ctx ui restart 重启 Web UI\n');
@@ -1,7 +1,7 @@
1
1
  const express = require('express');
2
2
  const router = express.Router();
3
3
  const notificationHooks = require('../services/notification-hooks');
4
- const { createSameOriginGuard } = require('../services/network-access');
4
+ const { createSameOriginGuard, isLoopbackRequest } = require('../services/network-access');
5
5
 
6
6
  router.use(createSameOriginGuard({
7
7
  message: '禁止跨站访问通知配置接口'
@@ -42,4 +42,21 @@ router.post('/test', async (req, res) => {
42
42
  }
43
43
  });
44
44
 
45
+ router.post('/browser-event', (req, res) => {
46
+ if (!isLoopbackRequest(req)) {
47
+ return res.status(403).json({ error: '仅允许本机通知脚本访问浏览器通知接口' });
48
+ }
49
+
50
+ try {
51
+ const payload = notificationHooks.emitBrowserNotification(req.body || {});
52
+ res.json({
53
+ success: true,
54
+ notification: payload
55
+ });
56
+ } catch (error) {
57
+ console.error('Error dispatching browser notification event:', error);
58
+ res.status(error.statusCode || 500).json({ error: error.message });
59
+ }
60
+ });
61
+
45
62
  module.exports = router;
@@ -14,7 +14,12 @@ const { createDecodedStream } = require('./services/response-decoder');
14
14
  const { getEffectiveApiKey } = require('./services/codex-channels');
15
15
  const { persistProxyRequestSnapshot } = require('./services/request-logger');
16
16
  const { publishUsageLog, publishFailureLog } = require('./services/proxy-log-helper');
17
- const { redirectModel, resolveTargetUrl } = require('./services/base/proxy-utils');
17
+ const {
18
+ redirectModel,
19
+ resolveTargetUrl,
20
+ isChatCompletionsPath,
21
+ ensureOpenAiStreamUsage
22
+ } = require('./services/base/proxy-utils');
18
23
  const { parseSSEUsage, parseNonStreamingUsage, mergeUsageIntoTokenData, createTokenData } = require('./services/base/response-usage-parser');
19
24
  const { attachServerShutdownHandling, expediteServerShutdown } = require('./services/server-shutdown');
20
25
 
@@ -139,9 +144,10 @@ async function startCodexProxyServer(options = {}) {
139
144
  const requestId = `codex-${Date.now()}-${Math.random()}`;
140
145
  requestMetadata.set(req, {
141
146
  id: requestId,
142
- channel: activeChannel.name,
143
- channelId: activeChannel.id,
144
- startTime: Date.now()
147
+ channel: activeChannel.name,
148
+ channelId: activeChannel.id,
149
+ startTime: Date.now(),
150
+ requestModel: req.body?.model || ''
145
151
  });
146
152
 
147
153
  proxyReq.removeHeader('authorization');
@@ -216,6 +222,8 @@ async function startCodexProxyServer(options = {}) {
216
222
  }
217
223
  });
218
224
 
225
+ let bodyMutated = false;
226
+
219
227
  // 应用模型重定向(当 proxy 开启时)
220
228
  if (req.body && req.body.model) {
221
229
  const originalModel = req.body.model;
@@ -223,14 +231,14 @@ async function startCodexProxyServer(options = {}) {
223
231
 
224
232
  if (redirectedModel !== originalModel) {
225
233
  req.body.model = redirectedModel;
226
- // 更新 rawBody 以匹配修改后的 body
227
- req.rawBody = Buffer.from(JSON.stringify(req.body));
234
+ bodyMutated = true;
228
235
 
229
236
  // 将原始模型和重定向模型存入 metadata,用于日志记录
230
237
  const meta = requestMetadata.get(req);
231
238
  if (meta) {
232
239
  meta.originalModel = originalModel;
233
240
  meta.redirectedModel = redirectedModel;
241
+ meta.requestModel = redirectedModel;
234
242
  }
235
243
 
236
244
  // 只在重定向规则变化时打印日志(避免每次请求都打印)
@@ -243,6 +251,14 @@ async function startCodexProxyServer(options = {}) {
243
251
  }
244
252
  }
245
253
 
254
+ if (shouldParseJson(req) && isChatCompletionsPath(req.url) && ensureOpenAiStreamUsage(req.body)) {
255
+ bodyMutated = true;
256
+ }
257
+
258
+ if (bodyMutated) {
259
+ req.rawBody = Buffer.from(JSON.stringify(req.body));
260
+ }
261
+
246
262
  const target = resolveCodexTarget(channel.baseUrl, req.url);
247
263
 
248
264
  proxy.web(req, res, {
@@ -131,7 +131,8 @@ async function startGeminiProxyServer(options = {}) {
131
131
  channel: activeChannel.name,
132
132
  channelId: activeChannel.id,
133
133
  startTime: Date.now(),
134
- modelFromUrl
134
+ modelFromUrl,
135
+ requestModel: modelFromUrl
135
136
  });
136
137
 
137
138
  proxyReq.removeHeader('authorization');
@@ -214,6 +215,7 @@ async function startGeminiProxyServer(options = {}) {
214
215
  meta.originalModel = originalModel;
215
216
  meta.redirectedModel = redirectedModel;
216
217
  meta.modelFromUrl = redirectedModel;
218
+ meta.requestModel = redirectedModel;
217
219
  }
218
220
 
219
221
  // 只在重定向规则变化时打印日志(避免每次请求都打印)
@@ -1,3 +1,5 @@
1
+ const http = require('http');
2
+ const https = require('https');
1
3
  const express = require('express');
2
4
  const path = require('path');
3
5
  const chalk = require('chalk');
@@ -23,6 +25,8 @@ const { startGeminiProxyServer } = require('./gemini-proxy-server');
23
25
  const { startOpenCodeProxyServer, collectProxyModelList } = require('./opencode-proxy-server');
24
26
  const { createRemoteMutationGuard } = require('./services/network-access');
25
27
  const { createApiRequestLogger } = require('./services/request-logger');
28
+ const { inspectWebBuildState, ensureWebDistReady } = require('./services/web-build');
29
+ const { ensureHttpsCredentials } = require('./services/https-cert');
26
30
 
27
31
  function getInquirer() {
28
32
  return require('inquirer');
@@ -130,7 +134,21 @@ async function startServer(port, host = '127.0.0.1', options = {}) {
130
134
  console.log(chalk.green('[v] 端口已释放\n'));
131
135
  }
132
136
 
137
+ const webBuildState = inspectWebBuildState();
138
+ if (webBuildState.needsBuild) {
139
+ const reasonText = webBuildState.reason === 'dist-missing'
140
+ ? '缺少 Web UI 静态资源'
141
+ : '检测到 Web UI 静态资源已过期';
142
+ console.log(chalk.cyan(`[BUILD] ${reasonText},正在重新构建...`));
143
+
144
+ const buildResult = await ensureWebDistReady({ state: webBuildState });
145
+ if (buildResult.built) {
146
+ console.log(chalk.green('[OK] Web UI 静态资源已更新\n'));
147
+ }
148
+ }
149
+
133
150
  const app = express();
151
+ const useHttps = options.useHttps === true || process.argv.includes('--https');
134
152
  const lanMode = host === '0.0.0.0';
135
153
  const allowRemoteMutation = process.env.CC_TOOL_ALLOW_REMOTE_WRITE === 'true';
136
154
 
@@ -245,7 +263,15 @@ async function startServer(port, host = '127.0.0.1', options = {}) {
245
263
  }
246
264
 
247
265
  // Start server(确保监听成功后才返回,避免命令误报“已启动”)
248
- const server = app.listen(port, host);
266
+ let server;
267
+ if (useHttps) {
268
+ const httpsCredentials = ensureHttpsCredentials();
269
+ server = https.createServer(httpsCredentials, app);
270
+ server.listen(port, host);
271
+ } else {
272
+ server = http.createServer(app);
273
+ server.listen(port, host);
274
+ }
249
275
  await new Promise((resolve) => {
250
276
  const onListening = () => {
251
277
  server.off('error', onError);
@@ -270,17 +296,23 @@ async function startServer(port, host = '127.0.0.1', options = {}) {
270
296
  });
271
297
 
272
298
  console.log(`\n[START] Coding-Tool Web UI running at:`);
299
+ const protocol = useHttps ? 'https' : 'http';
300
+ const wsProtocol = useHttps ? 'wss' : 'ws';
273
301
  if (host === '0.0.0.0') {
274
302
  console.log(chalk.yellow(` [WARN] 警告: 服务正在监听所有网络接口 (LAN 可访问)`));
275
- console.log(` http://localhost:${port}`);
276
- console.log(chalk.gray(` http://<your-ip>:${port} (LAN 访问)`));
303
+ console.log(` ${protocol}://localhost:${port}`);
304
+ console.log(chalk.gray(` ${protocol}://<your-ip>:${port} (LAN 访问)`));
277
305
  } else {
278
- console.log(` http://localhost:${port}`);
306
+ console.log(` ${protocol}://localhost:${port}`);
279
307
  }
280
308
 
281
309
  // 附加 WebSocket 服务器到同一个端口
282
310
  attachWebSocketServer(server, { host });
283
- console.log(` ws://localhost:${port}/ws\n`);
311
+ console.log(` ${wsProtocol}://localhost:${port}/ws\n`);
312
+
313
+ if (useHttps) {
314
+ console.log(chalk.gray(' [TIP] 首次访问自签名证书时,浏览器可能会提示手动信任本地证书'));
315
+ }
284
316
 
285
317
  if (host === '0.0.0.0' && !allowRemoteMutation) {
286
318
  console.log(chalk.yellow(' [LOCK] 已启用 LAN 安全保护:远程写操作默认禁用'));
@@ -21,7 +21,11 @@ const { getEnabledChannels, getEffectiveApiKey } = require('./services/opencode-
21
21
  const { persistProxyRequestSnapshot, loadClaudeRequestTemplate } = require('./services/request-logger');
22
22
  const { probeModelAvailability, fetchModelsFromProvider } = require('./services/model-detector');
23
23
  const { publishUsageLog, publishFailureLog } = require('./services/proxy-log-helper');
24
- const { redirectModel, resolveTargetUrl } = require('./services/base/proxy-utils');
24
+ const {
25
+ redirectModel,
26
+ resolveTargetUrl,
27
+ ensureOpenAiStreamUsage
28
+ } = require('./services/base/proxy-utils');
25
29
  const { parseSSEUsage, parseNonStreamingUsage, mergeUsageIntoTokenData, createTokenData } = require('./services/base/response-usage-parser');
26
30
  const { attachServerShutdownHandling, expediteServerShutdown } = require('./services/server-shutdown');
27
31
 
@@ -1920,6 +1924,12 @@ function reportOpenCodeGatewayFailure({
1920
1924
  }
1921
1925
 
1922
1926
  function publishOpenCodeUsageLog({ requestId, channel, model, usage, startTime }) {
1927
+ const parsedUsage = parseNonStreamingUsage({
1928
+ model: model || '',
1929
+ usage: usage && typeof usage === 'object' ? usage : {}
1930
+ });
1931
+ const parsedTokens = parsedUsage.tokens || {};
1932
+
1923
1933
  return publishUsageLog({
1924
1934
  source: 'opencode',
1925
1935
  metadata: {
@@ -1928,13 +1938,15 @@ function publishOpenCodeUsageLog({ requestId, channel, model, usage, startTime }
1928
1938
  channelId: channel?.id,
1929
1939
  startTime
1930
1940
  },
1931
- model: model || '',
1941
+ model: parsedUsage.model || model || '',
1932
1942
  tokens: {
1933
- input: Number(usage?.input_tokens || usage?.prompt_tokens || 0),
1934
- output: Number(usage?.output_tokens || usage?.completion_tokens || 0),
1935
- cached: Number(usage?.input_tokens_details?.cached_tokens || 0),
1936
- reasoning: Number(usage?.output_tokens_details?.reasoning_tokens || 0),
1937
- total: Number(usage?.total_tokens || 0)
1943
+ input: Number(parsedTokens.input || 0),
1944
+ output: Number(parsedTokens.output || 0),
1945
+ cacheCreation: Number(parsedTokens.cacheCreation || 0),
1946
+ cacheRead: Number(parsedTokens.cacheRead || 0),
1947
+ cached: Number(parsedTokens.cached || 0),
1948
+ reasoning: Number(parsedTokens.reasoning || 0),
1949
+ total: Number(parsedTokens.total || 0)
1938
1950
  },
1939
1951
  calculateCost,
1940
1952
  broadcastLog,
@@ -3064,6 +3076,7 @@ async function handleClaudeGatewayRequest(req, res, channel, effectiveKey) {
3064
3076
  }
3065
3077
 
3066
3078
  const chatResponseObject = buildOpenAiChatCompletionsObject(parsedBody, originalPayload.model);
3079
+ const loggingUsage = buildOpenAiResponsesObject(parsedBody, originalPayload.model).usage;
3067
3080
  if (wantsStream) {
3068
3081
  sendChatCompletionsSse(res, chatResponseObject);
3069
3082
  } else {
@@ -3073,7 +3086,7 @@ async function handleClaudeGatewayRequest(req, res, channel, effectiveKey) {
3073
3086
  requestId,
3074
3087
  channel,
3075
3088
  model: chatResponseObject.model,
3076
- usage: chatResponseObject.usage,
3089
+ usage: loggingUsage,
3077
3090
  startTime
3078
3091
  });
3079
3092
  recordSuccess(channel.id, 'opencode');
@@ -4114,6 +4127,7 @@ async function handleGeminiGatewayRequest(req, res, channel, effectiveKey) {
4114
4127
  }
4115
4128
 
4116
4129
  const chatResponseObject = buildOpenAiChatCompletionsObjectFromGemini(parsedBody, targetModel);
4130
+ const loggingUsage = buildOpenAiResponsesObjectFromGemini(parsedBody, targetModel).usage;
4117
4131
  if (wantsStream) {
4118
4132
  sendChatCompletionsSse(res, chatResponseObject);
4119
4133
  } else {
@@ -4123,7 +4137,7 @@ async function handleGeminiGatewayRequest(req, res, channel, effectiveKey) {
4123
4137
  requestId,
4124
4138
  channel,
4125
4139
  model: chatResponseObject.model,
4126
- usage: chatResponseObject.usage,
4140
+ usage: loggingUsage,
4127
4141
  startTime
4128
4142
  });
4129
4143
  recordSuccess(channel.id, 'opencode');
@@ -4224,9 +4238,10 @@ async function startOpenCodeProxyServer(options = {}) {
4224
4238
  const requestId = `opencode-${Date.now()}-${Math.random()}`;
4225
4239
  requestMetadata.set(req, {
4226
4240
  id: requestId,
4227
- channel: activeChannel.name,
4228
- channelId: activeChannel.id,
4229
- startTime: Date.now()
4241
+ channel: activeChannel.name,
4242
+ channelId: activeChannel.id,
4243
+ startTime: Date.now(),
4244
+ requestModel: req.body?.model || ''
4230
4245
  });
4231
4246
 
4232
4247
  proxyReq.removeHeader('authorization');
@@ -4313,6 +4328,8 @@ async function startOpenCodeProxyServer(options = {}) {
4313
4328
  }
4314
4329
  });
4315
4330
 
4331
+ let bodyMutated = false;
4332
+
4316
4333
  // 应用模型重定向(当 proxy 开启时)
4317
4334
  if (req.body && typeof req.body === 'object' && !Array.isArray(req.body) && req.body.model) {
4318
4335
  const originalModel = req.body.model;
@@ -4320,14 +4337,14 @@ async function startOpenCodeProxyServer(options = {}) {
4320
4337
 
4321
4338
  if (redirectedModel !== originalModel) {
4322
4339
  req.body.model = redirectedModel;
4323
- // 更新 rawBody 以匹配修改后的 body
4324
- req.rawBody = Buffer.from(JSON.stringify(req.body));
4340
+ bodyMutated = true;
4325
4341
 
4326
4342
  // 将原始模型和重定向模型存入 metadata,用于日志记录
4327
4343
  const meta = requestMetadata.get(req);
4328
4344
  if (meta) {
4329
4345
  meta.originalModel = originalModel;
4330
4346
  meta.redirectedModel = redirectedModel;
4347
+ meta.requestModel = redirectedModel;
4331
4348
  }
4332
4349
 
4333
4350
  // 只在重定向规则变化时打印日志(避免每次请求都打印)
@@ -4340,6 +4357,14 @@ async function startOpenCodeProxyServer(options = {}) {
4340
4357
  }
4341
4358
  }
4342
4359
 
4360
+ if (shouldParseJson(req) && isChatCompletionsPath(req.url) && ensureOpenAiStreamUsage(req.body)) {
4361
+ bodyMutated = true;
4362
+ }
4363
+
4364
+ if (bodyMutated) {
4365
+ req.rawBody = Buffer.from(JSON.stringify(req.body));
4366
+ }
4367
+
4343
4368
  const release = (() => {
4344
4369
  let released = false;
4345
4370
  return () => {
@@ -204,7 +204,8 @@ async function startProxyServer(options = {}) {
204
204
  channel: selectedChannel.name,
205
205
  channelId: selectedChannel.id,
206
206
  startTime: Date.now(),
207
- sessionId: req.sessionId || null
207
+ sessionId: req.sessionId || null,
208
+ requestModel: req.body?.model || ''
208
209
  });
209
210
 
210
211
  proxyReq.removeHeader('x-api-key');
@@ -300,6 +301,7 @@ async function startProxyServer(options = {}) {
300
301
  if (meta) {
301
302
  meta.originalModel = originalModel;
302
303
  meta.redirectedModel = redirectedModel;
304
+ meta.requestModel = redirectedModel;
303
305
  }
304
306
 
305
307
  // 只在重定向规则变化时打印日志(避免每次请求都打印)
@@ -120,6 +120,45 @@ function normalizeNumber(value, defaultValue, max = null) {
120
120
  return num;
121
121
  }
122
122
 
123
+ /**
124
+ * 判断请求路径是否为 OpenAI Chat Completions。
125
+ * @param {string} requestPath
126
+ * @returns {boolean}
127
+ */
128
+ function isChatCompletionsPath(requestPath = '') {
129
+ const normalized = String(requestPath || '').trim().toLowerCase();
130
+ return normalized.includes('/chat/completions');
131
+ }
132
+
133
+ /**
134
+ * 为流式 Chat Completions 请求补齐 usage 输出,方便统计真实 token。
135
+ * @param {object} body
136
+ * @returns {boolean} 是否发生了修改
137
+ */
138
+ function ensureOpenAiStreamUsage(body) {
139
+ if (!body || typeof body !== 'object' || Array.isArray(body)) {
140
+ return false;
141
+ }
142
+
143
+ if (body.stream !== true) {
144
+ return false;
145
+ }
146
+
147
+ const current = body.stream_options && typeof body.stream_options === 'object' && !Array.isArray(body.stream_options)
148
+ ? body.stream_options
149
+ : {};
150
+
151
+ if (current.include_usage === true) {
152
+ return false;
153
+ }
154
+
155
+ body.stream_options = {
156
+ ...current,
157
+ include_usage: true
158
+ };
159
+ return true;
160
+ }
161
+
123
162
  /**
124
163
  * 记录模型重定向日志(避免重复打印)
125
164
  * @param {Map} cache - 重定向缓存 Map
@@ -148,5 +187,7 @@ module.exports = {
148
187
  resolveTargetUrl,
149
188
  normalizeGatewaySourceType,
150
189
  normalizeNumber,
190
+ isChatCompletionsPath,
191
+ ensureOpenAiStreamUsage,
151
192
  logModelRedirect,
152
193
  };