coding-tool-x 3.5.10 → 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.
- package/CHANGELOG.md +14 -1
- package/README.md +11 -1
- package/dist/web/assets/{Analytics-DPb1zErn.js → Analytics-GuTri2mr.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-Bki2k4Lo.js → ConfigTemplates-DILbDhRj.js} +1 -1
- package/dist/web/assets/Home-Bm7mK2qz.css +1 -0
- package/dist/web/assets/Home-ri7cXeJp.js +1 -0
- package/dist/web/assets/{PluginManager-jgeQOE24.js → PluginManager-DQPeUFkr.js} +1 -1
- package/dist/web/assets/{ProjectList-CF67RzP_.js → ProjectList-Cpf82Lgb.js} +1 -1
- package/dist/web/assets/SessionList-D12muXvx.css +1 -0
- package/dist/web/assets/{SessionList-DhHsY5q9.js → SessionList-DXhPIwJI.js} +1 -1
- package/dist/web/assets/{SkillManager-CtEz5gGp.js → SkillManager-DNVyDlEv.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-D3B0Y5XE.js → WorkspaceManager-Cp0t7cjn.js} +1 -1
- package/dist/web/assets/icons-QFXzEgqM.js +1 -0
- package/dist/web/assets/index-C5wcoiDy.js +2 -0
- package/dist/web/assets/{index-BS7aplYp.css → index-DfikIh5S.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/codex-channels.js +4 -0
- package/src/server/api/codex-proxy.js +42 -1
- 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 +27 -8
- 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 +109 -58
- package/src/server/services/codex-sessions.js +13 -3
- package/src/server/services/codex-statistics-service.js +1 -0
- package/src/server/services/gemini-sessions.js +12 -4
- 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/sessions.js +13 -3
- 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-BKYHKSax.js +0 -1
- package/dist/web/assets/Home-BQxQ1LhR.css +0 -1
- package/dist/web/assets/SessionList-lz23U6ZJ.css +0 -1
- package/dist/web/assets/icons-lKCTySl7.js +0 -1
- package/dist/web/assets/index-BnyNm74G.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-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-
|
|
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-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.
|
|
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
|
},
|
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');
|
|
@@ -201,6 +201,10 @@ module.exports = (config) => {
|
|
|
201
201
|
res.json(channel);
|
|
202
202
|
broadcastSchedulerState('codex', getSchedulerState('codex'));
|
|
203
203
|
} catch (err) {
|
|
204
|
+
if (err?.message === 'Channel not found') {
|
|
205
|
+
console.warn(`[Codex Channels API] Ignored update for missing channel: ${req.params.channelId}`);
|
|
206
|
+
return res.status(404).json({ error: 'Channel not found' });
|
|
207
|
+
}
|
|
204
208
|
console.error('[Codex Channels API] Failed to update channel:', err);
|
|
205
209
|
res.status(500).json({ error: err.message });
|
|
206
210
|
}
|
|
@@ -61,6 +61,31 @@ function resolveActiveChannel(channels, activeChannelId = null) {
|
|
|
61
61
|
|| null;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
function recoverChannelAfterMissingUpdate(previousChannel) {
|
|
65
|
+
const { channels } = getChannels();
|
|
66
|
+
const allChannels = Array.isArray(channels) ? channels : [];
|
|
67
|
+
const enabledChannels = allChannels.filter(channel => channel.enabled !== false);
|
|
68
|
+
|
|
69
|
+
const previousProviderKey = String(previousChannel?.providerKey || '').trim();
|
|
70
|
+
if (previousProviderKey) {
|
|
71
|
+
const sameProvider = enabledChannels.find(channel => channel.providerKey === previousProviderKey);
|
|
72
|
+
if (sameProvider) {
|
|
73
|
+
return sameProvider;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (allChannels.length > 0) {
|
|
78
|
+
return resolveActiveChannel(allChannels, null);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const refreshedEnabledChannels = getEnabledChannels();
|
|
82
|
+
if (Array.isArray(refreshedEnabledChannels) && refreshedEnabledChannels.length > 0) {
|
|
83
|
+
return refreshedEnabledChannels[0];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
64
89
|
// 保存激活渠道ID
|
|
65
90
|
function saveActiveChannelId(channelId) {
|
|
66
91
|
ensureStorageDirMigrated();
|
|
@@ -149,7 +174,23 @@ router.post('/start', async (req, res) => {
|
|
|
149
174
|
});
|
|
150
175
|
}
|
|
151
176
|
|
|
152
|
-
|
|
177
|
+
try {
|
|
178
|
+
currentChannel = markChannelAsRecentlyUsed(currentChannel.id) || currentChannel;
|
|
179
|
+
} catch (error) {
|
|
180
|
+
if (error?.message !== 'Channel not found') {
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const recoveredChannel = recoverChannelAfterMissingUpdate(currentChannel);
|
|
185
|
+
if (!recoveredChannel) {
|
|
186
|
+
return res.status(400).json({
|
|
187
|
+
error: 'No enabled Codex channel found. Please create and enable a channel first.'
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
currentChannel = recoveredChannel;
|
|
192
|
+
console.warn(`[Codex Proxy] Active channel disappeared before startup, falling back to ${currentChannel.name} (${currentChannel.id})`);
|
|
193
|
+
}
|
|
153
194
|
|
|
154
195
|
// 3. 保存当前激活渠道ID(用于代理模式)
|
|
155
196
|
saveActiveChannelId(currentChannel.id);
|
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
|
|
|
@@ -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:
|
|
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:
|
|
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
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
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
|
-
|
|
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
|
};
|