@adversity/coding-tool-x 3.1.0 → 3.1.1

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 (137) hide show
  1. package/CHANGELOG.md +15 -18
  2. package/README.md +8 -8
  3. package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
  4. package/dist/web/assets/ConfigTemplates-ZrK_s7ma.js +1 -0
  5. package/dist/web/assets/Home-B8YfhZ3c.js +1 -0
  6. package/dist/web/assets/Home-Di2qsylF.css +1 -0
  7. package/dist/web/assets/PluginManager-BD7QUZbU.js +1 -0
  8. package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
  9. package/dist/web/assets/ProjectList-C1fQb9OW.css +1 -0
  10. package/dist/web/assets/ProjectList-DRb1DuHV.js +1 -0
  11. package/dist/web/assets/SessionList-BGJWyneI.css +1 -0
  12. package/dist/web/assets/SessionList-lZ0LKzfT.js +1 -0
  13. package/dist/web/assets/SkillManager-C1xG5B4Q.js +1 -0
  14. package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
  15. package/dist/web/assets/Terminal-DGNJeVtc.css +1 -0
  16. package/dist/web/assets/Terminal-DksBo_lM.js +1 -0
  17. package/dist/web/assets/WorkspaceManager-Burx7XOo.js +1 -0
  18. package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
  19. package/dist/web/assets/icons-kcfLIMBB.js +1 -0
  20. package/dist/web/assets/index-Ufv5rCa5.css +1 -0
  21. package/dist/web/assets/index-lAkrRC3h.js +2 -0
  22. package/dist/web/assets/markdown-BfC0goYb.css +10 -0
  23. package/dist/web/assets/markdown-C9MYpaSi.js +1 -0
  24. package/dist/web/assets/naive-ui-CSrLusZZ.js +1 -0
  25. package/dist/web/assets/{vendors-D2HHw_aW.js → vendors-CO3Upi1d.js} +2 -2
  26. package/dist/web/assets/vue-vendor-DqyWIXEb.js +45 -0
  27. package/dist/web/assets/xterm-6GBZ9nXN.css +32 -0
  28. package/dist/web/assets/xterm-BJzAjXCH.js +13 -0
  29. package/dist/web/index.html +8 -6
  30. package/package.json +4 -2
  31. package/src/commands/channels.js +48 -1
  32. package/src/commands/cli-type.js +4 -2
  33. package/src/commands/daemon.js +81 -12
  34. package/src/commands/doctor.js +10 -9
  35. package/src/commands/list.js +1 -1
  36. package/src/commands/logs.js +6 -4
  37. package/src/commands/port-config.js +24 -4
  38. package/src/commands/proxy-control.js +12 -6
  39. package/src/commands/search.js +1 -1
  40. package/src/commands/security.js +3 -2
  41. package/src/commands/stats.js +226 -52
  42. package/src/commands/switch.js +1 -1
  43. package/src/commands/toggle-proxy.js +31 -6
  44. package/src/commands/update.js +97 -0
  45. package/src/commands/workspace.js +1 -1
  46. package/src/config/default.js +39 -2
  47. package/src/config/loader.js +74 -8
  48. package/src/config/paths.js +105 -33
  49. package/src/index.js +64 -3
  50. package/src/plugins/constants.js +3 -2
  51. package/src/plugins/plugin-api.js +1 -1
  52. package/src/reset-config.js +4 -2
  53. package/src/server/api/agents.js +57 -14
  54. package/src/server/api/channels.js +112 -33
  55. package/src/server/api/codex-channels.js +111 -18
  56. package/src/server/api/codex-proxy.js +14 -8
  57. package/src/server/api/commands.js +71 -18
  58. package/src/server/api/config-export.js +0 -6
  59. package/src/server/api/config-registry.js +11 -3
  60. package/src/server/api/config.js +376 -5
  61. package/src/server/api/convert.js +133 -0
  62. package/src/server/api/dashboard.js +22 -6
  63. package/src/server/api/gemini-channels.js +107 -18
  64. package/src/server/api/gemini-proxy.js +14 -8
  65. package/src/server/api/gemini-sessions.js +1 -1
  66. package/src/server/api/health-check.js +4 -3
  67. package/src/server/api/mcp.js +3 -3
  68. package/src/server/api/opencode-channels.js +419 -0
  69. package/src/server/api/opencode-projects.js +99 -0
  70. package/src/server/api/opencode-proxy.js +198 -0
  71. package/src/server/api/opencode-sessions.js +403 -0
  72. package/src/server/api/opencode-statistics.js +57 -0
  73. package/src/server/api/plugins.js +66 -19
  74. package/src/server/api/prompts.js +2 -2
  75. package/src/server/api/proxy.js +7 -4
  76. package/src/server/api/sessions.js +3 -0
  77. package/src/server/api/skills.js +69 -18
  78. package/src/server/api/workspaces.js +78 -6
  79. package/src/server/codex-proxy-server.js +30 -18
  80. package/src/server/dev-server.js +1 -1
  81. package/src/server/gemini-proxy-server.js +15 -3
  82. package/src/server/index.js +165 -58
  83. package/src/server/opencode-proxy-server.js +4375 -0
  84. package/src/server/proxy-server.js +27 -18
  85. package/src/server/services/agents-service.js +61 -24
  86. package/src/server/services/channel-scheduler.js +9 -5
  87. package/src/server/services/channels.js +64 -37
  88. package/src/server/services/codex-channels.js +56 -43
  89. package/src/server/services/codex-settings-manager.js +271 -49
  90. package/src/server/services/codex-statistics-service.js +2 -2
  91. package/src/server/services/commands-service.js +84 -25
  92. package/src/server/services/config-export-service.js +7 -45
  93. package/src/server/services/config-registry-service.js +63 -17
  94. package/src/server/services/config-sync-manager.js +160 -7
  95. package/src/server/services/config-templates-service.js +204 -51
  96. package/src/server/services/env-checker.js +26 -12
  97. package/src/server/services/env-manager.js +126 -18
  98. package/src/server/services/favorites.js +5 -3
  99. package/src/server/services/gemini-channels.js +33 -44
  100. package/src/server/services/gemini-statistics-service.js +2 -2
  101. package/src/server/services/mcp-service.js +350 -9
  102. package/src/server/services/model-detector.js +707 -221
  103. package/src/server/services/network-access.js +80 -0
  104. package/src/server/services/opencode-channels.js +206 -0
  105. package/src/server/services/opencode-gateway-converter.js +639 -0
  106. package/src/server/services/opencode-sessions.js +663 -0
  107. package/src/server/services/opencode-settings-manager.js +342 -0
  108. package/src/server/services/opencode-statistics-service.js +255 -0
  109. package/src/server/services/plugins-service.js +479 -22
  110. package/src/server/services/prompts-service.js +53 -11
  111. package/src/server/services/proxy-runtime.js +1 -1
  112. package/src/server/services/repo-scanner-base.js +1 -1
  113. package/src/server/services/security-config.js +1 -1
  114. package/src/server/services/session-cache.js +1 -1
  115. package/src/server/services/skill-service.js +300 -46
  116. package/src/server/services/speed-test.js +464 -186
  117. package/src/server/services/statistics-service.js +2 -2
  118. package/src/server/services/terminal-commands.js +10 -3
  119. package/src/server/services/terminal-config.js +1 -1
  120. package/src/server/services/ui-config.js +1 -1
  121. package/src/server/services/workspace-service.js +57 -100
  122. package/src/server/websocket-server.js +132 -3
  123. package/src/ui/menu.js +49 -40
  124. package/src/utils/port-helper.js +22 -8
  125. package/src/utils/session.js +5 -4
  126. package/dist/web/assets/icons-CO_2OFES.js +0 -1
  127. package/dist/web/assets/index-DI8QOi-E.js +0 -14
  128. package/dist/web/assets/index-uLHGdeZh.css +0 -41
  129. package/dist/web/assets/naive-ui-B1re3c-e.js +0 -1
  130. package/dist/web/assets/vue-vendor-6JaYHOiI.js +0 -44
  131. package/src/server/api/oauth.js +0 -294
  132. package/src/server/api/permissions.js +0 -385
  133. package/src/server/config/oauth-providers.js +0 -68
  134. package/src/server/services/oauth-callback-server.js +0 -284
  135. package/src/server/services/oauth-service.js +0 -378
  136. package/src/server/services/oauth-token-storage.js +0 -135
  137. package/src/server/services/permission-templates-service.js +0 -308
@@ -20,9 +20,17 @@ const { injectEnvToShell, removeEnvFromShell, isProxyConfig } = require('./codex
20
20
  * - 使用 weight 和 maxConcurrency 控制负载均衡
21
21
  */
22
22
 
23
+ function normalizeGatewaySourceType(value, fallback = 'codex') {
24
+ const normalized = String(value || '').trim().toLowerCase();
25
+ if (normalized === 'claude') return 'claude';
26
+ if (normalized === 'codex') return 'codex';
27
+ if (normalized === 'gemini') return 'gemini';
28
+ return fallback;
29
+ }
30
+
23
31
  // 获取渠道存储文件路径
24
32
  function getChannelsFilePath() {
25
- const ccToolDir = path.join(os.homedir(), '.claude', 'cc-tool');
33
+ const ccToolDir = path.join(os.homedir(), '.cc-tool');
26
34
  if (!fs.existsSync(ccToolDir)) {
27
35
  fs.mkdirSync(ccToolDir, { recursive: true });
28
36
  }
@@ -43,15 +51,17 @@ function loadChannels() {
43
51
  const data = JSON.parse(content);
44
52
  // 确保渠道有 enabled 字段(兼容旧数据)
45
53
  if (data.channels) {
46
- data.channels = data.channels.map(ch => ({
47
- ...ch,
48
- enabled: ch.enabled !== false, // 默认启用
49
- weight: ch.weight || 1,
50
- maxConcurrency: ch.maxConcurrency || null,
51
- modelRedirects: ch.modelRedirects || [],
52
- speedTestModel: ch.speedTestModel || null,
53
- authType: ch.authType || 'apiKey' // 默认 API Key 认证
54
- }));
54
+ data.channels = data.channels.map(ch => {
55
+ return {
56
+ ...ch,
57
+ enabled: ch.enabled !== false, // 默认启用
58
+ weight: ch.weight || 1,
59
+ maxConcurrency: ch.maxConcurrency || null,
60
+ modelRedirects: ch.modelRedirects || [],
61
+ speedTestModel: ch.speedTestModel || null,
62
+ gatewaySourceType: normalizeGatewaySourceType(ch.gatewaySourceType, 'codex')
63
+ };
64
+ });
55
65
  }
56
66
  return data;
57
67
  } catch (err) {
@@ -111,6 +121,7 @@ function initializeFromConfig() {
111
121
  enabled: config.model_provider === providerKey, // 当前激活的渠道启用
112
122
  weight: 1,
113
123
  maxConcurrency: null,
124
+ gatewaySourceType: 'codex',
114
125
  createdAt: Date.now(),
115
126
  updatedAt: Date.now()
116
127
  });
@@ -180,9 +191,7 @@ function createChannel(name, providerKey, baseUrl, apiKey, wireApi = 'responses'
180
191
  maxConcurrency: extraConfig.maxConcurrency || null,
181
192
  modelRedirects: extraConfig.modelRedirects || [],
182
193
  speedTestModel: extraConfig.speedTestModel || null,
183
- authType: extraConfig.authType || 'apiKey',
184
- oauthProvider: extraConfig.oauthProvider || null,
185
- oauthTokenId: extraConfig.oauthTokenId || null,
194
+ gatewaySourceType: normalizeGatewaySourceType(extraConfig.gatewaySourceType, 'codex'),
186
195
  createdAt: Date.now(),
187
196
  updatedAt: Date.now()
188
197
  };
@@ -191,8 +200,8 @@ function createChannel(name, providerKey, baseUrl, apiKey, wireApi = 'responses'
191
200
  saveChannels(data);
192
201
 
193
202
  // 注入该渠道的环境变量(用于直接使用 codex 命令)
194
- if (apiKey && envKey) {
195
- const injectResult = injectEnvToShell(envKey, apiKey);
203
+ if (newChannel.enabled !== false && newChannel.apiKey && envKey) {
204
+ const injectResult = injectEnvToShell(envKey, newChannel.apiKey);
196
205
  if (injectResult.success) {
197
206
  console.log(`[Codex Channels] Environment variable ${envKey} injected for new channel`);
198
207
  } else {
@@ -232,6 +241,7 @@ function updateChannel(channelId, updates) {
232
241
  createdAt: oldChannel.createdAt, // 保持创建时间
233
242
  modelRedirects: merged.modelRedirects || [],
234
243
  speedTestModel: merged.speedTestModel !== undefined ? merged.speedTestModel : (oldChannel.speedTestModel || null),
244
+ gatewaySourceType: normalizeGatewaySourceType(merged.gatewaySourceType, 'codex'),
235
245
  updatedAt: Date.now()
236
246
  };
237
247
 
@@ -273,19 +283,23 @@ function updateChannel(channelId, updates) {
273
283
  // 如果 envKey 或 apiKey 变化,需要更新环境变量
274
284
  const oldEnvKey = oldChannel.envKey;
275
285
  const newEnvKey = newChannel.envKey;
276
- const oldApiKey = oldChannel.apiKey;
277
286
  const newApiKey = newChannel.apiKey;
278
-
279
- // 如果 envKey 改变,删除旧的,注入新的
280
- if (oldEnvKey !== newEnvKey && oldEnvKey) {
287
+ const shouldRemoveOldEnv =
288
+ !!oldEnvKey && (
289
+ oldEnvKey !== newEnvKey ||
290
+ !newApiKey ||
291
+ newChannel.enabled === false
292
+ );
293
+
294
+ // 禁用或 key 变化时都要清理旧环境变量,避免残留
295
+ if (shouldRemoveOldEnv) {
281
296
  const removeResult = removeEnvFromShell(oldEnvKey);
282
297
  if (removeResult.success) {
283
298
  console.log(`[Codex Channels] Old environment variable ${oldEnvKey} removed`);
284
299
  }
285
300
  }
286
301
 
287
- // 如果有新的 API Key,注入到环境变量
288
- if (newApiKey && newEnvKey) {
302
+ if (newChannel.enabled !== false && newApiKey && newEnvKey) {
289
303
  const injectResult = injectEnvToShell(newEnvKey, newApiKey);
290
304
  if (injectResult.success) {
291
305
  console.log(`[Codex Channels] Environment variable ${newEnvKey} updated`);
@@ -299,7 +313,7 @@ function updateChannel(channelId, updates) {
299
313
  }
300
314
 
301
315
  // 删除渠道
302
- function deleteChannel(channelId) {
316
+ async function deleteChannel(channelId) {
303
317
  const data = loadChannels();
304
318
 
305
319
  const index = data.channels.findIndex(c => c.id === channelId);
@@ -401,7 +415,7 @@ function writeCodexConfigForMultiChannel(allChannels) {
401
415
  // 回退默认的代理配置(使用默认端口),确保 provider 存在
402
416
  config.model_providers['cc-proxy'] = {
403
417
  name: 'cc-proxy',
404
- base_url: 'http://127.0.0.1:10089/v1',
418
+ base_url: 'http://127.0.0.1:20089/v1',
405
419
  wire_api: 'responses',
406
420
  env_key: 'CC_PROXY_KEY'
407
421
  };
@@ -452,6 +466,12 @@ ${tomlContent}`;
452
466
  }
453
467
 
454
468
  // 更新所有渠道的 API Key
469
+ for (const channel of allChannels) {
470
+ if (channel.envKey && !channel.apiKey) {
471
+ delete auth[channel.envKey];
472
+ }
473
+ }
474
+
455
475
  for (const channel of allChannels) {
456
476
  if (channel.apiKey) {
457
477
  auth[channel.envKey] = channel.apiKey;
@@ -512,7 +532,10 @@ function syncAllChannelEnvVars() {
512
532
  const results = [];
513
533
 
514
534
  for (const channel of channels) {
515
- if (channel.apiKey && channel.envKey) {
535
+ if (!channel.envKey) continue;
536
+
537
+ const shouldInject = channel.enabled !== false && !!channel.apiKey;
538
+ if (shouldInject) {
516
539
  const injectResult = injectEnvToShell(channel.envKey, channel.apiKey);
517
540
  if (injectResult.success) {
518
541
  syncedCount++;
@@ -520,7 +543,11 @@ function syncAllChannelEnvVars() {
520
543
  } else {
521
544
  results.push({ envKey: channel.envKey, success: false, error: injectResult.error });
522
545
  }
546
+ continue;
523
547
  }
548
+
549
+ // 清理已停用或缺失 key 的渠道环境变量,避免残留
550
+ removeEnvFromShell(channel.envKey);
524
551
  }
525
552
 
526
553
  console.log(`[Codex Channels] Synced ${syncedCount} environment variables`);
@@ -624,19 +651,21 @@ ${tomlContent}`;
624
651
  }
625
652
  }
626
653
 
627
- // 添加当前渠道的 API Key
628
654
  if (channel.apiKey && channel.envKey) {
629
655
  auth[channel.envKey] = channel.apiKey;
656
+ } else if (channel.envKey) {
657
+ delete auth[channel.envKey];
630
658
  }
631
659
 
632
660
  fs.writeFileSync(authPath, JSON.stringify(auth, null, 2), 'utf8');
633
661
 
634
- // 注入环境变量到 shell 配置文件
635
662
  if (channel.apiKey && channel.envKey) {
636
663
  const injectResult = injectEnvToShell(channel.envKey, channel.apiKey);
637
664
  if (injectResult.success) {
638
665
  console.log(`[Codex Channels] Environment variable ${channel.envKey} injected`);
639
666
  }
667
+ } else if (channel.envKey) {
668
+ removeEnvFromShell(channel.envKey);
640
669
  }
641
670
 
642
671
  return channel;
@@ -653,24 +682,8 @@ try {
653
682
  console.warn('[Codex Channels] Auto sync env vars failed:', err.message);
654
683
  }
655
684
 
656
- /**
657
- * 获取渠道的有效 API Key
658
- * 如果渠道使用 OAuth 认证,返回 OAuth 令牌;否则返回静态 API Key
659
- *
660
- * @param {Object} channel - 渠道对象
661
- * @returns {string|null} 有效的 API Key
662
- */
663
685
  function getEffectiveApiKey(channel) {
664
- if (channel.authType === 'oauth' && channel.oauthTokenId) {
665
- const { getToken, isTokenExpired } = require('./oauth-token-storage');
666
- const token = getToken(channel.oauthTokenId);
667
- if (token && !isTokenExpired(token)) {
668
- return token.accessToken;
669
- }
670
- // OAuth 令牌无效或已过期,返回 null
671
- return null;
672
- }
673
- return channel.apiKey;
686
+ return channel.apiKey || null;
674
687
  }
675
688
 
676
689
  module.exports = {
@@ -2,6 +2,7 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const os = require('os');
4
4
  const toml = require('toml');
5
+ const tomlStringify = require('@iarna/toml').stringify;
5
6
 
6
7
  // Codex 配置文件路径
7
8
  function getConfigPath() {
@@ -35,6 +36,167 @@ function hasBackup() {
35
36
  return fs.existsSync(getConfigBackupPath()) || fs.existsSync(getAuthBackupPath());
36
37
  }
37
38
 
39
+ const INVALID_ENV_NAME_PATTERN = /[\r\n]/;
40
+ const SHELL_MARKER_PREFIX = '# Added by Coding-Tool for Codex';
41
+
42
+ function normalizeEnvName(envName) {
43
+ const normalized = String(envName || '').trim();
44
+ if (!normalized || INVALID_ENV_NAME_PATTERN.test(normalized)) {
45
+ return null;
46
+ }
47
+ return normalized;
48
+ }
49
+
50
+ function ensureParentDir(filePath) {
51
+ const dirPath = path.dirname(filePath);
52
+ if (!fs.existsSync(dirPath)) {
53
+ fs.mkdirSync(dirPath, { recursive: true });
54
+ }
55
+ }
56
+
57
+ function writeFileAtomic(filePath, content) {
58
+ ensureParentDir(filePath);
59
+ const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
60
+
61
+ try {
62
+ fs.writeFileSync(tempPath, content, 'utf8');
63
+ fs.renameSync(tempPath, filePath);
64
+ } finally {
65
+ if (fs.existsSync(tempPath)) {
66
+ try {
67
+ fs.unlinkSync(tempPath);
68
+ } catch (cleanupErr) {
69
+ // ignore cleanup errors
70
+ }
71
+ }
72
+ }
73
+ }
74
+
75
+ function normalizeHomePath(filePath) {
76
+ const normalizedPath = String(filePath || '').replace(/\\/g, '/');
77
+ const normalizedHome = os.homedir().replace(/\\/g, '/');
78
+ if (normalizedPath.startsWith(normalizedHome)) {
79
+ return `~${normalizedPath.slice(normalizedHome.length)}`;
80
+ }
81
+ return filePath;
82
+ }
83
+
84
+ function compactBlankLines(lines) {
85
+ const compacted = [];
86
+ let previousIsBlank = false;
87
+
88
+ for (const line of lines) {
89
+ const isBlank = line.trim() === '';
90
+ if (isBlank) {
91
+ if (!previousIsBlank) {
92
+ compacted.push('');
93
+ }
94
+ previousIsBlank = true;
95
+ continue;
96
+ }
97
+
98
+ compacted.push(line);
99
+ previousIsBlank = false;
100
+ }
101
+
102
+ while (compacted.length > 0 && compacted[compacted.length - 1].trim() === '') {
103
+ compacted.pop();
104
+ }
105
+
106
+ return compacted;
107
+ }
108
+
109
+ function isPowerShellProfile(filePath) {
110
+ return String(filePath || '').toLowerCase().endsWith('.ps1');
111
+ }
112
+
113
+ function getShellConfigCandidates() {
114
+ const homeDir = os.homedir();
115
+ const shell = String(process.env.SHELL || '').toLowerCase();
116
+ const candidates = [];
117
+
118
+ if (process.platform === 'win32') {
119
+ if (shell.includes('zsh')) {
120
+ candidates.push(path.join(homeDir, '.zshrc'));
121
+ }
122
+
123
+ if (shell.includes('bash')) {
124
+ candidates.push(path.join(homeDir, '.bashrc'));
125
+ candidates.push(path.join(homeDir, '.bash_profile'));
126
+ }
127
+
128
+ candidates.push(path.join(homeDir, 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1'));
129
+ candidates.push(path.join(homeDir, 'Documents', 'WindowsPowerShell', 'Microsoft.PowerShell_profile.ps1'));
130
+ candidates.push(path.join(homeDir, '.bashrc'));
131
+ candidates.push(path.join(homeDir, '.profile'));
132
+ } else if (shell.includes('zsh')) {
133
+ candidates.push(path.join(homeDir, '.zshrc'));
134
+ candidates.push(path.join(homeDir, '.zprofile'));
135
+ candidates.push(path.join(homeDir, '.profile'));
136
+ } else if (shell.includes('bash')) {
137
+ if (process.platform === 'darwin') {
138
+ candidates.push(path.join(homeDir, '.bash_profile'));
139
+ candidates.push(path.join(homeDir, '.bashrc'));
140
+ } else {
141
+ candidates.push(path.join(homeDir, '.bashrc'));
142
+ candidates.push(path.join(homeDir, '.bash_profile'));
143
+ }
144
+ candidates.push(path.join(homeDir, '.profile'));
145
+ } else {
146
+ candidates.push(path.join(homeDir, '.zshrc'));
147
+ candidates.push(path.join(homeDir, '.bashrc'));
148
+ candidates.push(path.join(homeDir, '.bash_profile'));
149
+ candidates.push(path.join(homeDir, '.profile'));
150
+ }
151
+
152
+ return [...new Set(candidates)];
153
+ }
154
+
155
+ function getShellReloadCommand(configPath) {
156
+ if (!configPath) {
157
+ return process.platform === 'win32' ? '重启终端' : 'source ~/.zshrc';
158
+ }
159
+
160
+ const displayPath = normalizeHomePath(configPath);
161
+ const normalized = String(displayPath || '').replace(/\\/g, '/').toLowerCase();
162
+
163
+ if (normalized.endsWith('microsoft.powershell_profile.ps1')) {
164
+ return '. $PROFILE';
165
+ }
166
+ if (normalized.endsWith('/.zshrc')) {
167
+ return 'source ~/.zshrc';
168
+ }
169
+ if (normalized.endsWith('/.bash_profile')) {
170
+ return 'source ~/.bash_profile';
171
+ }
172
+ if (normalized.endsWith('/.bashrc')) {
173
+ return 'source ~/.bashrc';
174
+ }
175
+ if (normalized.endsWith('/.profile')) {
176
+ return 'source ~/.profile';
177
+ }
178
+
179
+ if (process.platform === 'win32') {
180
+ return '. $PROFILE';
181
+ }
182
+
183
+ return `source ${displayPath}`;
184
+ }
185
+
186
+ function escapeShellValue(value) {
187
+ return String(value ?? '')
188
+ .replace(/\\/g, '\\\\')
189
+ .replace(/"/g, '\\"')
190
+ .replace(/\$/g, '\\$')
191
+ .replace(/`/g, '\\`');
192
+ }
193
+
194
+ function escapePowerShellValue(value) {
195
+ return String(value ?? '')
196
+ .replace(/`/g, '``')
197
+ .replace(/"/g, '`"');
198
+ }
199
+
38
200
  // 读取 config.toml
39
201
  function readConfig() {
40
202
  try {
@@ -89,7 +251,8 @@ function configToToml(config) {
89
251
  // 写入 config.toml
90
252
  function writeConfig(config) {
91
253
  try {
92
- const content = configToToml(config);
254
+ const safeConfig = JSON.parse(JSON.stringify(config || {}));
255
+ const content = tomlStringify(safeConfig);
93
256
  fs.writeFileSync(getConfigPath(), content, 'utf8');
94
257
  } catch (err) {
95
258
  throw new Error('Failed to write config.toml: ' + err.message);
@@ -182,28 +345,28 @@ function restoreSettings() {
182
345
 
183
346
  // 获取用户的 shell 配置文件路径
184
347
  function getShellConfigPath() {
185
- const shell = process.env.SHELL || '';
186
- if (shell.includes('zsh')) {
187
- return path.join(os.homedir(), '.zshrc');
188
- } else if (shell.includes('bash')) {
189
- // macOS 使用 .bash_profile,Linux 使用 .bashrc
190
- const bashProfile = path.join(os.homedir(), '.bash_profile');
191
- const bashrc = path.join(os.homedir(), '.bashrc');
192
- if (fs.existsSync(bashProfile)) {
193
- return bashProfile;
194
- }
195
- return bashrc;
196
- }
197
- // 默认使用 .zshrc (macOS 默认)
198
- return path.join(os.homedir(), '.zshrc');
348
+ const candidates = getShellConfigCandidates();
349
+ const existing = candidates.find(filePath => fs.existsSync(filePath));
350
+ return existing || candidates[0];
199
351
  }
200
352
 
201
353
  // 注入环境变量到 shell 配置文件
202
354
  function injectEnvToShell(envName, envValue) {
355
+ const normalizedEnvName = normalizeEnvName(envName);
356
+ if (!normalizedEnvName) {
357
+ return {
358
+ success: false,
359
+ error: `Invalid environment variable name: ${envName}`,
360
+ isFirstTime: false
361
+ };
362
+ }
363
+
203
364
  const configPath = getShellConfigPath();
204
- const exportLine = `export ${envName}="${envValue}"`;
205
- // 使用更具体的标记,包含环境变量名,方便后续精确移除
206
- const marker = `# Added by Coding-Tool for Codex [${envName}]`;
365
+ const marker = `${SHELL_MARKER_PREFIX} [${normalizedEnvName}]`;
366
+ const usePowerShell = isPowerShellProfile(configPath);
367
+ const exportLine = usePowerShell
368
+ ? `$env:${normalizedEnvName} = "${escapePowerShellValue(envValue)}"`
369
+ : `export ${normalizedEnvName}="${escapeShellValue(envValue)}"`;
207
370
 
208
371
  try {
209
372
  let content = '';
@@ -211,23 +374,52 @@ function injectEnvToShell(envName, envValue) {
211
374
  content = fs.readFileSync(configPath, 'utf8');
212
375
  }
213
376
 
214
- // 检查是否已经存在这个环境变量配置
215
- const regex = new RegExp(`^export ${envName}=`, 'm');
216
- const alreadyExists = regex.test(content);
377
+ const envKeyEscaped = String(normalizedEnvName).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
378
+ const envLineRegex = usePowerShell
379
+ ? new RegExp(`^\\s*\\$env:${envKeyEscaped}\\s*=`, 'i')
380
+ : new RegExp(`^\\s*(?:export\\s+)?${envKeyEscaped}=`);
217
381
 
218
- if (alreadyExists) {
219
- // 已存在,替换它(保留原有的标记注释)
220
- content = content.replace(
221
- new RegExp(`^(# Added by Coding-Tool for Codex \\[${envName}\\]\n)?export ${envName}=.*$`, 'm'),
222
- `${marker}\n${exportLine}`
223
- );
224
- } else {
225
- // 不存在,追加到文件末尾
226
- content = content.trimEnd() + `\n\n${marker}\n${exportLine}\n`;
382
+ const originalLines = content ? content.split(/\r?\n/) : [];
383
+ const cleanedLines = [];
384
+ let existed = false;
385
+
386
+ for (let i = 0; i < originalLines.length; i++) {
387
+ const currentLine = originalLines[i];
388
+ const trimmedLine = currentLine.trim();
389
+
390
+ if (trimmedLine === marker) {
391
+ const nextLine = originalLines[i + 1] || '';
392
+ if (envLineRegex.test(nextLine.trim())) {
393
+ i += 1;
394
+ }
395
+ existed = true;
396
+ continue;
397
+ }
398
+
399
+ if (envLineRegex.test(trimmedLine)) {
400
+ existed = true;
401
+ continue;
402
+ }
403
+
404
+ cleanedLines.push(currentLine);
405
+ }
406
+
407
+ while (cleanedLines.length > 0 && cleanedLines[cleanedLines.length - 1].trim() === '') {
408
+ cleanedLines.pop();
409
+ }
410
+
411
+ if (cleanedLines.length > 0) {
412
+ cleanedLines.push('');
413
+ }
414
+
415
+ cleanedLines.push(marker, exportLine);
416
+
417
+ const nextContent = `${cleanedLines.join('\n')}\n`;
418
+ if (nextContent !== content) {
419
+ writeFileAtomic(configPath, nextContent);
227
420
  }
228
421
 
229
- fs.writeFileSync(configPath, content, 'utf8');
230
- return { success: true, path: configPath, isFirstTime: !alreadyExists };
422
+ return { success: true, path: configPath, isFirstTime: !existed };
231
423
  } catch (err) {
232
424
  // 不抛出错误,只是警告,因为这不是致命问题
233
425
  console.warn(`[Codex] Failed to inject env to shell config: ${err.message}`);
@@ -237,6 +429,14 @@ function injectEnvToShell(envName, envValue) {
237
429
 
238
430
  // 从 shell 配置文件移除环境变量
239
431
  function removeEnvFromShell(envName) {
432
+ const normalizedEnvName = normalizeEnvName(envName);
433
+ if (!normalizedEnvName) {
434
+ return {
435
+ success: false,
436
+ error: `Invalid environment variable name: ${envName}`
437
+ };
438
+ }
439
+
240
440
  const configPath = getShellConfigPath();
241
441
 
242
442
  try {
@@ -244,24 +444,46 @@ function removeEnvFromShell(envName) {
244
444
  return { success: true };
245
445
  }
246
446
 
247
- let content = fs.readFileSync(configPath, 'utf8');
447
+ const content = fs.readFileSync(configPath, 'utf8');
448
+ const usePowerShell = isPowerShellProfile(configPath);
449
+ const marker = `${SHELL_MARKER_PREFIX} [${normalizedEnvName}]`;
450
+ const envKeyEscaped = String(normalizedEnvName).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
451
+ const envLineRegex = usePowerShell
452
+ ? new RegExp(`^\\s*\\$env:${envKeyEscaped}\\s*=`, 'i')
453
+ : new RegExp(`^\\s*(?:export\\s+)?${envKeyEscaped}=`);
454
+
455
+ const originalLines = content ? content.split(/\r?\n/) : [];
456
+ const cleanedLines = [];
457
+ let changed = false;
458
+
459
+ for (let i = 0; i < originalLines.length; i++) {
460
+ const currentLine = originalLines[i];
461
+ const trimmedLine = currentLine.trim();
462
+
463
+ if (trimmedLine === marker) {
464
+ const nextLine = originalLines[i + 1] || '';
465
+ if (envLineRegex.test(nextLine.trim())) {
466
+ i += 1;
467
+ }
468
+ changed = true;
469
+ continue;
470
+ }
248
471
 
249
- // 移除具体标记的环境变量(推荐方式)
250
- content = content.replace(
251
- new RegExp(`\\n?# Added by Coding-Tool for Codex \\[${envName}\\]\\nexport ${envName}=.*\\n?`, 'g'),
252
- '\n'
253
- );
472
+ if (envLineRegex.test(trimmedLine)) {
473
+ changed = true;
474
+ continue;
475
+ }
254
476
 
255
- // 如果没有标记,也尝试移除(兼容旧数据)
256
- content = content.replace(
257
- new RegExp(`^export ${envName}=.*\\n?`, 'gm'),
258
- ''
259
- );
477
+ cleanedLines.push(currentLine);
478
+ }
260
479
 
261
- // 清理多余的空行
262
- content = content.replace(/\n\n\n+/g, '\n\n');
480
+ if (!changed) {
481
+ return { success: true };
482
+ }
263
483
 
264
- fs.writeFileSync(configPath, content, 'utf8');
484
+ const normalized = compactBlankLines(cleanedLines);
485
+ const nextContent = normalized.length > 0 ? `${normalized.join('\n')}\n` : '';
486
+ writeFileAtomic(configPath, nextContent);
265
487
  return { success: true };
266
488
  } catch (err) {
267
489
  console.warn(`[Codex] Failed to remove env from shell config: ${err.message}`);
@@ -306,8 +528,8 @@ function setProxyConfig(proxyPort) {
306
528
  const shellInjectResult = injectEnvToShell('CC_PROXY_KEY', 'PROXY_KEY');
307
529
 
308
530
  // 获取 shell 配置文件路径用于提示信息
309
- const shellConfigPath = getShellConfigPath();
310
- const sourceCommand = process.env.SHELL?.includes('zsh') ? 'source ~/.zshrc' : 'source ~/.bashrc';
531
+ const shellConfigPath = shellInjectResult.path || getShellConfigPath();
532
+ const sourceCommand = getShellReloadCommand(shellConfigPath);
311
533
 
312
534
  console.log(`Codex settings updated to use proxy on port ${proxyPort}`);
313
535
  return {
@@ -341,7 +563,7 @@ function isProxyConfig() {
341
563
  const currentProvider = config.model_provider;
342
564
  if (currentProvider && config.model_providers && config.model_providers[currentProvider]) {
343
565
  const baseUrl = config.model_providers[currentProvider].base_url || '';
344
- if (baseUrl.includes('127.0.0.1') && baseUrl.includes('10089')) {
566
+ if (baseUrl.includes('127.0.0.1') || baseUrl.includes('localhost')) {
345
567
  return true;
346
568
  }
347
569
  }
@@ -6,7 +6,7 @@ const os = require('os');
6
6
  * Codex 统计服务 - 数据采集和存储
7
7
  *
8
8
  * 文件结构:
9
- * ~/.claude/cc-tool/
9
+ * ~/.cc-tool/
10
10
  * ├── codex-statistics.json # Codex 总体统计
11
11
  * └── codex-daily-stats/
12
12
  * ├── 2025-12-05.json # 每日汇总统计
@@ -15,7 +15,7 @@ const os = require('os');
15
15
 
16
16
  // 获取基础目录
17
17
  function getBaseDir() {
18
- const dir = path.join(os.homedir(), '.claude', 'cc-tool');
18
+ const dir = path.join(os.homedir(), '.cc-tool');
19
19
  if (!fs.existsSync(dir)) {
20
20
  fs.mkdirSync(dir, { recursive: true });
21
21
  }