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.
Files changed (63) hide show
  1. package/dist/web/assets/{Analytics-CbGxotgz.js → Analytics-DFWyPf5C.js} +1 -1
  2. package/dist/web/assets/{ConfigTemplates-oP6nrFEb.js → ConfigTemplates-BFE7hmKd.js} +1 -1
  3. package/dist/web/assets/{Home-DMntmEvh.js → Home-DZUuCrxk.js} +1 -1
  4. package/dist/web/assets/{PluginManager-BUC_c7nH.js → PluginManager-WyGY2BQN.js} +1 -1
  5. package/dist/web/assets/{ProjectList-CW8J49n7.js → ProjectList-CBc0QawN.js} +1 -1
  6. package/dist/web/assets/{ProjectList-oJIyIRkP.css → ProjectList-DL4JK6ci.css} +1 -1
  7. package/dist/web/assets/{SessionList-7lYnF92v.js → SessionList-CdPR7QLq.js} +1 -1
  8. package/dist/web/assets/{SkillManager-Cs08216i.js → SkillManager-B5-DxQOS.js} +1 -1
  9. package/dist/web/assets/{WorkspaceManager-CY-oGtyB.js → WorkspaceManager-C7yqFjpi.js} +1 -1
  10. package/dist/web/assets/index-BDsmoSfO.js +2 -0
  11. package/dist/web/assets/{index-5qy5NMIP.css → index-C1pzEgmj.css} +1 -1
  12. package/dist/web/index.html +2 -2
  13. package/package.json +2 -2
  14. package/src/commands/channels.js +13 -13
  15. package/src/commands/cli-type.js +5 -5
  16. package/src/commands/daemon.js +31 -31
  17. package/src/commands/doctor.js +14 -14
  18. package/src/commands/export-config.js +23 -23
  19. package/src/commands/list.js +4 -4
  20. package/src/commands/logs.js +19 -19
  21. package/src/commands/plugin.js +62 -62
  22. package/src/commands/port-config.js +4 -4
  23. package/src/commands/proxy-control.js +35 -35
  24. package/src/commands/proxy.js +28 -28
  25. package/src/commands/resume.js +4 -4
  26. package/src/commands/search.js +9 -9
  27. package/src/commands/security.js +5 -5
  28. package/src/commands/stats.js +18 -18
  29. package/src/commands/switch.js +1 -1
  30. package/src/commands/toggle-proxy.js +18 -18
  31. package/src/commands/ui.js +11 -11
  32. package/src/commands/update.js +9 -9
  33. package/src/commands/workspace.js +11 -11
  34. package/src/index.js +24 -24
  35. package/src/plugins/plugin-installer.js +1 -1
  36. package/src/reset-config.js +9 -9
  37. package/src/server/api/channels.js +1 -1
  38. package/src/server/api/claude-hooks.js +3 -2
  39. package/src/server/api/plugins.js +165 -14
  40. package/src/server/api/pm2-autostart.js +2 -2
  41. package/src/server/api/proxy.js +6 -6
  42. package/src/server/api/skills.js +66 -7
  43. package/src/server/codex-proxy-server.js +10 -2
  44. package/src/server/dev-server.js +2 -2
  45. package/src/server/gemini-proxy-server.js +10 -2
  46. package/src/server/index.js +37 -37
  47. package/src/server/opencode-proxy-server.js +10 -2
  48. package/src/server/proxy-server.js +14 -6
  49. package/src/server/services/codex-channels.js +64 -21
  50. package/src/server/services/codex-env-manager.js +44 -28
  51. package/src/server/services/config-export-service.js +1 -1
  52. package/src/server/services/mcp-service.js +2 -1
  53. package/src/server/services/model-detector.js +2 -2
  54. package/src/server/services/native-keychain.js +1 -0
  55. package/src/server/services/plugins-service.js +1066 -261
  56. package/src/server/services/proxy-runtime.js +129 -5
  57. package/src/server/services/server-shutdown.js +79 -0
  58. package/src/server/services/settings-manager.js +3 -3
  59. package/src/server/services/skill-service.js +146 -29
  60. package/src/server/websocket-server.js +8 -8
  61. package/src/ui/menu.js +2 -2
  62. package/src/ui/prompts.js +5 -5
  63. package/dist/web/assets/index-ClCqKpvX.js +0 -2
@@ -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💡 ${lines[0]}`));
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⚠️ 端口 ${port} 已被占用\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💡 请手动关闭占用端口的程序,或使用其他端口\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💡 请稍后重试,或手动检查端口占用情况\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(' 端口已释放\n'));
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 端口 ${port} 已被占用`));
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 启动服务器失败: ${err.message}\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🚀 Coding-Tool Web UI running at:`);
266
+ console.log(`\n[START] Coding-Tool Web UI running at:`);
267
267
  if (host === '0.0.0.0') {
268
- console.log(chalk.yellow(` ⚠️ 警告: 服务正在监听所有网络接口 (LAN 可访问)`));
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(' 🔒 已启用 LAN 安全保护:远程写操作默认禁用'));
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🔄 检测到 Claude 代理状态文件,正在自动启动...'));
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(`✅ Claude 代理已自动启动,端口: ${proxyPort}`));
303
+ console.log(chalk.green(`[OK] Claude 代理已自动启动,端口: ${proxyPort}`));
304
304
  })
305
305
  .catch((err) => {
306
- console.error(chalk.red(`❌ Claude 代理启动失败: ${err.message}`));
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🔄 检测到 Codex 代理状态文件,正在自动启动...'));
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(`✅ Codex 代理已自动启动,端口: ${port}`));
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(`❌ Codex 代理配置同步失败: ${err.message}`));
327
+ console.error(chalk.red(`[ERROR] Codex 代理配置同步失败: ${err.message}`));
328
328
  }
329
329
  })
330
330
  .catch((err) => {
331
- console.error(chalk.red(`❌ Codex 代理启动失败: ${err.message}`));
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🔄 检测到 Gemini 代理状态文件,正在自动启动...'));
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(`✅ Gemini 代理已自动启动,端口: ${result.port}`));
343
+ console.log(chalk.green(`[OK] Gemini 代理已自动启动,端口: ${result.port}`));
344
344
  } else {
345
- console.error(chalk.red(`❌ Gemini 代理启动失败: ${result.error || 'Unknown error'}`));
345
+ console.error(chalk.red(`[ERROR] Gemini 代理启动失败: ${result.error || 'Unknown error'}`));
346
346
  }
347
347
  })
348
348
  .catch((err) => {
349
- console.error(chalk.red(`❌ Gemini 代理启动失败: ${err.message}`));
349
+ console.error(chalk.red(`[ERROR] Gemini 代理启动失败: ${err.message}`));
350
350
  });
351
351
  } else {
352
- console.log(chalk.gray('\n💡 提示: 如需使用 Gemini 代理,请在前端界面激活 Gemini 渠道'));
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🔄 检测到 OpenCode 代理状态文件,正在自动启动...'));
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(`✅ OpenCode 代理已自动启动,端口: ${result.port}`));
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(`❌ OpenCode 代理配置同步失败: ${err.message}`));
393
+ console.error(chalk.red(`[ERROR] OpenCode 代理配置同步失败: ${err.message}`));
394
394
  }
395
395
  } else {
396
- console.error(chalk.red(`❌ OpenCode 代理启动失败: ${result.error || 'Unknown error'}`));
396
+ console.error(chalk.red(`[ERROR] OpenCode 代理启动失败: ${result.error || 'Unknown error'}`));
397
397
  }
398
398
  })
399
399
  .catch((err) => {
400
- console.error(chalk.red(`❌ OpenCode 代理启动失败: ${err.message}`));
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(` 已为 ${healthResult.summary.created} 个项目创建 .claude/sessions 目录`));
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(` ${healthResult.summary.errors} 个项目检查失败`));
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(` 所有 ${healthResult.summary.healthy} 个项目状态正常`));
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(' 健康检查失败:'), err.message);
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 startTime = getProxyStartTime('opencode');
4717
- const runtime = getProxyRuntime('opencode');
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(`✅ Proxy server started on http://127.0.0.1:${port}`);
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 代理服务端口 ${port} 已被占用`));
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
- console.log('✅ Proxy server stopped');
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 startTime = getProxyStartTime('claude');
603
- const runtime = getProxyRuntime('claude');
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 CODEX_PROXY_ENV_KEY = 'CC_PROXY_KEY';
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 buildManagedCodexEnvMap(channels = [], { includeProxyKey = false } = {}) {
19
- if (includeProxyKey) {
20
- return { [CODEX_PROXY_ENV_KEY]: CODEX_PROXY_ENV_VALUE };
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
- const envMap = {};
23
- for (const ch of channels) {
24
- if (ch.enabled !== false && ch.envKey && ch.apiKey) {
25
- envMap[ch.envKey] = ch.apiKey;
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
- return envMap;
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: ch.envKey,
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 = 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, _allChannels) {
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, _allChannels) {
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: channel.envKey,
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
- const script = [
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
- let changed = false;
385
+ const operations = [];
366
386
 
367
387
  for (const [key, value] of Object.entries(nextValues)) {
368
388
  if (previousValues[key] === value) continue;
369
- setWindowsUserEnv(key, value, execSync);
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
- removeWindowsUserEnv(key, execSync);
376
- changed = true;
394
+ operations.push({ key, remove: true });
377
395
  }
378
396
 
379
- // 广播 WM_SETTINGCHANGE,通知已打开的应用(如 VSCode)刷新环境变量
397
+ const changed = operations.length > 0;
380
398
  if (changed) {
381
- try {
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
- `[Environment]::SetEnvironmentVariable(${powershellQuote(key)}, ${powershellQuote(value)}, 'User')`,
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
- `[Environment]::SetEnvironmentVariable(${powershellQuote(key)}, $null, 'User')`,
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,
@@ -214,7 +214,7 @@ function buildExportReadme(exportData) {
214
214
  导出时间: ${exportedAt}
215
215
  版本: ${exportData.version || CONFIG_VERSION}
216
216
 
217
- ## 📦 包含内容
217
+ ## [PKG] 包含内容
218
218
  - 配置模板、频道配置、工作区、收藏
219
219
  - Agents / Skills / Commands
220
220
  - 插件 (Plugins)
@@ -1380,7 +1380,8 @@ async function testStdioServer(spec) {
1380
1380
  child = spawn(resolvedCommand, args, {
1381
1381
  env: mergedEnv,
1382
1382
  stdio: ['pipe', 'pipe', 'pipe'],
1383
- cwd
1383
+ cwd,
1384
+ windowsHide: true
1384
1385
  });
1385
1386
 
1386
1387
  child.stdout.on('data', (data) => {
@@ -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] ${model} available`);
893
+ console.log(`[ModelDetector] [v] ${model} available`);
894
894
  if (stopOnFirstAvailable) {
895
895
  break;
896
896
  }
897
897
  } else {
898
- console.log(`[ModelDetector] ${model} not available${formatProbeFailureDetail(probeResult.failureDetail)}`);
898
+ console.log(`[ModelDetector] [x] ${model} not available${formatProbeFailureDetail(probeResult.failureDetail)}`);
899
899
  }
900
900
  }
901
901
 
@@ -4,6 +4,7 @@ function runCommand(command, args, options = {}) {
4
4
  const result = spawnSync(command, args, {
5
5
  encoding: 'utf8',
6
6
  maxBuffer: 10 * 1024 * 1024,
7
+ windowsHide: true,
7
8
  ...options
8
9
  });
9
10