coding-tool-x 3.3.7 → 3.3.9

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 (89) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +253 -326
  3. package/dist/web/assets/{Analytics-IW6eAy9u.js → Analytics-D6LzK9hk.js} +1 -1
  4. package/dist/web/assets/{ConfigTemplates-BPtkTMSc.js → ConfigTemplates-BUDYuxRi.js} +1 -1
  5. package/dist/web/assets/Home-BQxQ1LhR.css +1 -0
  6. package/dist/web/assets/Home-D7KX7iF8.js +1 -0
  7. package/dist/web/assets/{PluginManager-BGx9MSDV.js → PluginManager-DTgQ--vB.js} +1 -1
  8. package/dist/web/assets/{ProjectList-BCn-mrCx.js → ProjectList-DMCiGmCT.js} +1 -1
  9. package/dist/web/assets/{SessionList-CzLfebJQ.js → SessionList-CRBsdVRe.js} +1 -1
  10. package/dist/web/assets/{SkillManager-CXz2vBQx.js → SkillManager-DMwx2Q4k.js} +1 -1
  11. package/dist/web/assets/{WorkspaceManager-CHtgMfKc.js → WorkspaceManager-DapB4ljL.js} +1 -1
  12. package/dist/web/assets/{icons-B29onFfZ.js → icons-B5Pl4lrD.js} +1 -1
  13. package/dist/web/assets/index-CL-qpoJ_.js +2 -0
  14. package/dist/web/assets/index-D_5dRFOL.css +1 -0
  15. package/dist/web/assets/{markdown-C9MYpaSi.js → markdown-DyTJGI4N.js} +1 -1
  16. package/dist/web/assets/{naive-ui-CxpuzdjU.js → naive-ui-Bdxp09n2.js} +1 -1
  17. package/dist/web/assets/{vendors-DMjSfzlv.js → vendors-CKPV1OAU.js} +2 -2
  18. package/dist/web/assets/{vue-vendor-DET08QYg.js → vue-vendor-3bf-fPGP.js} +1 -1
  19. package/dist/web/index.html +7 -7
  20. package/docs/home.png +0 -0
  21. package/package.json +14 -5
  22. package/src/commands/daemon.js +3 -2
  23. package/src/commands/security.js +1 -2
  24. package/src/commands/toggle-proxy.js +100 -5
  25. package/src/config/paths.js +718 -90
  26. package/src/server/api/agents.js +1 -1
  27. package/src/server/api/channels.js +9 -0
  28. package/src/server/api/claude-hooks.js +13 -8
  29. package/src/server/api/codex-channels.js +9 -0
  30. package/src/server/api/codex-proxy.js +27 -15
  31. package/src/server/api/gemini-proxy.js +22 -11
  32. package/src/server/api/hooks.js +45 -0
  33. package/src/server/api/oauth-credentials.js +163 -0
  34. package/src/server/api/opencode-proxy.js +22 -10
  35. package/src/server/api/plugins.js +2 -1
  36. package/src/server/api/proxy.js +39 -44
  37. package/src/server/api/skills.js +91 -13
  38. package/src/server/api/ui-config.js +5 -0
  39. package/src/server/codex-proxy-server.js +90 -70
  40. package/src/server/gemini-proxy-server.js +107 -88
  41. package/src/server/index.js +2 -0
  42. package/src/server/opencode-proxy-server.js +381 -225
  43. package/src/server/proxy-server.js +86 -60
  44. package/src/server/services/alias.js +3 -3
  45. package/src/server/services/channels.js +21 -24
  46. package/src/server/services/codex-channels.js +158 -255
  47. package/src/server/services/codex-config.js +2 -5
  48. package/src/server/services/codex-env-manager.js +423 -0
  49. package/src/server/services/codex-settings-manager.js +21 -357
  50. package/src/server/services/codex-statistics-service.js +3 -27
  51. package/src/server/services/config-export-service.js +43 -9
  52. package/src/server/services/config-registry-service.js +3 -2
  53. package/src/server/services/config-sync-manager.js +1 -1
  54. package/src/server/services/favorites.js +4 -3
  55. package/src/server/services/gemini-channels.js +14 -12
  56. package/src/server/services/gemini-statistics-service.js +3 -25
  57. package/src/server/services/mcp-service.js +35 -19
  58. package/src/server/services/model-detector.js +4 -3
  59. package/src/server/services/native-keychain.js +243 -0
  60. package/src/server/services/native-oauth-adapters.js +891 -0
  61. package/src/server/services/network-access.js +39 -1
  62. package/src/server/services/notification-hooks.js +951 -0
  63. package/src/server/services/oauth-credentials-service.js +786 -0
  64. package/src/server/services/oauth-utils.js +49 -0
  65. package/src/server/services/opencode-channels.js +19 -15
  66. package/src/server/services/opencode-sessions.js +2 -2
  67. package/src/server/services/opencode-settings-manager.js +169 -16
  68. package/src/server/services/opencode-statistics-service.js +3 -27
  69. package/src/server/services/plugins-service.js +115 -15
  70. package/src/server/services/prompts-service.js +2 -3
  71. package/src/server/services/proxy-log-helper.js +242 -0
  72. package/src/server/services/proxy-runtime.js +6 -4
  73. package/src/server/services/repo-scanner-base.js +12 -4
  74. package/src/server/services/request-logger.js +7 -7
  75. package/src/server/services/security-config.js +4 -4
  76. package/src/server/services/session-cache.js +2 -2
  77. package/src/server/services/sessions.js +2 -2
  78. package/src/server/services/settings-manager.js +13 -0
  79. package/src/server/services/skill-service.js +867 -368
  80. package/src/server/services/statistics-service.js +5 -5
  81. package/src/server/services/ui-config.js +4 -3
  82. package/src/server/services/workspace-service.js +1 -1
  83. package/src/server/websocket-server.js +5 -4
  84. package/dist/web/assets/Home-BsSioaaB.css +0 -1
  85. package/dist/web/assets/Home-obifg_9E.js +0 -1
  86. package/dist/web/assets/index-C7LPdVsN.js +0 -2
  87. package/dist/web/assets/index-eEmjZKWP.css +0 -1
  88. package/docs/bannel.png +0 -0
  89. package/docs/model-redirection.md +0 -251
@@ -1,21 +1,23 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const os = require('os');
4
3
  const crypto = require('crypto');
5
4
  const toml = require('toml');
6
5
  const tomlStringify = require('@iarna/toml').stringify;
7
- const { resolvePreferredHomeDir } = require('../../utils/home-dir');
6
+ const { PATHS } = require('../../config/paths');
8
7
  const { getCodexDir } = require('./codex-config');
9
- const { injectEnvToShell, removeEnvFromShell, isProxyConfig } = require('./codex-settings-manager');
8
+ const { isProxyConfig } = require('./codex-settings-manager');
9
+ const { clearNativeOAuth } = require('./native-oauth-adapters');
10
+ const { syncCodexUserEnvironment } = require('./codex-env-manager');
10
11
 
11
- const HOME_DIR = resolvePreferredHomeDir(process.platform, process.env, os.homedir());
12
+ const CODEX_PROXY_ENV_KEY = 'CC_PROXY_KEY';
13
+ const CODEX_PROXY_ENV_VALUE = 'PROXY_KEY';
12
14
 
13
15
  /**
14
16
  * Codex 渠道管理服务(多渠道架构)
15
17
  *
16
18
  * Codex 配置结构:
17
19
  * - config.toml: 主配置,包含 model_provider 和各提供商配置
18
- * - auth.json: API Key 存储
20
+ * - 用户级环境变量: env_key 对应的 API Key 存储
19
21
  * - 我们的 codex-channels.json: 完整渠道信息(用于管理)
20
22
  *
21
23
  * 多渠道模式:
@@ -31,13 +33,28 @@ function normalizeGatewaySourceType(value, fallback = 'codex') {
31
33
  return fallback;
32
34
  }
33
35
 
36
+ function buildManagedCodexEnvMap(channels = [], { includeProxyKey = false } = {}) {
37
+ const envMap = {};
38
+
39
+ for (const channel of channels) {
40
+ if (!channel?.envKey || !channel?.apiKey) continue;
41
+ envMap[channel.envKey] = channel.apiKey;
42
+ }
43
+
44
+ if (includeProxyKey) {
45
+ envMap[CODEX_PROXY_ENV_KEY] = CODEX_PROXY_ENV_VALUE;
46
+ }
47
+
48
+ return envMap;
49
+ }
50
+
34
51
  // 获取渠道存储文件路径
35
52
  function getChannelsFilePath() {
36
- const ccToolDir = path.join(HOME_DIR, '.cc-tool');
37
- if (!fs.existsSync(ccToolDir)) {
38
- fs.mkdirSync(ccToolDir, { recursive: true });
53
+ const channelsDir = path.dirname(PATHS.channels.codex);
54
+ if (!fs.existsSync(channelsDir)) {
55
+ fs.mkdirSync(channelsDir, { recursive: true });
39
56
  }
40
- return path.join(ccToolDir, 'codex-channels.json');
57
+ return PATHS.channels.codex;
41
58
  }
42
59
 
43
60
  // 读取所有渠道(从我们的存储文件)
@@ -102,11 +119,11 @@ function initializeFromConfig() {
102
119
  for (const [providerKey, providerConfig] of Object.entries(config.model_providers)) {
103
120
  // env_key 优先级:配置的 env_key > PROVIDER_API_KEY > OPENAI_API_KEY
104
121
  let envKey = providerConfig.env_key || `${providerKey.toUpperCase()}_API_KEY`;
105
- let apiKey = auth[envKey] || '';
122
+ let apiKey = process.env[envKey] || auth[envKey] || '';
106
123
 
107
124
  // 如果没找到,尝试 OPENAI_API_KEY 作为通用 fallback
108
- if (!apiKey && auth['OPENAI_API_KEY']) {
109
- apiKey = auth['OPENAI_API_KEY'];
125
+ if (!apiKey && (process.env.OPENAI_API_KEY || auth['OPENAI_API_KEY'])) {
126
+ apiKey = process.env.OPENAI_API_KEY || auth['OPENAI_API_KEY'];
110
127
  envKey = 'OPENAI_API_KEY';
111
128
  }
112
129
 
@@ -128,14 +145,6 @@ function initializeFromConfig() {
128
145
  createdAt: Date.now(),
129
146
  updatedAt: Date.now()
130
147
  });
131
-
132
- // 自动注入环境变量(从 Codex 迁移过来时使用)
133
- if (apiKey && envKey) {
134
- const injectResult = injectEnvToShell(envKey, apiKey);
135
- if (injectResult.success) {
136
- console.log(`[Codex Channels] Environment variable ${envKey} injected during initialization`);
137
- }
138
- }
139
148
  }
140
149
  }
141
150
 
@@ -158,6 +167,86 @@ function saveChannels(data) {
158
167
  fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
159
168
  }
160
169
 
170
+ function getDefaultCodexConfig() {
171
+ return {
172
+ model: 'gpt-4',
173
+ model_reasoning_effort: 'high',
174
+ model_reasoning_summary_format: 'experimental',
175
+ network_access: 'enabled',
176
+ disable_response_storage: false,
177
+ show_raw_agent_reasoning: true
178
+ };
179
+ }
180
+
181
+ function cloneConfigValue(value) {
182
+ return JSON.parse(JSON.stringify(value));
183
+ }
184
+
185
+ function readCodexConfigOrThrow(configPath, fallbackConfig = {}) {
186
+ if (!fs.existsSync(configPath)) {
187
+ return cloneConfigValue(fallbackConfig);
188
+ }
189
+
190
+ const content = fs.readFileSync(configPath, 'utf8');
191
+
192
+ try {
193
+ return toml.parse(content);
194
+ } catch (err) {
195
+ throw new Error(`Failed to parse existing config.toml: ${err.message}`);
196
+ }
197
+ }
198
+
199
+ function writeTextAtomic(filePath, content) {
200
+ const dirPath = path.dirname(filePath);
201
+ if (!fs.existsSync(dirPath)) {
202
+ fs.mkdirSync(dirPath, { recursive: true });
203
+ }
204
+
205
+ const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
206
+
207
+ try {
208
+ fs.writeFileSync(tempPath, content, 'utf8');
209
+ fs.renameSync(tempPath, filePath);
210
+ } finally {
211
+ if (fs.existsSync(tempPath)) {
212
+ try {
213
+ fs.unlinkSync(tempPath);
214
+ } catch {
215
+ // ignore temp cleanup errors
216
+ }
217
+ }
218
+ }
219
+ }
220
+
221
+ function writeAnnotatedCodexConfig(configPath, config, headerLines = []) {
222
+ const tomlContent = tomlStringify(config);
223
+ const prefix = headerLines.length > 0 ? `${headerLines.join('\n')}\n\n` : '';
224
+ writeTextAtomic(configPath, `${prefix}${tomlContent}`);
225
+ }
226
+
227
+ function getManagedProviderKeys(channels = []) {
228
+ const keys = new Set(['cc-proxy']);
229
+ for (const channel of channels) {
230
+ if (channel?.providerKey) {
231
+ keys.add(channel.providerKey);
232
+ }
233
+ }
234
+ return keys;
235
+ }
236
+
237
+ function pruneManagedProviders(existingProviders = {}, currentProviderKey, channels = []) {
238
+ const managedProviderKeys = getManagedProviderKeys(channels);
239
+ const preservedProviders = {};
240
+
241
+ for (const [providerKey, providerConfig] of Object.entries(existingProviders)) {
242
+ if (!managedProviderKeys.has(providerKey) || providerKey === currentProviderKey) {
243
+ preservedProviders[providerKey] = providerConfig;
244
+ }
245
+ }
246
+
247
+ return preservedProviders;
248
+ }
249
+
161
250
  // 获取所有渠道
162
251
  function getChannels() {
163
252
  const data = loadChannels();
@@ -201,16 +290,7 @@ function createChannel(name, providerKey, baseUrl, apiKey, wireApi = 'responses'
201
290
 
202
291
  data.channels.push(newChannel);
203
292
  saveChannels(data);
204
-
205
- // 注入该渠道的环境变量(用于直接使用 codex 命令)
206
- if (newChannel.enabled !== false && newChannel.apiKey && envKey) {
207
- const injectResult = injectEnvToShell(envKey, newChannel.apiKey);
208
- if (injectResult.success) {
209
- console.log(`[Codex Channels] Environment variable ${envKey} injected for new channel`);
210
- } else {
211
- console.warn(`[Codex Channels] Failed to inject ${envKey}: ${injectResult.error}`);
212
- }
213
- }
293
+ syncAllChannelEnvVars(data.channels);
214
294
 
215
295
  // 注意:不再自动写入 config.toml,只在开启代理控制时才同步
216
296
  // writeCodexConfigForMultiChannel(data.channels);
@@ -266,14 +346,6 @@ function updateChannel(channelId, updates) {
266
346
  console.log(`[Codex Single-channel mode] Enabled "${newChannel.name}", disabled all others`);
267
347
  }
268
348
 
269
- // Prevent disabling last enabled channel when proxy is OFF
270
- if (!isProxyRunning && !newChannel.enabled && oldChannel.enabled) {
271
- const enabledCount = data.channels.filter(ch => ch.enabled).length;
272
- if (enabledCount === 0) {
273
- throw new Error('无法禁用最后一个启用的渠道。请先启用其他渠道或启动动态切换。');
274
- }
275
- }
276
-
277
349
  saveChannels(data);
278
350
 
279
351
  // Sync config.toml only when proxy is OFF.
@@ -283,32 +355,7 @@ function updateChannel(channelId, updates) {
283
355
  applyChannelToSettings(channelId);
284
356
  }
285
357
 
286
- // 处理环境变量更新
287
- // 如果 envKey 或 apiKey 变化,需要更新环境变量
288
- const oldEnvKey = oldChannel.envKey;
289
- const newEnvKey = newChannel.envKey;
290
- const newApiKey = newChannel.apiKey;
291
- const shouldRemoveOldEnv =
292
- !!oldEnvKey && (
293
- oldEnvKey !== newEnvKey ||
294
- !newApiKey ||
295
- newChannel.enabled === false
296
- );
297
-
298
- // 禁用或 key 变化时都要清理旧环境变量,避免残留
299
- if (shouldRemoveOldEnv) {
300
- const removeResult = removeEnvFromShell(oldEnvKey);
301
- if (removeResult.success) {
302
- console.log(`[Codex Channels] Old environment variable ${oldEnvKey} removed`);
303
- }
304
- }
305
-
306
- if (newChannel.enabled !== false && newApiKey && newEnvKey) {
307
- const injectResult = injectEnvToShell(newEnvKey, newApiKey);
308
- if (injectResult.success) {
309
- console.log(`[Codex Channels] Environment variable ${newEnvKey} updated`);
310
- }
311
- }
358
+ syncAllChannelEnvVars(data.channels);
312
359
 
313
360
  // 注意:不再自动写入 config.toml,只在开启代理控制时才同步
314
361
  // writeCodexConfigForMultiChannel(data.channels);
@@ -328,16 +375,7 @@ async function deleteChannel(channelId) {
328
375
  const deletedChannel = data.channels[index];
329
376
  data.channels.splice(index, 1);
330
377
  saveChannels(data);
331
-
332
- // 从 shell 配置文件移除该渠道的环境变量
333
- if (deletedChannel.envKey) {
334
- const removeResult = removeEnvFromShell(deletedChannel.envKey);
335
- if (removeResult.success) {
336
- console.log(`[Codex Channels] Environment variable ${deletedChannel.envKey} removed`);
337
- } else {
338
- console.warn(`[Codex Channels] Failed to remove ${deletedChannel.envKey}: ${removeResult.error}`);
339
- }
340
- }
378
+ syncAllChannelEnvVars(data.channels);
341
379
 
342
380
  // 注意:不再自动写入 config.toml,只在开启代理控制时才同步
343
381
  // writeCodexConfigForMultiChannel(data.channels);
@@ -361,41 +399,20 @@ function writeCodexConfigForMultiChannel(allChannels) {
361
399
  }
362
400
 
363
401
  const configPath = path.join(codexDir, 'config.toml');
364
- const authPath = path.join(codexDir, 'auth.json');
365
402
 
366
403
  // 读取现有配置,保留所有现有字段(特别是 mcp_servers, projects 等)
404
+ const defaultConfig = getDefaultCodexConfig();
405
+ const parsedConfig = readCodexConfigOrThrow(configPath, defaultConfig);
367
406
  let config = {
368
- model: 'gpt-4',
369
- model_reasoning_effort: 'high',
370
- model_reasoning_summary_format: 'experimental',
371
- network_access: 'enabled',
372
- disable_response_storage: false,
373
- show_raw_agent_reasoning: true
407
+ ...parsedConfig,
408
+ model: parsedConfig.model || defaultConfig.model,
409
+ model_reasoning_effort: parsedConfig.model_reasoning_effort || defaultConfig.model_reasoning_effort,
410
+ model_reasoning_summary_format: parsedConfig.model_reasoning_summary_format || defaultConfig.model_reasoning_summary_format,
411
+ network_access: parsedConfig.network_access || defaultConfig.network_access,
412
+ disable_response_storage: parsedConfig.disable_response_storage !== undefined ? parsedConfig.disable_response_storage : defaultConfig.disable_response_storage,
413
+ show_raw_agent_reasoning: parsedConfig.show_raw_agent_reasoning !== undefined ? parsedConfig.show_raw_agent_reasoning : defaultConfig.show_raw_agent_reasoning
374
414
  };
375
415
 
376
- if (fs.existsSync(configPath)) {
377
- try {
378
- const content = fs.readFileSync(configPath, 'utf8');
379
- const parsedConfig = toml.parse(content);
380
-
381
- // 深度合并,保留原有的所有配置
382
- config = {
383
- ...parsedConfig,
384
- // 只覆盖这些字段
385
- model: parsedConfig.model || config.model,
386
- model_reasoning_effort: parsedConfig.model_reasoning_effort || config.model_reasoning_effort,
387
- model_reasoning_summary_format: parsedConfig.model_reasoning_summary_format || config.model_reasoning_summary_format,
388
- network_access: parsedConfig.network_access || config.network_access,
389
- disable_response_storage: parsedConfig.disable_response_storage !== undefined ? parsedConfig.disable_response_storage : config.disable_response_storage,
390
- show_raw_agent_reasoning: parsedConfig.show_raw_agent_reasoning !== undefined ? parsedConfig.show_raw_agent_reasoning : config.show_raw_agent_reasoning,
391
- // mcp_servers 和 projects 会从 parsedConfig 自动继承
392
- // model_provider 会根据动态切换情况决定是否更新
393
- };
394
- } catch (err) {
395
- // ignore read error, use defaults
396
- }
397
- }
398
-
399
416
  // 判断是否已启用动态切换
400
417
  const isProxyMode = config.model_provider === 'cc-proxy';
401
418
  const existingProviders = (config && typeof config.model_providers === 'object') ? config.model_providers : {};
@@ -441,51 +458,15 @@ function writeCodexConfigForMultiChannel(allChannels) {
441
458
  }
442
459
  }
443
460
 
444
- // 使用 TOML 序列化写入配置(保留注释和格式)
445
- try {
446
- const tomlContent = tomlStringify(config);
447
- // 在开头添加标记注释
448
- const annotatedContent = `# Codex Configuration
449
- # Managed by Coding-Tool
450
- # WARNING: MCP servers and projects are preserved automatically
451
-
452
- ${tomlContent}`;
453
-
454
- fs.writeFileSync(configPath, annotatedContent, 'utf8');
455
- } catch (err) {
456
- console.error('[Codex Channels] Failed to write config with TOML stringify:', err);
457
- // 降级处理:如果 tomlStringify 失败,使用手工拼接(但这样会丢失注释)
458
- const fallbackContent = JSON.stringify(config, null, 2);
459
- fs.writeFileSync(configPath, fallbackContent, 'utf8');
460
- }
461
-
462
- // 更新 auth.json
463
- let auth = {};
464
- if (fs.existsSync(authPath)) {
465
- try {
466
- auth = JSON.parse(fs.readFileSync(authPath, 'utf8'));
467
- } catch (err) {
468
- console.warn('[Codex Channels] Failed to read auth.json, creating new');
469
- }
470
- }
471
-
472
- // 更新所有渠道的 API Key
473
- for (const channel of allChannels) {
474
- if (channel.envKey && !channel.apiKey) {
475
- delete auth[channel.envKey];
476
- }
477
- }
478
-
479
- for (const channel of allChannels) {
480
- if (channel.apiKey) {
481
- auth[channel.envKey] = channel.apiKey;
482
- }
483
- }
484
-
485
- fs.writeFileSync(authPath, JSON.stringify(auth, null, 2), 'utf8');
486
-
487
- // 注意:环境变量注入在 createChannel 和 updateChannel 时已经处理
488
- // 这里不再重复注入,避免多次写入 shell 配置文件
461
+ writeAnnotatedCodexConfig(configPath, config, [
462
+ '# Codex Configuration',
463
+ '# Managed by Coding-Tool',
464
+ '# WARNING: MCP servers and projects are preserved automatically'
465
+ ]);
466
+ syncCodexUserEnvironment(
467
+ buildManagedCodexEnvMap(allChannels, { includeProxyKey: isProxyMode }),
468
+ { replace: true }
469
+ );
489
470
  }
490
471
 
491
472
  // 获取所有启用的渠道(供调度器使用)
@@ -523,43 +504,12 @@ function saveChannelOrder(order) {
523
504
  * 确保用户可以直接使用 codex 命令而无需手动设置环境变量
524
505
  * 这个函数会在服务启动时自动调用
525
506
  */
526
- function syncAllChannelEnvVars() {
527
- try {
528
- const data = loadChannels();
529
- const channels = data.channels || [];
530
-
531
- if (channels.length === 0) {
532
- return { success: true, synced: 0 };
533
- }
534
-
535
- let syncedCount = 0;
536
- const results = [];
537
-
538
- for (const channel of channels) {
539
- if (!channel.envKey) continue;
540
-
541
- const shouldInject = channel.enabled !== false && !!channel.apiKey;
542
- if (shouldInject) {
543
- const injectResult = injectEnvToShell(channel.envKey, channel.apiKey);
544
- if (injectResult.success) {
545
- syncedCount++;
546
- results.push({ envKey: channel.envKey, success: true });
547
- } else {
548
- results.push({ envKey: channel.envKey, success: false, error: injectResult.error });
549
- }
550
- continue;
551
- }
552
-
553
- // 清理已停用或缺失 key 的渠道环境变量,避免残留
554
- removeEnvFromShell(channel.envKey);
555
- }
556
-
557
- console.log(`[Codex Channels] Synced ${syncedCount} environment variables`);
558
- return { success: true, synced: syncedCount, results };
559
- } catch (err) {
560
- console.error('[Codex Channels] Failed to sync env vars:', err);
561
- return { success: false, error: err.message };
562
- }
507
+ function syncAllChannelEnvVars(channels = null) {
508
+ const data = channels ? { channels } : loadChannels();
509
+ return syncCodexUserEnvironment(
510
+ buildManagedCodexEnvMap(data.channels || [], { includeProxyKey: isProxyConfig() }),
511
+ { replace: true }
512
+ );
563
513
  }
564
514
 
565
515
  /**
@@ -584,6 +534,7 @@ function applyChannelToSettings(channelId, options = {}) {
584
534
  ch.enabled = ch.id === channelId;
585
535
  });
586
536
  saveChannels(data);
537
+ clearNativeOAuth('codex');
587
538
 
588
539
  const codexDir = getCodexDir();
589
540
 
@@ -592,36 +543,20 @@ function applyChannelToSettings(channelId, options = {}) {
592
543
  }
593
544
 
594
545
  const configPath = path.join(codexDir, 'config.toml');
595
- const authPath = path.join(codexDir, 'auth.json');
596
546
 
597
547
  // 读取现有配置,保留 mcp_servers, projects 等
598
- let config = {
599
- model: 'gpt-4',
600
- model_reasoning_effort: 'high',
601
- model_reasoning_summary_format: 'experimental',
602
- network_access: 'enabled',
603
- disable_response_storage: false,
604
- show_raw_agent_reasoning: true
605
- };
606
-
607
- if (fs.existsSync(configPath)) {
608
- try {
609
- const content = fs.readFileSync(configPath, 'utf8');
610
- const parsedConfig = toml.parse(content);
611
- // 深度合并,保留原有的所有配置
612
- config = { ...parsedConfig };
613
- } catch (err) {
614
- console.warn('[Codex Channels] Failed to read existing config, using defaults');
615
- }
616
- }
548
+ let config = readCodexConfigOrThrow(configPath, getDefaultCodexConfig());
617
549
 
618
550
  // 设置当前渠道为 model_provider
619
551
  config.model_provider = channel.providerKey;
620
552
 
621
553
  // 可选:清理 provider,关闭动态切换后只保留当前渠道配置
622
554
  if (options.pruneProviders === true) {
623
- config.model_providers = {};
624
- } else if (!config.model_providers) {
555
+ const existingProviders = (config.model_providers && typeof config.model_providers === 'object')
556
+ ? config.model_providers
557
+ : {};
558
+ config.model_providers = pruneManagedProviders(existingProviders, channel.providerKey, data.channels);
559
+ } else if (!config.model_providers || typeof config.model_providers !== 'object') {
625
560
  // 默认兼容历史行为:保留已有 provider
626
561
  config.model_providers = {};
627
562
  }
@@ -640,52 +575,13 @@ function applyChannelToSettings(channelId, options = {}) {
640
575
  config.model_providers[channel.providerKey].query_params = channel.queryParams;
641
576
  }
642
577
 
643
- // 使用 TOML 序列化写入配置
644
- try {
645
- const tomlContent = tomlStringify(config);
646
- const annotatedContent = `# Codex Configuration
647
- # Managed by Coding-Tool
648
- # Current provider: ${channel.name}
649
-
650
- ${tomlContent}`;
651
-
652
- fs.writeFileSync(configPath, annotatedContent, 'utf8');
653
- console.log(`[Codex Channels] Applied channel ${channel.name} to config.toml`);
654
- } catch (err) {
655
- console.error('[Codex Channels] Failed to write config with TOML stringify:', err);
656
- throw new Error('Failed to write config.toml: ' + err.message);
657
- }
658
-
659
- // 更新 auth.json
660
- let auth = {};
661
- if (fs.existsSync(authPath)) {
662
- try {
663
- auth = JSON.parse(fs.readFileSync(authPath, 'utf8'));
664
- } catch (err) {
665
- console.warn('[Codex Channels] Failed to read auth.json, creating new');
666
- }
667
- }
668
-
669
- if (channel.apiKey && channel.envKey) {
670
- auth[channel.envKey] = channel.apiKey;
671
- } else if (channel.envKey) {
672
- delete auth[channel.envKey];
673
- }
674
-
675
- // 清除 chatgpt token 认证字段,避免 Codex 优先用过期 token 而报 usage limit
676
- delete auth.tokens;
677
- delete auth.auth_mode;
678
-
679
- fs.writeFileSync(authPath, JSON.stringify(auth, null, 2), 'utf8');
680
-
681
- if (channel.apiKey && channel.envKey) {
682
- const injectResult = injectEnvToShell(channel.envKey, channel.apiKey);
683
- if (injectResult.success) {
684
- console.log(`[Codex Channels] Environment variable ${channel.envKey} injected`);
685
- }
686
- } else if (channel.envKey) {
687
- removeEnvFromShell(channel.envKey);
688
- }
578
+ writeAnnotatedCodexConfig(configPath, config, [
579
+ '# Codex Configuration',
580
+ '# Managed by Coding-Tool',
581
+ `# Current provider: ${channel.name}`
582
+ ]);
583
+ console.log(`[Codex Channels] Applied channel ${channel.name} to config.toml`);
584
+ syncAllChannelEnvVars();
689
585
 
690
586
  return channel;
691
587
  }
@@ -705,6 +601,12 @@ function getEffectiveApiKey(channel) {
705
601
  return channel.apiKey || null;
706
602
  }
707
603
 
604
+ function disableAllChannels() {
605
+ const data = loadChannels();
606
+ data.channels.forEach(ch => { ch.enabled = false; });
607
+ saveChannels(data);
608
+ }
609
+
708
610
  module.exports = {
709
611
  getChannels,
710
612
  createChannel,
@@ -715,5 +617,6 @@ module.exports = {
715
617
  syncAllChannelEnvVars,
716
618
  writeCodexConfigForMultiChannel,
717
619
  applyChannelToSettings,
718
- getEffectiveApiKey
620
+ getEffectiveApiKey,
621
+ disableAllChannels
719
622
  };
@@ -1,16 +1,13 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const os = require('os');
4
3
  const toml = require('toml');
5
- const { resolvePreferredHomeDir } = require('../../utils/home-dir');
6
-
7
- const HOME_DIR = resolvePreferredHomeDir(process.platform, process.env, os.homedir());
4
+ const { NATIVE_PATHS } = require('../../config/paths');
8
5
 
9
6
  /**
10
7
  * 获取 Codex 配置目录
11
8
  */
12
9
  function getCodexDir() {
13
- return path.join(HOME_DIR, '.codex');
10
+ return NATIVE_PATHS.codex.dir;
14
11
  }
15
12
 
16
13
  /**