coding-tool-x 3.4.3 → 3.4.5
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/dist/web/assets/{Analytics-CbGxotgz.js → Analytics-DFWyPf5C.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-oP6nrFEb.js → ConfigTemplates-BFE7hmKd.js} +1 -1
- package/dist/web/assets/{Home-DMntmEvh.js → Home-DZUuCrxk.js} +1 -1
- package/dist/web/assets/{PluginManager-BUC_c7nH.js → PluginManager-WyGY2BQN.js} +1 -1
- package/dist/web/assets/{ProjectList-CW8J49n7.js → ProjectList-CBc0QawN.js} +1 -1
- package/dist/web/assets/{ProjectList-oJIyIRkP.css → ProjectList-DL4JK6ci.css} +1 -1
- package/dist/web/assets/{SessionList-7lYnF92v.js → SessionList-CdPR7QLq.js} +1 -1
- package/dist/web/assets/{SkillManager-Cs08216i.js → SkillManager-B5-DxQOS.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-CY-oGtyB.js → WorkspaceManager-C7yqFjpi.js} +1 -1
- package/dist/web/assets/index-BDsmoSfO.js +2 -0
- package/dist/web/assets/{index-5qy5NMIP.css → index-C1pzEgmj.css} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +2 -2
- package/src/commands/channels.js +13 -13
- package/src/commands/cli-type.js +5 -5
- package/src/commands/daemon.js +31 -31
- package/src/commands/doctor.js +14 -14
- package/src/commands/export-config.js +23 -23
- package/src/commands/list.js +4 -4
- package/src/commands/logs.js +19 -19
- package/src/commands/plugin.js +62 -62
- package/src/commands/port-config.js +4 -4
- package/src/commands/proxy-control.js +35 -35
- package/src/commands/proxy.js +28 -28
- package/src/commands/resume.js +4 -4
- package/src/commands/search.js +9 -9
- package/src/commands/security.js +5 -5
- package/src/commands/stats.js +18 -18
- package/src/commands/switch.js +1 -1
- package/src/commands/toggle-proxy.js +18 -18
- package/src/commands/ui.js +11 -11
- package/src/commands/update.js +9 -9
- package/src/commands/workspace.js +11 -11
- package/src/index.js +24 -24
- package/src/plugins/plugin-installer.js +1 -1
- package/src/reset-config.js +9 -9
- package/src/server/api/channels.js +1 -1
- package/src/server/api/claude-hooks.js +3 -2
- package/src/server/api/plugins.js +165 -14
- package/src/server/api/pm2-autostart.js +2 -2
- package/src/server/api/proxy.js +6 -6
- package/src/server/api/skills.js +66 -7
- package/src/server/codex-proxy-server.js +10 -2
- package/src/server/dev-server.js +2 -2
- package/src/server/gemini-proxy-server.js +10 -2
- package/src/server/index.js +37 -37
- package/src/server/opencode-proxy-server.js +10 -2
- package/src/server/proxy-server.js +14 -6
- package/src/server/services/codex-channels.js +64 -21
- package/src/server/services/codex-env-manager.js +44 -28
- package/src/server/services/config-export-service.js +1 -1
- package/src/server/services/mcp-service.js +2 -1
- package/src/server/services/model-detector.js +2 -2
- package/src/server/services/native-keychain.js +1 -0
- package/src/server/services/plugins-service.js +1066 -261
- package/src/server/services/proxy-runtime.js +129 -5
- package/src/server/services/server-shutdown.js +79 -0
- package/src/server/services/settings-manager.js +3 -3
- package/src/server/services/skill-service.js +146 -29
- package/src/server/websocket-server.js +8 -8
- package/src/ui/menu.js +2 -2
- package/src/ui/prompts.js +5 -5
- package/dist/web/assets/index-ClCqKpvX.js +0 -2
package/src/server/index.js
CHANGED
|
@@ -36,7 +36,7 @@ function isInteractivePortConflictMode(options = {}) {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
function printPortConflictHelp(port) {
|
|
39
|
-
console.log(chalk.yellow('\n
|
|
39
|
+
console.log(chalk.yellow('\n[TIP] 解决方案:'));
|
|
40
40
|
console.log(chalk.gray(' 1. 运行 ctx 命令,选择"配置端口"修改端口'));
|
|
41
41
|
console.log(chalk.gray(` 2. 或手动关闭占用端口 ${port} 的程序\n`));
|
|
42
42
|
}
|
|
@@ -47,7 +47,7 @@ function printPortToolIssue(issue = getPortToolIssue()) {
|
|
|
47
47
|
return;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
console.error(chalk.yellow(`\n
|
|
50
|
+
console.error(chalk.yellow(`\n[TIP] ${lines[0]}`));
|
|
51
51
|
lines.slice(1).forEach((line) => {
|
|
52
52
|
console.error(chalk.gray(` ${line}`));
|
|
53
53
|
});
|
|
@@ -64,7 +64,7 @@ async function startServer(port, host = '127.0.0.1', options = {}) {
|
|
|
64
64
|
// 检查端口是否被占用
|
|
65
65
|
const portInUse = await isPortInUse(port, host);
|
|
66
66
|
if (portInUse) {
|
|
67
|
-
console.log(chalk.yellow(`\n
|
|
67
|
+
console.log(chalk.yellow(`\n[WARN] 端口 ${port} 已被占用\n`));
|
|
68
68
|
|
|
69
69
|
const interactiveMode = isInteractivePortConflictMode(options);
|
|
70
70
|
let shouldKill = false;
|
|
@@ -87,7 +87,7 @@ async function startServer(port, host = '127.0.0.1', options = {}) {
|
|
|
87
87
|
]);
|
|
88
88
|
shouldKill = answer.shouldKill;
|
|
89
89
|
} else {
|
|
90
|
-
console.error(chalk.red('
|
|
90
|
+
console.error(chalk.red('[ERROR] 当前为非交互模式,无法确认端口清理操作,已取消启动。'));
|
|
91
91
|
printPortConflictHelp(port);
|
|
92
92
|
process.exit(1);
|
|
93
93
|
}
|
|
@@ -107,9 +107,9 @@ async function startServer(port, host = '127.0.0.1', options = {}) {
|
|
|
107
107
|
if (toolIssue) {
|
|
108
108
|
printPortToolIssue(toolIssue);
|
|
109
109
|
} else {
|
|
110
|
-
console.error(chalk.red('\n
|
|
110
|
+
console.error(chalk.red('\n[ERROR] 无法关闭占用端口的进程'));
|
|
111
111
|
}
|
|
112
|
-
console.error(chalk.yellow('\n
|
|
112
|
+
console.error(chalk.yellow('\n[TIP] 请手动关闭占用端口的程序,或使用其他端口\n'));
|
|
113
113
|
process.exit(1);
|
|
114
114
|
}
|
|
115
115
|
|
|
@@ -118,12 +118,12 @@ async function startServer(port, host = '127.0.0.1', options = {}) {
|
|
|
118
118
|
const released = await waitForPortRelease(port, 3000, host);
|
|
119
119
|
|
|
120
120
|
if (!released) {
|
|
121
|
-
console.error(chalk.red('\n
|
|
122
|
-
console.error(chalk.yellow('\n
|
|
121
|
+
console.error(chalk.red('\n[ERROR] 端口释放超时'));
|
|
122
|
+
console.error(chalk.yellow('\n[TIP] 请稍后重试,或手动检查端口占用情况\n'));
|
|
123
123
|
process.exit(1);
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
console.log(chalk.green('
|
|
126
|
+
console.log(chalk.green('[v] 端口已释放\n'));
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
const app = express();
|
|
@@ -249,12 +249,12 @@ async function startServer(port, host = '127.0.0.1', options = {}) {
|
|
|
249
249
|
const onError = (err) => {
|
|
250
250
|
server.off('listening', onListening);
|
|
251
251
|
if (err.code === 'EADDRINUSE') {
|
|
252
|
-
console.error(chalk.red(`\n
|
|
253
|
-
console.error(chalk.yellow('\n
|
|
252
|
+
console.error(chalk.red(`\n[ERROR] 端口 ${port} 已被占用`));
|
|
253
|
+
console.error(chalk.yellow('\n[TIP] 解决方案:'));
|
|
254
254
|
console.error(chalk.gray(' 1. 运行 ctx 命令,选择"配置端口"修改端口'));
|
|
255
255
|
console.error(chalk.gray(` 2. 或关闭占用端口 ${port} 的程序\n`));
|
|
256
256
|
} else {
|
|
257
|
-
console.error(chalk.red(`\n
|
|
257
|
+
console.error(chalk.red(`\n[ERROR] 启动服务器失败: ${err.message}\n`));
|
|
258
258
|
}
|
|
259
259
|
process.exit(1);
|
|
260
260
|
};
|
|
@@ -263,9 +263,9 @@ async function startServer(port, host = '127.0.0.1', options = {}) {
|
|
|
263
263
|
server.once('error', onError);
|
|
264
264
|
});
|
|
265
265
|
|
|
266
|
-
console.log(`\n
|
|
266
|
+
console.log(`\n[START] Coding-Tool Web UI running at:`);
|
|
267
267
|
if (host === '0.0.0.0') {
|
|
268
|
-
console.log(chalk.yellow(`
|
|
268
|
+
console.log(chalk.yellow(` [WARN] 警告: 服务正在监听所有网络接口 (LAN 可访问)`));
|
|
269
269
|
console.log(` http://localhost:${port}`);
|
|
270
270
|
console.log(chalk.gray(` http://<your-ip>:${port} (LAN 访问)`));
|
|
271
271
|
} else {
|
|
@@ -277,7 +277,7 @@ async function startServer(port, host = '127.0.0.1', options = {}) {
|
|
|
277
277
|
console.log(` ws://localhost:${port}/ws\n`);
|
|
278
278
|
|
|
279
279
|
if (host === '0.0.0.0' && !allowRemoteMutation) {
|
|
280
|
-
console.log(chalk.yellow('
|
|
280
|
+
console.log(chalk.yellow(' [LOCK] 已启用 LAN 安全保护:远程写操作默认禁用'));
|
|
281
281
|
}
|
|
282
282
|
// 自动恢复代理状态
|
|
283
283
|
autoRestoreProxies();
|
|
@@ -296,26 +296,26 @@ function autoRestoreProxies() {
|
|
|
296
296
|
// 检查 Claude 代理状态文件
|
|
297
297
|
const claudeActiveFile = PATHS.activeChannel.claude;
|
|
298
298
|
if (fs.existsSync(claudeActiveFile)) {
|
|
299
|
-
console.log(chalk.cyan('\n
|
|
299
|
+
console.log(chalk.cyan('\n[SYNC] 检测到 Claude 代理状态文件,正在自动启动...'));
|
|
300
300
|
const proxyPort = config.ports?.proxy || 20088;
|
|
301
301
|
startProxyServer(proxyPort)
|
|
302
302
|
.then(() => {
|
|
303
|
-
console.log(chalk.green(
|
|
303
|
+
console.log(chalk.green(`[OK] Claude 代理已自动启动,端口: ${proxyPort}`));
|
|
304
304
|
})
|
|
305
305
|
.catch((err) => {
|
|
306
|
-
console.error(chalk.red(
|
|
306
|
+
console.error(chalk.red(`[ERROR] Claude 代理启动失败: ${err.message}`));
|
|
307
307
|
});
|
|
308
308
|
}
|
|
309
309
|
|
|
310
310
|
// 检查 Codex 代理状态文件
|
|
311
311
|
const codexActiveFile = PATHS.activeChannel.codex;
|
|
312
312
|
if (fs.existsSync(codexActiveFile)) {
|
|
313
|
-
console.log(chalk.cyan('\n
|
|
313
|
+
console.log(chalk.cyan('\n[SYNC] 检测到 Codex 代理状态文件,正在自动启动...'));
|
|
314
314
|
const codexProxyPort = config.ports?.codexProxy || 20089;
|
|
315
315
|
startCodexProxyServer(codexProxyPort)
|
|
316
316
|
.then((result) => {
|
|
317
317
|
const port = result?.port || codexProxyPort;
|
|
318
|
-
console.log(chalk.green(
|
|
318
|
+
console.log(chalk.green(`[OK] Codex 代理已自动启动,端口: ${port}`));
|
|
319
319
|
|
|
320
320
|
// 重启后重新写入 cc-proxy 配置与环境变量,避免缺少 provider/env 导致报错
|
|
321
321
|
try {
|
|
@@ -324,43 +324,43 @@ function autoRestoreProxies() {
|
|
|
324
324
|
console.log(chalk.gray(' 已同步 codex config.toml 与 CC_PROXY_KEY'));
|
|
325
325
|
}
|
|
326
326
|
} catch (err) {
|
|
327
|
-
console.error(chalk.red(
|
|
327
|
+
console.error(chalk.red(`[ERROR] Codex 代理配置同步失败: ${err.message}`));
|
|
328
328
|
}
|
|
329
329
|
})
|
|
330
330
|
.catch((err) => {
|
|
331
|
-
console.error(chalk.red(
|
|
331
|
+
console.error(chalk.red(`[ERROR] Codex 代理启动失败: ${err.message}`));
|
|
332
332
|
});
|
|
333
333
|
}
|
|
334
334
|
|
|
335
335
|
// 检查 Gemini 代理状态文件
|
|
336
336
|
const geminiActiveFile = PATHS.activeChannel.gemini;
|
|
337
337
|
if (fs.existsSync(geminiActiveFile)) {
|
|
338
|
-
console.log(chalk.cyan('\n
|
|
338
|
+
console.log(chalk.cyan('\n[SYNC] 检测到 Gemini 代理状态文件,正在自动启动...'));
|
|
339
339
|
const geminiProxyPort = config.ports?.geminiProxy || 20090;
|
|
340
340
|
startGeminiProxyServer(geminiProxyPort)
|
|
341
341
|
.then((result) => {
|
|
342
342
|
if (result.success) {
|
|
343
|
-
console.log(chalk.green(
|
|
343
|
+
console.log(chalk.green(`[OK] Gemini 代理已自动启动,端口: ${result.port}`));
|
|
344
344
|
} else {
|
|
345
|
-
console.error(chalk.red(
|
|
345
|
+
console.error(chalk.red(`[ERROR] Gemini 代理启动失败: ${result.error || 'Unknown error'}`));
|
|
346
346
|
}
|
|
347
347
|
})
|
|
348
348
|
.catch((err) => {
|
|
349
|
-
console.error(chalk.red(
|
|
349
|
+
console.error(chalk.red(`[ERROR] Gemini 代理启动失败: ${err.message}`));
|
|
350
350
|
});
|
|
351
351
|
} else {
|
|
352
|
-
console.log(chalk.gray('\n
|
|
352
|
+
console.log(chalk.gray('\n[TIP] 提示: 如需使用 Gemini 代理,请在前端界面激活 Gemini 渠道'));
|
|
353
353
|
}
|
|
354
354
|
|
|
355
355
|
// 检查 OpenCode 代理状态文件
|
|
356
356
|
const opencodeActiveFile = PATHS.activeChannel.opencode;
|
|
357
357
|
if (fs.existsSync(opencodeActiveFile)) {
|
|
358
|
-
console.log(chalk.cyan('\n
|
|
358
|
+
console.log(chalk.cyan('\n[SYNC] 检测到 OpenCode 代理状态文件,正在自动启动...'));
|
|
359
359
|
const opencodeProxyPort = config.ports?.opencodeProxy || 20091;
|
|
360
360
|
startOpenCodeProxyServer(opencodeProxyPort)
|
|
361
361
|
.then(async (result) => {
|
|
362
362
|
if (result.success) {
|
|
363
|
-
console.log(chalk.green(
|
|
363
|
+
console.log(chalk.green(`[OK] OpenCode 代理已自动启动,端口: ${result.port}`));
|
|
364
364
|
try {
|
|
365
365
|
const { getEnabledChannels: getEnabledOpenCodeChannels } = require('./services/opencode-channels');
|
|
366
366
|
const enabledChs = getEnabledOpenCodeChannels();
|
|
@@ -390,14 +390,14 @@ function autoRestoreProxies() {
|
|
|
390
390
|
console.log(chalk.gray(' 已同步 OpenCode 配置文件'));
|
|
391
391
|
}
|
|
392
392
|
} catch (err) {
|
|
393
|
-
console.error(chalk.red(
|
|
393
|
+
console.error(chalk.red(`[ERROR] OpenCode 代理配置同步失败: ${err.message}`));
|
|
394
394
|
}
|
|
395
395
|
} else {
|
|
396
|
-
console.error(chalk.red(
|
|
396
|
+
console.error(chalk.red(`[ERROR] OpenCode 代理启动失败: ${result.error || 'Unknown error'}`));
|
|
397
397
|
}
|
|
398
398
|
})
|
|
399
399
|
.catch((err) => {
|
|
400
|
-
console.error(chalk.red(
|
|
400
|
+
console.error(chalk.red(`[ERROR] OpenCode 代理启动失败: ${err.message}`));
|
|
401
401
|
});
|
|
402
402
|
}
|
|
403
403
|
}
|
|
@@ -408,7 +408,7 @@ async function performStartupHealthCheck() {
|
|
|
408
408
|
const { getProjects } = require('./services/sessions');
|
|
409
409
|
|
|
410
410
|
try {
|
|
411
|
-
console.log(chalk.cyan('\n
|
|
411
|
+
console.log(chalk.cyan('\n[SEARCH] 正在进行启动健康检查...'));
|
|
412
412
|
|
|
413
413
|
// 获取所有项目
|
|
414
414
|
const config = loadConfig();
|
|
@@ -423,20 +423,20 @@ async function performStartupHealthCheck() {
|
|
|
423
423
|
const healthResult = healthCheckAllProjects(projects);
|
|
424
424
|
|
|
425
425
|
if (healthResult.summary.created > 0) {
|
|
426
|
-
console.log(chalk.green(`
|
|
426
|
+
console.log(chalk.green(` [v] 已为 ${healthResult.summary.created} 个项目创建 .claude/sessions 目录`));
|
|
427
427
|
}
|
|
428
428
|
|
|
429
429
|
if (healthResult.summary.errors > 0) {
|
|
430
|
-
console.log(chalk.yellow(`
|
|
430
|
+
console.log(chalk.yellow(` [!] ${healthResult.summary.errors} 个项目检查失败`));
|
|
431
431
|
}
|
|
432
432
|
|
|
433
433
|
if (healthResult.summary.created === 0 && healthResult.summary.errors === 0) {
|
|
434
|
-
console.log(chalk.green(`
|
|
434
|
+
console.log(chalk.green(` [v] 所有 ${healthResult.summary.healthy} 个项目状态正常`));
|
|
435
435
|
}
|
|
436
436
|
|
|
437
437
|
console.log('');
|
|
438
438
|
} catch (err) {
|
|
439
|
-
console.error(chalk.red('
|
|
439
|
+
console.error(chalk.red(' [x] 健康检查失败:'), err.message);
|
|
440
440
|
}
|
|
441
441
|
}
|
|
442
442
|
|
|
@@ -22,6 +22,7 @@ const { persistProxyRequestSnapshot, loadClaudeRequestTemplate } = require('./se
|
|
|
22
22
|
const { probeModelAvailability, fetchModelsFromProvider } = require('./services/model-detector');
|
|
23
23
|
const { publishUsageLog, publishFailureLog } = require('./services/proxy-log-helper');
|
|
24
24
|
const { redirectModel, resolveTargetUrl } = require('./services/base/proxy-utils');
|
|
25
|
+
const { attachServerShutdownHandling, expediteServerShutdown } = require('./services/server-shutdown');
|
|
25
26
|
|
|
26
27
|
let proxyServer = null;
|
|
27
28
|
let proxyApp = null;
|
|
@@ -4650,6 +4651,7 @@ async function startOpenCodeProxyServer(options = {}) {
|
|
|
4650
4651
|
|
|
4651
4652
|
// 启动服务器
|
|
4652
4653
|
proxyServer = http.createServer(proxyApp);
|
|
4654
|
+
attachServerShutdownHandling(proxyServer);
|
|
4653
4655
|
|
|
4654
4656
|
return new Promise((resolve, reject) => {
|
|
4655
4657
|
proxyServer.listen(port, '127.0.0.1', () => {
|
|
@@ -4692,8 +4694,13 @@ async function stopOpenCodeProxyServer(options = {}) {
|
|
|
4692
4694
|
|
|
4693
4695
|
requestMetadata.clear();
|
|
4694
4696
|
|
|
4697
|
+
const shutdownTimer = expediteServerShutdown(proxyServer);
|
|
4698
|
+
|
|
4695
4699
|
return new Promise((resolve) => {
|
|
4696
4700
|
proxyServer.close(() => {
|
|
4701
|
+
if (shutdownTimer) {
|
|
4702
|
+
clearTimeout(shutdownTimer);
|
|
4703
|
+
}
|
|
4697
4704
|
console.log('OpenCode proxy server stopped');
|
|
4698
4705
|
|
|
4699
4706
|
// 清除代理启动时间(仅当明确要求时)
|
|
@@ -4713,8 +4720,9 @@ async function stopOpenCodeProxyServer(options = {}) {
|
|
|
4713
4720
|
// 获取代理服务器状态
|
|
4714
4721
|
function getOpenCodeProxyStatus() {
|
|
4715
4722
|
const config = loadConfig();
|
|
4716
|
-
const
|
|
4717
|
-
const
|
|
4723
|
+
const allowRecovery = !!proxyServer;
|
|
4724
|
+
const startTime = getProxyStartTime('opencode', { allowRecovery });
|
|
4725
|
+
const runtime = getProxyRuntime('opencode', { allowRecovery });
|
|
4718
4726
|
|
|
4719
4727
|
return {
|
|
4720
4728
|
running: !!proxyServer,
|
|
@@ -20,6 +20,7 @@ const { getEffectiveApiKey } = require('./services/channels');
|
|
|
20
20
|
const { persistProxyRequestSnapshot, persistClaudeRequestTemplate } = require('./services/request-logger');
|
|
21
21
|
const { publishUsageLog, publishFailureLog } = require('./services/proxy-log-helper');
|
|
22
22
|
const { redirectModel } = require('./services/base/proxy-utils');
|
|
23
|
+
const { attachServerShutdownHandling, expediteServerShutdown } = require('./services/server-shutdown');
|
|
23
24
|
|
|
24
25
|
let proxyServer = null;
|
|
25
26
|
let proxyApp = null;
|
|
@@ -541,10 +542,11 @@ async function startProxyServer(options = {}) {
|
|
|
541
542
|
});
|
|
542
543
|
|
|
543
544
|
proxyServer = http.createServer(proxyApp);
|
|
545
|
+
attachServerShutdownHandling(proxyServer);
|
|
544
546
|
|
|
545
547
|
return new Promise((resolve, reject) => {
|
|
546
548
|
proxyServer.listen(port, '127.0.0.1', () => {
|
|
547
|
-
console.log(
|
|
549
|
+
console.log(`[OK] Proxy server started on http://127.0.0.1:${port}`);
|
|
548
550
|
saveProxyStartTime('claude', preserveStartTime);
|
|
549
551
|
eventBus.emitSync('proxy:start', { channel: 'claude', port });
|
|
550
552
|
resolve({ success: true, port });
|
|
@@ -552,8 +554,8 @@ async function startProxyServer(options = {}) {
|
|
|
552
554
|
|
|
553
555
|
proxyServer.on('error', (err) => {
|
|
554
556
|
if (err.code === 'EADDRINUSE') {
|
|
555
|
-
console.error(chalk.red(`\n
|
|
556
|
-
console.error(chalk.yellow('\n
|
|
557
|
+
console.error(chalk.red(`\n[ERROR] 代理服务端口 ${port} 已被占用`));
|
|
558
|
+
console.error(chalk.yellow('\n[TIP] 解决方案:'));
|
|
557
559
|
console.error(chalk.gray(' 1. 运行 ctx 命令,选择"配置端口"修改端口'));
|
|
558
560
|
console.error(chalk.gray(` 2. 或关闭占用端口 ${port} 的程序\n`));
|
|
559
561
|
} else {
|
|
@@ -580,9 +582,14 @@ async function stopProxyServer(options = {}) {
|
|
|
580
582
|
|
|
581
583
|
requestMetadata.clear();
|
|
582
584
|
|
|
585
|
+
const shutdownTimer = expediteServerShutdown(proxyServer);
|
|
586
|
+
|
|
583
587
|
return new Promise((resolve) => {
|
|
584
588
|
proxyServer.close(() => {
|
|
585
|
-
|
|
589
|
+
if (shutdownTimer) {
|
|
590
|
+
clearTimeout(shutdownTimer);
|
|
591
|
+
}
|
|
592
|
+
console.log('[OK] Proxy server stopped');
|
|
586
593
|
if (clearStartTime) {
|
|
587
594
|
clearProxyStartTime('claude');
|
|
588
595
|
}
|
|
@@ -599,8 +606,9 @@ async function stopProxyServer(options = {}) {
|
|
|
599
606
|
// 获取代理服务器状态
|
|
600
607
|
function getProxyStatus() {
|
|
601
608
|
const config = loadConfig();
|
|
602
|
-
const
|
|
603
|
-
const
|
|
609
|
+
const allowRecovery = !!proxyServer;
|
|
610
|
+
const startTime = getProxyStartTime('claude', { allowRecovery });
|
|
611
|
+
const runtime = getProxyRuntime('claude', { allowRecovery });
|
|
604
612
|
|
|
605
613
|
return {
|
|
606
614
|
running: !!proxyServer,
|
|
@@ -5,27 +5,46 @@ const toml = require('toml');
|
|
|
5
5
|
const tomlStringify = require('@iarna/toml').stringify;
|
|
6
6
|
const { PATHS } = require('../../config/paths');
|
|
7
7
|
const { getCodexDir } = require('./codex-config');
|
|
8
|
-
const { isProxyConfig } = require('./codex-settings-manager');
|
|
8
|
+
const { isProxyConfig, readConfig } = require('./codex-settings-manager');
|
|
9
9
|
const { clearNativeOAuth } = require('./native-oauth-adapters');
|
|
10
10
|
const { syncCodexUserEnvironment } = require('./codex-env-manager');
|
|
11
11
|
const BaseChannelService = require('./base/base-channel-service');
|
|
12
12
|
|
|
13
|
-
const
|
|
13
|
+
const CODEX_MANAGED_ENV_KEY = 'CC_PROXY_KEY';
|
|
14
14
|
const CODEX_PROXY_ENV_VALUE = 'PROXY_KEY';
|
|
15
15
|
|
|
16
16
|
// ── Codex 特有工具函数 ──
|
|
17
17
|
|
|
18
|
-
function
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
function resolveCurrentManagedChannel(channels = []) {
|
|
19
|
+
const allChannels = Array.isArray(channels) ? channels : [];
|
|
20
|
+
let currentProvider = '';
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
currentProvider = String(readConfig()?.model_provider || '').trim();
|
|
24
|
+
} catch (err) {
|
|
25
|
+
currentProvider = '';
|
|
21
26
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
|
|
28
|
+
if (currentProvider && currentProvider !== 'cc-proxy') {
|
|
29
|
+
const matched = allChannels.find(ch => ch.providerKey === currentProvider);
|
|
30
|
+
if (matched) {
|
|
31
|
+
return matched;
|
|
26
32
|
}
|
|
27
33
|
}
|
|
28
|
-
|
|
34
|
+
|
|
35
|
+
return allChannels.find(ch => ch.enabled !== false) || null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function buildManagedCodexEnvMap(channels = [], { includeProxyKey = false, activeChannel = null } = {}) {
|
|
39
|
+
if (includeProxyKey) {
|
|
40
|
+
return { [CODEX_MANAGED_ENV_KEY]: CODEX_PROXY_ENV_VALUE };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const targetChannel = activeChannel || resolveCurrentManagedChannel(channels);
|
|
44
|
+
if (targetChannel?.apiKey) {
|
|
45
|
+
return { [CODEX_MANAGED_ENV_KEY]: targetChannel.apiKey };
|
|
46
|
+
}
|
|
47
|
+
return {};
|
|
29
48
|
}
|
|
30
49
|
|
|
31
50
|
function syncAllChannelEnvVars() {
|
|
@@ -34,7 +53,8 @@ function syncAllChannelEnvVars() {
|
|
|
34
53
|
const data = svc.loadChannels();
|
|
35
54
|
const proxyRunning = isProxyConfig();
|
|
36
55
|
const envMap = buildManagedCodexEnvMap(data.channels, {
|
|
37
|
-
includeProxyKey: proxyRunning
|
|
56
|
+
includeProxyKey: proxyRunning,
|
|
57
|
+
activeChannel: proxyRunning ? null : resolveCurrentManagedChannel(data.channels)
|
|
38
58
|
});
|
|
39
59
|
syncCodexUserEnvironment(envMap, { replace: true });
|
|
40
60
|
} catch (err) {
|
|
@@ -87,7 +107,7 @@ function writeCodexConfigForMultiChannel(channels) {
|
|
|
87
107
|
name: ch.name,
|
|
88
108
|
base_url: ch.baseUrl,
|
|
89
109
|
wire_api: ch.wireApi || 'responses',
|
|
90
|
-
env_key:
|
|
110
|
+
env_key: CODEX_MANAGED_ENV_KEY,
|
|
91
111
|
requires_openai_auth: ch.requiresOpenaiAuth !== false
|
|
92
112
|
};
|
|
93
113
|
if (ch.queryParams && Object.keys(ch.queryParams).length > 0) {
|
|
@@ -121,7 +141,7 @@ class CodexChannelService extends BaseChannelService {
|
|
|
121
141
|
_applyDefaults(channel) {
|
|
122
142
|
const ch = super._applyDefaults(channel);
|
|
123
143
|
ch.providerKey = ch.providerKey || '';
|
|
124
|
-
ch.envKey =
|
|
144
|
+
ch.envKey = CODEX_MANAGED_ENV_KEY;
|
|
125
145
|
ch.wireApi = ch.wireApi || 'responses';
|
|
126
146
|
ch.model = ch.model || '';
|
|
127
147
|
ch.speedTestModel = ch.speedTestModel || null;
|
|
@@ -143,19 +163,38 @@ class CodexChannelService extends BaseChannelService {
|
|
|
143
163
|
}
|
|
144
164
|
|
|
145
165
|
_onAfterCreate(_channel, _allChannels) {
|
|
166
|
+
if (_channel.enabled !== false && !isProxyConfig()) {
|
|
167
|
+
this._applyToNativeSettings(_channel);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
146
170
|
syncAllChannelEnvVars();
|
|
147
|
-
// 注意:不再自动写入 config.toml,只在开启代理控制时才同步
|
|
148
171
|
}
|
|
149
172
|
|
|
150
|
-
_onAfterUpdate(_old, _next,
|
|
173
|
+
_onAfterUpdate(_old, _next, allChannels) {
|
|
174
|
+
if (!isProxyConfig()) {
|
|
175
|
+
if (_old.enabled === false && _next.enabled !== false) {
|
|
176
|
+
this._applyToNativeSettings(_next);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const activeChannel = resolveCurrentManagedChannel(allChannels);
|
|
180
|
+
if (_next.enabled !== false && activeChannel?.id === _next.id) {
|
|
181
|
+
this._applyToNativeSettings(_next);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
151
185
|
syncAllChannelEnvVars();
|
|
152
|
-
// 注意:不再自动写入 config.toml,只在开启代理控制时才同步
|
|
153
186
|
}
|
|
154
187
|
|
|
155
|
-
_onAfterDelete(_channel,
|
|
188
|
+
_onAfterDelete(_channel, allChannels) {
|
|
189
|
+
if (!isProxyConfig()) {
|
|
190
|
+
const activeChannel = resolveCurrentManagedChannel(allChannels);
|
|
191
|
+
if (activeChannel && activeChannel.enabled !== false) {
|
|
192
|
+
this._applyToNativeSettings(activeChannel);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
156
196
|
clearNativeOAuth('codex');
|
|
157
197
|
syncAllChannelEnvVars();
|
|
158
|
-
// 注意:不再自动写入 config.toml,只在开启代理控制时才同步
|
|
159
198
|
}
|
|
160
199
|
|
|
161
200
|
_applyToNativeSettings(channel) {
|
|
@@ -184,7 +223,7 @@ class CodexChannelService extends BaseChannelService {
|
|
|
184
223
|
name: channel.name,
|
|
185
224
|
base_url: channel.baseUrl,
|
|
186
225
|
wire_api: channel.wireApi || 'responses',
|
|
187
|
-
env_key:
|
|
226
|
+
env_key: CODEX_MANAGED_ENV_KEY,
|
|
188
227
|
requires_openai_auth: channel.requiresOpenaiAuth !== false
|
|
189
228
|
};
|
|
190
229
|
|
|
@@ -215,10 +254,9 @@ const service = getServiceInstance();
|
|
|
215
254
|
function getChannels() { return service.getChannels(); }
|
|
216
255
|
function getEnabledChannels() { return service.getEnabledChannels(); }
|
|
217
256
|
function createChannel(name, providerKey, baseUrl, apiKey, wireApi, extraConfig = {}) {
|
|
218
|
-
const envKey = extraConfig.envKey || `${providerKey.toUpperCase()}_API_KEY`;
|
|
219
257
|
return service.createChannel({
|
|
220
258
|
name, providerKey, baseUrl, apiKey, wireApi,
|
|
221
|
-
envKey,
|
|
259
|
+
envKey: CODEX_MANAGED_ENV_KEY,
|
|
222
260
|
...extraConfig,
|
|
223
261
|
});
|
|
224
262
|
}
|
|
@@ -251,4 +289,9 @@ module.exports = {
|
|
|
251
289
|
applyChannelToSettings,
|
|
252
290
|
getEffectiveApiKey,
|
|
253
291
|
disableAllChannels,
|
|
292
|
+
_test: {
|
|
293
|
+
buildManagedCodexEnvMap,
|
|
294
|
+
CODEX_MANAGED_ENV_KEY,
|
|
295
|
+
resolveCurrentManagedChannel
|
|
296
|
+
}
|
|
254
297
|
};
|
|
@@ -32,6 +32,40 @@ function powershellQuote(value) {
|
|
|
32
32
|
return `'${String(value).replace(/'/g, "''")}'`;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
function buildWindowsSettingChangeScript() {
|
|
36
|
+
return [
|
|
37
|
+
'Add-Type -Namespace Win32 -Name NativeMethods -MemberDefinition @"',
|
|
38
|
+
'[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]',
|
|
39
|
+
'public static extern IntPtr SendMessageTimeout(',
|
|
40
|
+
' IntPtr hWnd, uint Msg, UIntPtr wParam, string lParam,',
|
|
41
|
+
' uint fuFlags, uint uTimeout, out UIntPtr lpdwResult);',
|
|
42
|
+
'"@',
|
|
43
|
+
'$HWND_BROADCAST = [IntPtr]0xffff',
|
|
44
|
+
'$WM_SETTINGCHANGE = 0x1a',
|
|
45
|
+
'$SMTO_ABORTIFHUNG = 0x0002',
|
|
46
|
+
'$result = [UIntPtr]::Zero',
|
|
47
|
+
'[Win32.NativeMethods]::SendMessageTimeout($HWND_BROADCAST, $WM_SETTINGCHANGE,',
|
|
48
|
+
' [UIntPtr]::Zero, "Environment", $SMTO_ABORTIFHUNG, 5000, [ref]$result) | Out-Null'
|
|
49
|
+
].join('\n');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function buildWindowsEnvBatchScript(operations = [], { includeSettingChangeBroadcast = true } = {}) {
|
|
53
|
+
const normalizedOperations = Array.isArray(operations) ? operations.filter(Boolean) : [];
|
|
54
|
+
const lines = normalizedOperations.map((operation) => {
|
|
55
|
+
const key = powershellQuote(operation.key || '');
|
|
56
|
+
if (operation.remove) {
|
|
57
|
+
return `[Environment]::SetEnvironmentVariable(${key}, $null, 'User')`;
|
|
58
|
+
}
|
|
59
|
+
return `[Environment]::SetEnvironmentVariable(${key}, ${powershellQuote(operation.value || '')}, 'User')`;
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (includeSettingChangeBroadcast && lines.length > 0) {
|
|
63
|
+
lines.push(buildWindowsSettingChangeScript());
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return lines.join('\n');
|
|
67
|
+
}
|
|
68
|
+
|
|
35
69
|
function buildHomeRelativeShellPath(filePath, homeDir) {
|
|
36
70
|
const normalizedHome = path.resolve(homeDir);
|
|
37
71
|
const normalizedFilePath = path.resolve(filePath);
|
|
@@ -341,48 +375,28 @@ function runLaunchctlCommand(args, execSync) {
|
|
|
341
375
|
}
|
|
342
376
|
|
|
343
377
|
function broadcastWindowsSettingChange(execSync) {
|
|
344
|
-
|
|
345
|
-
'Add-Type -Namespace Win32 -Name NativeMethods -MemberDefinition @"',
|
|
346
|
-
'[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]',
|
|
347
|
-
'public static extern IntPtr SendMessageTimeout(',
|
|
348
|
-
' IntPtr hWnd, uint Msg, UIntPtr wParam, string lParam,',
|
|
349
|
-
' uint fuFlags, uint uTimeout, out UIntPtr lpdwResult);',
|
|
350
|
-
'"@',
|
|
351
|
-
'$HWND_BROADCAST = [IntPtr]0xffff',
|
|
352
|
-
'$WM_SETTINGCHANGE = 0x1a',
|
|
353
|
-
'$SMTO_ABORTIFHUNG = 0x0002',
|
|
354
|
-
'$result = [UIntPtr]::Zero',
|
|
355
|
-
'[Win32.NativeMethods]::SendMessageTimeout($HWND_BROADCAST, $WM_SETTINGCHANGE,',
|
|
356
|
-
' [UIntPtr]::Zero, "Environment", $SMTO_ABORTIFHUNG, 5000, [ref]$result) | Out-Null'
|
|
357
|
-
].join('\n');
|
|
358
|
-
runWindowsEnvCommand(script, execSync);
|
|
378
|
+
runWindowsEnvCommand(buildWindowsSettingChangeScript(), execSync);
|
|
359
379
|
}
|
|
360
380
|
|
|
361
381
|
function syncWindowsEnvironment(nextValues, previousState, options) {
|
|
362
382
|
const { stateFilePath, execSync } = options;
|
|
363
383
|
const nextKeys = Object.keys(nextValues).sort();
|
|
364
384
|
const previousValues = previousState.values || {};
|
|
365
|
-
|
|
385
|
+
const operations = [];
|
|
366
386
|
|
|
367
387
|
for (const [key, value] of Object.entries(nextValues)) {
|
|
368
388
|
if (previousValues[key] === value) continue;
|
|
369
|
-
|
|
370
|
-
changed = true;
|
|
389
|
+
operations.push({ key, value });
|
|
371
390
|
}
|
|
372
391
|
|
|
373
392
|
for (const key of Object.keys(previousValues)) {
|
|
374
393
|
if (Object.prototype.hasOwnProperty.call(nextValues, key)) continue;
|
|
375
|
-
|
|
376
|
-
changed = true;
|
|
394
|
+
operations.push({ key, remove: true });
|
|
377
395
|
}
|
|
378
396
|
|
|
379
|
-
|
|
397
|
+
const changed = operations.length > 0;
|
|
380
398
|
if (changed) {
|
|
381
|
-
|
|
382
|
-
broadcastWindowsSettingChange(execSync);
|
|
383
|
-
} catch {
|
|
384
|
-
// 广播失败不影响主流程,环境变量已写入注册表
|
|
385
|
-
}
|
|
399
|
+
runWindowsEnvCommand(buildWindowsEnvBatchScript(operations), execSync);
|
|
386
400
|
}
|
|
387
401
|
|
|
388
402
|
if (nextKeys.length > 0) {
|
|
@@ -427,14 +441,14 @@ function runWindowsEnvCommand(script, execSync) {
|
|
|
427
441
|
|
|
428
442
|
function setWindowsUserEnv(key, value, execSync) {
|
|
429
443
|
runWindowsEnvCommand(
|
|
430
|
-
|
|
444
|
+
buildWindowsEnvBatchScript([{ key, value }], { includeSettingChangeBroadcast: false }),
|
|
431
445
|
execSync
|
|
432
446
|
);
|
|
433
447
|
}
|
|
434
448
|
|
|
435
449
|
function removeWindowsUserEnv(key, execSync) {
|
|
436
450
|
runWindowsEnvCommand(
|
|
437
|
-
|
|
451
|
+
buildWindowsEnvBatchScript([{ key, remove: true }], { includeSettingChangeBroadcast: false }),
|
|
438
452
|
execSync
|
|
439
453
|
);
|
|
440
454
|
}
|
|
@@ -473,8 +487,10 @@ module.exports = {
|
|
|
473
487
|
syncCodexUserEnvironment,
|
|
474
488
|
_test: {
|
|
475
489
|
broadcastWindowsSettingChange,
|
|
490
|
+
buildWindowsEnvBatchScript,
|
|
476
491
|
buildHomeRelativeShellPath,
|
|
477
492
|
buildNextEnvValues,
|
|
493
|
+
buildWindowsSettingChangeScript,
|
|
478
494
|
buildSourceSnippet,
|
|
479
495
|
getPosixProfileCandidates,
|
|
480
496
|
readState,
|
|
@@ -890,12 +890,12 @@ async function probeModelAvailability(channel, channelType, options = {}) {
|
|
|
890
890
|
|
|
891
891
|
if (isAvailable) {
|
|
892
892
|
availableModels.push(model);
|
|
893
|
-
console.log(`[ModelDetector]
|
|
893
|
+
console.log(`[ModelDetector] [v] ${model} available`);
|
|
894
894
|
if (stopOnFirstAvailable) {
|
|
895
895
|
break;
|
|
896
896
|
}
|
|
897
897
|
} else {
|
|
898
|
-
console.log(`[ModelDetector]
|
|
898
|
+
console.log(`[ModelDetector] [x] ${model} not available${formatProbeFailureDetail(probeResult.failureDetail)}`);
|
|
899
899
|
}
|
|
900
900
|
}
|
|
901
901
|
|