coding-tool-x 3.5.11 → 3.5.12

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-GuTri2mr.js} +1 -1
  4. package/dist/web/assets/{ConfigTemplates-C6QRIeDj.js → ConfigTemplates-DILbDhRj.js} +1 -1
  5. package/dist/web/assets/Home-Bm7mK2qz.css +1 -0
  6. package/dist/web/assets/Home-ri7cXeJp.js +1 -0
  7. package/dist/web/assets/{PluginManager-Ce-mws6Q.js → PluginManager-DQPeUFkr.js} +1 -1
  8. package/dist/web/assets/{ProjectList-9Z7FFCIT.js → ProjectList-Cpf82Lgb.js} +1 -1
  9. package/dist/web/assets/{SessionList-CyETQcs8.js → SessionList-DXhPIwJI.js} +1 -1
  10. package/dist/web/assets/{SkillManager-DSUcP1se.js → SkillManager-DNVyDlEv.js} +1 -1
  11. package/dist/web/assets/{WorkspaceManager-Cvbc11bJ.js → WorkspaceManager-Cp0t7cjn.js} +1 -1
  12. package/dist/web/assets/icons-QFXzEgqM.js +1 -0
  13. package/dist/web/assets/index-C5wcoiDy.js +2 -0
  14. package/dist/web/assets/{index-BS7aplYp.css → index-DfikIh5S.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 +27 -8
  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 +109 -58
  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-C5wcoiDy.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-DfikIh5S.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.12",
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
 
@@ -1932,6 +1936,8 @@ function publishOpenCodeUsageLog({ requestId, channel, model, usage, startTime }
1932
1936
  tokens: {
1933
1937
  input: Number(usage?.input_tokens || usage?.prompt_tokens || 0),
1934
1938
  output: Number(usage?.output_tokens || usage?.completion_tokens || 0),
1939
+ cacheCreation: Number(usage?.cache_creation_input_tokens || 0),
1940
+ cacheRead: Number(usage?.cache_read_input_tokens || 0),
1935
1941
  cached: Number(usage?.input_tokens_details?.cached_tokens || 0),
1936
1942
  reasoning: Number(usage?.output_tokens_details?.reasoning_tokens || 0),
1937
1943
  total: Number(usage?.total_tokens || 0)
@@ -3064,6 +3070,7 @@ async function handleClaudeGatewayRequest(req, res, channel, effectiveKey) {
3064
3070
  }
3065
3071
 
3066
3072
  const chatResponseObject = buildOpenAiChatCompletionsObject(parsedBody, originalPayload.model);
3073
+ const loggingUsage = buildOpenAiResponsesObject(parsedBody, originalPayload.model).usage;
3067
3074
  if (wantsStream) {
3068
3075
  sendChatCompletionsSse(res, chatResponseObject);
3069
3076
  } else {
@@ -3073,7 +3080,7 @@ async function handleClaudeGatewayRequest(req, res, channel, effectiveKey) {
3073
3080
  requestId,
3074
3081
  channel,
3075
3082
  model: chatResponseObject.model,
3076
- usage: chatResponseObject.usage,
3083
+ usage: loggingUsage,
3077
3084
  startTime
3078
3085
  });
3079
3086
  recordSuccess(channel.id, 'opencode');
@@ -4114,6 +4121,7 @@ async function handleGeminiGatewayRequest(req, res, channel, effectiveKey) {
4114
4121
  }
4115
4122
 
4116
4123
  const chatResponseObject = buildOpenAiChatCompletionsObjectFromGemini(parsedBody, targetModel);
4124
+ const loggingUsage = buildOpenAiResponsesObjectFromGemini(parsedBody, targetModel).usage;
4117
4125
  if (wantsStream) {
4118
4126
  sendChatCompletionsSse(res, chatResponseObject);
4119
4127
  } else {
@@ -4123,7 +4131,7 @@ async function handleGeminiGatewayRequest(req, res, channel, effectiveKey) {
4123
4131
  requestId,
4124
4132
  channel,
4125
4133
  model: chatResponseObject.model,
4126
- usage: chatResponseObject.usage,
4134
+ usage: loggingUsage,
4127
4135
  startTime
4128
4136
  });
4129
4137
  recordSuccess(channel.id, 'opencode');
@@ -4224,9 +4232,10 @@ async function startOpenCodeProxyServer(options = {}) {
4224
4232
  const requestId = `opencode-${Date.now()}-${Math.random()}`;
4225
4233
  requestMetadata.set(req, {
4226
4234
  id: requestId,
4227
- channel: activeChannel.name,
4228
- channelId: activeChannel.id,
4229
- startTime: Date.now()
4235
+ channel: activeChannel.name,
4236
+ channelId: activeChannel.id,
4237
+ startTime: Date.now(),
4238
+ requestModel: req.body?.model || ''
4230
4239
  });
4231
4240
 
4232
4241
  proxyReq.removeHeader('authorization');
@@ -4313,6 +4322,8 @@ async function startOpenCodeProxyServer(options = {}) {
4313
4322
  }
4314
4323
  });
4315
4324
 
4325
+ let bodyMutated = false;
4326
+
4316
4327
  // 应用模型重定向(当 proxy 开启时)
4317
4328
  if (req.body && typeof req.body === 'object' && !Array.isArray(req.body) && req.body.model) {
4318
4329
  const originalModel = req.body.model;
@@ -4320,14 +4331,14 @@ async function startOpenCodeProxyServer(options = {}) {
4320
4331
 
4321
4332
  if (redirectedModel !== originalModel) {
4322
4333
  req.body.model = redirectedModel;
4323
- // 更新 rawBody 以匹配修改后的 body
4324
- req.rawBody = Buffer.from(JSON.stringify(req.body));
4334
+ bodyMutated = true;
4325
4335
 
4326
4336
  // 将原始模型和重定向模型存入 metadata,用于日志记录
4327
4337
  const meta = requestMetadata.get(req);
4328
4338
  if (meta) {
4329
4339
  meta.originalModel = originalModel;
4330
4340
  meta.redirectedModel = redirectedModel;
4341
+ meta.requestModel = redirectedModel;
4331
4342
  }
4332
4343
 
4333
4344
  // 只在重定向规则变化时打印日志(避免每次请求都打印)
@@ -4340,6 +4351,14 @@ async function startOpenCodeProxyServer(options = {}) {
4340
4351
  }
4341
4352
  }
4342
4353
 
4354
+ if (shouldParseJson(req) && isChatCompletionsPath(req.url) && ensureOpenAiStreamUsage(req.body)) {
4355
+ bodyMutated = true;
4356
+ }
4357
+
4358
+ if (bodyMutated) {
4359
+ req.rawBody = Buffer.from(JSON.stringify(req.body));
4360
+ }
4361
+
4343
4362
  const release = (() => {
4344
4363
  let released = false;
4345
4364
  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
  };