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.
- package/CHANGELOG.md +13 -0
- package/README.md +11 -1
- package/dist/web/assets/{Analytics-Bw-0r4Ys.js → Analytics-B9Shk-uq.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-C6QRIeDj.js → ConfigTemplates-Dlkn-_zW.js} +1 -1
- package/dist/web/assets/Home-AGf32LvJ.js +1 -0
- package/dist/web/assets/Home-BeGkC4bb.css +1 -0
- package/dist/web/assets/{PluginManager-Ce-mws6Q.js → PluginManager-BnAjHN4u.js} +1 -1
- package/dist/web/assets/{ProjectList-9Z7FFCIT.js → ProjectList-7YV_zk9L.js} +1 -1
- package/dist/web/assets/{SessionList-CyETQcs8.js → SessionList-D0un-UKS.js} +1 -1
- package/dist/web/assets/{SkillManager-DSUcP1se.js → SkillManager-DhYBmbtd.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-Cvbc11bJ.js → WorkspaceManager-JOZIPAGo.js} +1 -1
- package/dist/web/assets/icons-QFXzEgqM.js +1 -0
- package/dist/web/assets/index-DM3yF1Oo.js +2 -0
- package/dist/web/assets/{index-BS7aplYp.css → index-dzKgnRhx.css} +1 -1
- package/dist/web/index.html +4 -4
- package/package.json +2 -1
- package/src/commands/daemon.js +6 -2
- package/src/commands/ui.js +8 -3
- package/src/index.js +2 -0
- package/src/server/api/hooks.js +18 -1
- package/src/server/codex-proxy-server.js +22 -6
- package/src/server/gemini-proxy-server.js +3 -1
- package/src/server/index.js +37 -5
- package/src/server/opencode-proxy-server.js +39 -14
- package/src/server/proxy-server.js +3 -1
- package/src/server/services/base/proxy-utils.js +41 -0
- package/src/server/services/base/response-usage-parser.js +137 -59
- package/src/server/services/codex-statistics-service.js +1 -0
- package/src/server/services/gemini-statistics-service.js +1 -0
- package/src/server/services/https-cert.js +202 -0
- package/src/server/services/notification-hooks.js +138 -6
- package/src/server/services/opencode-gateway-converter.js +3 -0
- package/src/server/services/opencode-statistics-service.js +1 -0
- package/src/server/services/proxy-log-helper.js +44 -12
- package/src/server/services/statistics-service.js +73 -28
- package/src/server/services/web-build.js +172 -0
- package/src/server/websocket-server.js +18 -1
- package/dist/web/assets/Home-BQxQ1LhR.css +0 -1
- package/dist/web/assets/Home-B_Z9ythw.js +0 -1
- package/dist/web/assets/icons-lKCTySl7.js +0 -1
- package/dist/web/assets/index-CTeg1xG-.js +0 -2
package/dist/web/index.html
CHANGED
|
@@ -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>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
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-
|
|
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-
|
|
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.
|
|
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
|
},
|
package/src/commands/daemon.js
CHANGED
|
@@ -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常用命令:'));
|
package/src/commands/ui.js
CHANGED
|
@@ -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 =
|
|
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');
|
package/src/server/api/hooks.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
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
|
// 只在重定向规则变化时打印日志(避免每次请求都打印)
|
package/src/server/index.js
CHANGED
|
@@ -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
|
-
|
|
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(`
|
|
276
|
-
console.log(chalk.gray(`
|
|
303
|
+
console.log(` ${protocol}://localhost:${port}`);
|
|
304
|
+
console.log(chalk.gray(` ${protocol}://<your-ip>:${port} (LAN 访问)`));
|
|
277
305
|
} else {
|
|
278
|
-
console.log(`
|
|
306
|
+
console.log(` ${protocol}://localhost:${port}`);
|
|
279
307
|
}
|
|
280
308
|
|
|
281
309
|
// 附加 WebSocket 服务器到同一个端口
|
|
282
310
|
attachWebSocketServer(server, { host });
|
|
283
|
-
console.log(`
|
|
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 {
|
|
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(
|
|
1934
|
-
output: Number(
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
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
|
-
|
|
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
|
};
|