coding-tool-x 3.3.7 → 3.3.8

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 (48) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/dist/web/assets/{Analytics-IW6eAy9u.js → Analytics-DLpoDZ2M.js} +1 -1
  3. package/dist/web/assets/{ConfigTemplates-BPtkTMSc.js → ConfigTemplates-D_hRb55W.js} +1 -1
  4. package/dist/web/assets/Home-BMoFdAwy.css +1 -0
  5. package/dist/web/assets/Home-DNwp-0J-.js +1 -0
  6. package/dist/web/assets/{PluginManager-BGx9MSDV.js → PluginManager-JXsyym1s.js} +1 -1
  7. package/dist/web/assets/{ProjectList-BCn-mrCx.js → ProjectList-DZWSeb-q.js} +1 -1
  8. package/dist/web/assets/{SessionList-CzLfebJQ.js → SessionList-Cs624DR3.js} +1 -1
  9. package/dist/web/assets/{SkillManager-CXz2vBQx.js → SkillManager-bEliz7qz.js} +1 -1
  10. package/dist/web/assets/{WorkspaceManager-CHtgMfKc.js → WorkspaceManager-J3RecFGn.js} +1 -1
  11. package/dist/web/assets/{icons-B29onFfZ.js → icons-Cuc23WS7.js} +1 -1
  12. package/dist/web/assets/index-BXeSvAwU.js +2 -0
  13. package/dist/web/assets/index-DWAC3Tdv.css +1 -0
  14. package/dist/web/index.html +3 -3
  15. package/package.json +3 -2
  16. package/src/commands/toggle-proxy.js +100 -5
  17. package/src/config/paths.js +102 -19
  18. package/src/server/api/channels.js +9 -0
  19. package/src/server/api/codex-channels.js +9 -0
  20. package/src/server/api/codex-proxy.js +22 -11
  21. package/src/server/api/gemini-proxy.js +22 -11
  22. package/src/server/api/oauth-credentials.js +163 -0
  23. package/src/server/api/opencode-proxy.js +22 -10
  24. package/src/server/api/plugins.js +3 -1
  25. package/src/server/api/proxy.js +39 -44
  26. package/src/server/api/skills.js +91 -13
  27. package/src/server/codex-proxy-server.js +1 -11
  28. package/src/server/index.js +1 -0
  29. package/src/server/services/channels.js +18 -22
  30. package/src/server/services/codex-channels.js +124 -175
  31. package/src/server/services/codex-config.js +2 -5
  32. package/src/server/services/codex-settings-manager.js +12 -348
  33. package/src/server/services/config-export-service.js +23 -2
  34. package/src/server/services/gemini-channels.js +11 -9
  35. package/src/server/services/mcp-service.js +33 -16
  36. package/src/server/services/native-keychain.js +243 -0
  37. package/src/server/services/native-oauth-adapters.js +890 -0
  38. package/src/server/services/oauth-credentials-service.js +786 -0
  39. package/src/server/services/oauth-utils.js +49 -0
  40. package/src/server/services/opencode-channels.js +13 -9
  41. package/src/server/services/opencode-settings-manager.js +169 -16
  42. package/src/server/services/plugins-service.js +22 -1
  43. package/src/server/services/settings-manager.js +13 -0
  44. package/src/server/services/skill-service.js +712 -332
  45. package/dist/web/assets/Home-BsSioaaB.css +0 -1
  46. package/dist/web/assets/Home-obifg_9E.js +0 -1
  47. package/dist/web/assets/index-C7LPdVsN.js +0 -2
  48. package/dist/web/assets/index-eEmjZKWP.css +0 -1
@@ -3,26 +3,27 @@ const path = require('path');
3
3
  const os = require('os');
4
4
  const toml = require('toml');
5
5
  const tomlStringify = require('@iarna/toml').stringify;
6
- const { resolvePreferredHomeDir, isWindowsLikePlatform } = require('../../utils/home-dir');
6
+ const { resolvePreferredHomeDir } = require('../../utils/home-dir');
7
+ const { NATIVE_PATHS } = require('../../config/paths');
7
8
 
8
9
  const HOME_DIR = resolvePreferredHomeDir(process.platform, process.env, os.homedir());
9
10
 
10
11
  // Codex 配置文件路径
11
12
  function getConfigPath() {
12
- return path.join(HOME_DIR, '.codex', 'config.toml');
13
+ return NATIVE_PATHS.codex.config;
13
14
  }
14
15
 
15
16
  function getAuthPath() {
16
- return path.join(HOME_DIR, '.codex', 'auth.json');
17
+ return NATIVE_PATHS.codex.auth;
17
18
  }
18
19
 
19
20
  // 备份文件路径
20
21
  function getConfigBackupPath() {
21
- return path.join(HOME_DIR, '.codex', 'config.toml.cc-tool-backup');
22
+ return NATIVE_PATHS.codex.configBackup;
22
23
  }
23
24
 
24
25
  function getAuthBackupPath() {
25
- return path.join(HOME_DIR, '.codex', 'auth.json.cc-tool-backup');
26
+ return NATIVE_PATHS.codex.authBackup;
26
27
  }
27
28
 
28
29
  // 检查配置文件是否存在
@@ -39,172 +40,6 @@ function hasBackup() {
39
40
  return fs.existsSync(getConfigBackupPath()) || fs.existsSync(getAuthBackupPath());
40
41
  }
41
42
 
42
- const INVALID_ENV_NAME_PATTERN = /[\r\n]/;
43
- const SHELL_MARKER_PREFIX = '# Added by Coding-Tool for Codex';
44
-
45
- function normalizeEnvName(envName) {
46
- const normalized = String(envName || '').trim();
47
- if (!normalized || INVALID_ENV_NAME_PATTERN.test(normalized)) {
48
- return null;
49
- }
50
- return normalized;
51
- }
52
-
53
- function ensureParentDir(filePath) {
54
- const dirPath = path.dirname(filePath);
55
- if (!fs.existsSync(dirPath)) {
56
- fs.mkdirSync(dirPath, { recursive: true });
57
- }
58
- }
59
-
60
- function writeFileAtomic(filePath, content) {
61
- ensureParentDir(filePath);
62
- const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
63
-
64
- try {
65
- fs.writeFileSync(tempPath, content, 'utf8');
66
- fs.renameSync(tempPath, filePath);
67
- } finally {
68
- if (fs.existsSync(tempPath)) {
69
- try {
70
- fs.unlinkSync(tempPath);
71
- } catch (cleanupErr) {
72
- // ignore cleanup errors
73
- }
74
- }
75
- }
76
- }
77
-
78
- function normalizeHomePath(filePath) {
79
- const normalizedPath = String(filePath || '').replace(/\\/g, '/');
80
- const normalizedHome = HOME_DIR.replace(/\\/g, '/');
81
- if (normalizedPath.startsWith(normalizedHome)) {
82
- return `~${normalizedPath.slice(normalizedHome.length)}`;
83
- }
84
- return filePath;
85
- }
86
-
87
- function compactBlankLines(lines) {
88
- const compacted = [];
89
- let previousIsBlank = false;
90
-
91
- for (const line of lines) {
92
- const isBlank = line.trim() === '';
93
- if (isBlank) {
94
- if (!previousIsBlank) {
95
- compacted.push('');
96
- }
97
- previousIsBlank = true;
98
- continue;
99
- }
100
-
101
- compacted.push(line);
102
- previousIsBlank = false;
103
- }
104
-
105
- while (compacted.length > 0 && compacted[compacted.length - 1].trim() === '') {
106
- compacted.pop();
107
- }
108
-
109
- return compacted;
110
- }
111
-
112
- function isPowerShellProfile(filePath) {
113
- return String(filePath || '').toLowerCase().endsWith('.ps1');
114
- }
115
-
116
- function getShellConfigCandidates() {
117
- const homeDir = HOME_DIR;
118
- const shell = String(process.env.SHELL || '').toLowerCase();
119
- const candidates = [];
120
-
121
- if (isWindowsLikePlatform(process.platform, process.env)) {
122
- const oneDriveDir = process.env.OneDrive || process.env.ONEDRIVE || '';
123
-
124
- if (shell.includes('zsh')) {
125
- candidates.push(path.join(homeDir, '.zshrc'));
126
- }
127
-
128
- if (shell.includes('bash')) {
129
- candidates.push(path.join(homeDir, '.bashrc'));
130
- candidates.push(path.join(homeDir, '.bash_profile'));
131
- }
132
-
133
- candidates.push(path.join(homeDir, 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1'));
134
- candidates.push(path.join(homeDir, 'Documents', 'WindowsPowerShell', 'Microsoft.PowerShell_profile.ps1'));
135
- if (oneDriveDir) {
136
- candidates.push(path.join(oneDriveDir, 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1'));
137
- candidates.push(path.join(oneDriveDir, 'Documents', 'WindowsPowerShell', 'Microsoft.PowerShell_profile.ps1'));
138
- }
139
- candidates.push(path.join(homeDir, '.bashrc'));
140
- candidates.push(path.join(homeDir, '.profile'));
141
- } else if (shell.includes('zsh')) {
142
- candidates.push(path.join(homeDir, '.zshrc'));
143
- candidates.push(path.join(homeDir, '.zprofile'));
144
- candidates.push(path.join(homeDir, '.profile'));
145
- } else if (shell.includes('bash')) {
146
- if (process.platform === 'darwin') {
147
- candidates.push(path.join(homeDir, '.bash_profile'));
148
- candidates.push(path.join(homeDir, '.bashrc'));
149
- } else {
150
- candidates.push(path.join(homeDir, '.bashrc'));
151
- candidates.push(path.join(homeDir, '.bash_profile'));
152
- }
153
- candidates.push(path.join(homeDir, '.profile'));
154
- } else {
155
- candidates.push(path.join(homeDir, '.zshrc'));
156
- candidates.push(path.join(homeDir, '.bashrc'));
157
- candidates.push(path.join(homeDir, '.bash_profile'));
158
- candidates.push(path.join(homeDir, '.profile'));
159
- }
160
-
161
- return [...new Set(candidates)];
162
- }
163
-
164
- function getShellReloadCommand(configPath) {
165
- if (!configPath) {
166
- return isWindowsLikePlatform(process.platform, process.env) ? '重启终端' : 'source ~/.zshrc';
167
- }
168
-
169
- const displayPath = normalizeHomePath(configPath);
170
- const normalized = String(displayPath || '').replace(/\\/g, '/').toLowerCase();
171
-
172
- if (normalized.endsWith('microsoft.powershell_profile.ps1')) {
173
- return '. $PROFILE';
174
- }
175
- if (normalized.endsWith('/.zshrc')) {
176
- return 'source ~/.zshrc';
177
- }
178
- if (normalized.endsWith('/.bash_profile')) {
179
- return 'source ~/.bash_profile';
180
- }
181
- if (normalized.endsWith('/.bashrc')) {
182
- return 'source ~/.bashrc';
183
- }
184
- if (normalized.endsWith('/.profile')) {
185
- return 'source ~/.profile';
186
- }
187
-
188
- if (isWindowsLikePlatform(process.platform, process.env)) {
189
- return '. $PROFILE';
190
- }
191
-
192
- return `source ${displayPath}`;
193
- }
194
-
195
- function escapeShellValue(value) {
196
- return String(value ?? '')
197
- .replace(/\\/g, '\\\\')
198
- .replace(/"/g, '\\"')
199
- .replace(/\$/g, '\\$')
200
- .replace(/`/g, '\\`');
201
- }
202
-
203
- function escapePowerShellValue(value) {
204
- return String(value ?? '')
205
- .replace(/`/g, '``')
206
- .replace(/"/g, '`"');
207
- }
208
43
 
209
44
  // 读取 config.toml
210
45
  function readConfig() {
@@ -359,10 +194,7 @@ function restoreSettings() {
359
194
  fs.unlinkSync(getAuthBackupPath());
360
195
  }
361
196
 
362
- // 清理 shell 配置文件中的环境变量(可选,不影响恢复结果)
363
- removeEnvFromShell('CC_PROXY_KEY');
364
-
365
- // 同步删除当前进程的环境变量,使恢复立即生效(无需新开终端)
197
+ // auth.json 已恢复,同步删除当前进程的环境变量
366
198
  delete process.env.CC_PROXY_KEY;
367
199
 
368
200
  console.log('Codex settings restored from backup');
@@ -372,161 +204,6 @@ function restoreSettings() {
372
204
  }
373
205
  }
374
206
 
375
- // 获取用户的 shell 配置文件路径
376
- function getShellConfigPath() {
377
- const candidates = getShellConfigCandidates();
378
- const existing = candidates.find(filePath => fs.existsSync(filePath));
379
- return existing || candidates[0];
380
- }
381
-
382
- // 注入环境变量到 shell 配置文件
383
- function injectEnvToShell(envName, envValue) {
384
- const normalizedEnvName = normalizeEnvName(envName);
385
- if (!normalizedEnvName) {
386
- return {
387
- success: false,
388
- error: `Invalid environment variable name: ${envName}`,
389
- isFirstTime: false
390
- };
391
- }
392
-
393
- const configPath = getShellConfigPath();
394
- const marker = `${SHELL_MARKER_PREFIX} [${normalizedEnvName}]`;
395
- const usePowerShell = isPowerShellProfile(configPath);
396
- const exportLine = usePowerShell
397
- ? `$env:${normalizedEnvName} = "${escapePowerShellValue(envValue)}"`
398
- : `export ${normalizedEnvName}="${escapeShellValue(envValue)}"`;
399
-
400
- try {
401
- let content = '';
402
- if (fs.existsSync(configPath)) {
403
- content = fs.readFileSync(configPath, 'utf8');
404
- }
405
-
406
- const envKeyEscaped = String(normalizedEnvName).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
407
- const envLineRegex = usePowerShell
408
- ? new RegExp(`^\\s*\\$env:${envKeyEscaped}\\s*=`, 'i')
409
- : new RegExp(`^\\s*(?:export\\s+)?${envKeyEscaped}=`);
410
-
411
- const originalLines = content ? content.split(/\r?\n/) : [];
412
- const cleanedLines = [];
413
- let existed = false;
414
-
415
- for (let i = 0; i < originalLines.length; i++) {
416
- const currentLine = originalLines[i];
417
- const trimmedLine = currentLine.trim();
418
-
419
- if (trimmedLine === marker) {
420
- const nextLine = originalLines[i + 1] || '';
421
- if (envLineRegex.test(nextLine.trim())) {
422
- i += 1;
423
- }
424
- existed = true;
425
- continue;
426
- }
427
-
428
- if (envLineRegex.test(trimmedLine)) {
429
- existed = true;
430
- continue;
431
- }
432
-
433
- cleanedLines.push(currentLine);
434
- }
435
-
436
- while (cleanedLines.length > 0 && cleanedLines[cleanedLines.length - 1].trim() === '') {
437
- cleanedLines.pop();
438
- }
439
-
440
- if (cleanedLines.length > 0) {
441
- cleanedLines.push('');
442
- }
443
-
444
- cleanedLines.push(marker, exportLine);
445
-
446
- const nextContent = `${cleanedLines.join('\n')}\n`;
447
- if (nextContent !== content) {
448
- writeFileAtomic(configPath, nextContent);
449
- }
450
-
451
- // 同步更新当前进程的环境变量,使变更立即生效(无需新开终端)
452
- process.env[normalizedEnvName] = String(envValue ?? '');
453
-
454
- return { success: true, path: configPath, isFirstTime: !existed };
455
- } catch (err) {
456
- // 不抛出错误,只是警告,因为这不是致命问题
457
- console.warn(`[Codex] Failed to inject env to shell config: ${err.message}`);
458
- return { success: false, error: err.message, isFirstTime: false };
459
- }
460
- }
461
-
462
- // 从 shell 配置文件移除环境变量
463
- function removeEnvFromShell(envName) {
464
- const normalizedEnvName = normalizeEnvName(envName);
465
- if (!normalizedEnvName) {
466
- return {
467
- success: false,
468
- error: `Invalid environment variable name: ${envName}`
469
- };
470
- }
471
-
472
- const configPath = getShellConfigPath();
473
-
474
- try {
475
- if (!fs.existsSync(configPath)) {
476
- return { success: true };
477
- }
478
-
479
- const content = fs.readFileSync(configPath, 'utf8');
480
- const usePowerShell = isPowerShellProfile(configPath);
481
- const marker = `${SHELL_MARKER_PREFIX} [${normalizedEnvName}]`;
482
- const envKeyEscaped = String(normalizedEnvName).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
483
- const envLineRegex = usePowerShell
484
- ? new RegExp(`^\\s*\\$env:${envKeyEscaped}\\s*=`, 'i')
485
- : new RegExp(`^\\s*(?:export\\s+)?${envKeyEscaped}=`);
486
-
487
- const originalLines = content ? content.split(/\r?\n/) : [];
488
- const cleanedLines = [];
489
- let changed = false;
490
-
491
- for (let i = 0; i < originalLines.length; i++) {
492
- const currentLine = originalLines[i];
493
- const trimmedLine = currentLine.trim();
494
-
495
- if (trimmedLine === marker) {
496
- const nextLine = originalLines[i + 1] || '';
497
- if (envLineRegex.test(nextLine.trim())) {
498
- i += 1;
499
- }
500
- changed = true;
501
- continue;
502
- }
503
-
504
- if (envLineRegex.test(trimmedLine)) {
505
- changed = true;
506
- continue;
507
- }
508
-
509
- cleanedLines.push(currentLine);
510
- }
511
-
512
- if (!changed) {
513
- return { success: true };
514
- }
515
-
516
- const normalized = compactBlankLines(cleanedLines);
517
- const nextContent = normalized.length > 0 ? `${normalized.join('\n')}\n` : '';
518
- writeFileAtomic(configPath, nextContent);
519
-
520
- // 同步删除当前进程的环境变量,使变更立即生效(无需新开终端)
521
- delete process.env[normalizedEnvName];
522
-
523
- return { success: true };
524
- } catch (err) {
525
- console.warn(`[Codex] Failed to remove env from shell config: ${err.message}`);
526
- return { success: false, error: err.message };
527
- }
528
- }
529
-
530
207
  // 设置代理配置
531
208
  function setProxyConfig(proxyPort) {
532
209
  try {
@@ -560,24 +237,15 @@ function setProxyConfig(proxyPort) {
560
237
  auth.CC_PROXY_KEY = 'PROXY_KEY';
561
238
  writeAuth(auth);
562
239
 
563
- // 注入环境变量到 shell 配置文件(解决某些系统环境变量优先级问题)
564
- const shellInjectResult = injectEnvToShell('CC_PROXY_KEY', 'PROXY_KEY');
565
-
566
- // 同步更新当前进程的环境变量,使代理立即生效(无需新开终端)
567
- process.env.CC_PROXY_KEY = 'PROXY_KEY';
568
-
569
- // 获取 shell 配置文件路径用于提示信息
570
- const shellConfigPath = shellInjectResult.path || getShellConfigPath();
571
- const sourceCommand = getShellReloadCommand(shellConfigPath);
572
-
240
+ // auth.json 已写入 CC_PROXY_KEY,Codex 优先读取 auth.json,无需注入 shell 配置文件
573
241
  console.log(`Codex settings updated to use proxy on port ${proxyPort}`);
574
242
  return {
575
243
  success: true,
576
244
  port: proxyPort,
577
- envInjected: shellInjectResult.success,
578
- isFirstTime: shellInjectResult.isFirstTime,
579
- shellConfigPath: shellConfigPath,
580
- sourceCommand: sourceCommand
245
+ envInjected: true,
246
+ isFirstTime: false,
247
+ shellConfigPath: null,
248
+ sourceCommand: null
581
249
  };
582
250
  } catch (err) {
583
251
  throw new Error('Failed to set proxy config: ' + err.message);
@@ -652,8 +320,4 @@ module.exports = {
652
320
  setProxyConfig,
653
321
  isProxyConfig,
654
322
  getCurrentProxyPort,
655
- // 导出环境变量注入函数供其他模块使用
656
- getShellConfigPath,
657
- injectEnvToShell,
658
- removeEnvFromShell
659
323
  };
@@ -16,7 +16,7 @@ const { CommandsService } = require('./commands-service');
16
16
  const { SkillService } = require('./skill-service');
17
17
  const { PATHS, NATIVE_PATHS } = require('../../config/paths');
18
18
 
19
- const CONFIG_VERSION = '1.3.0';
19
+ const CONFIG_VERSION = '1.4.0';
20
20
  const SKILL_FILE_ENCODING = 'base64';
21
21
  const SKILL_IGNORE_DIRS = new Set(['.git']);
22
22
  const SKILL_IGNORE_FILES = new Set(['.DS_Store']);
@@ -206,6 +206,7 @@ function buildExportReadme(exportData) {
206
206
  - Agents / Skills / Commands
207
207
  - 插件 (Plugins)
208
208
  - MCP 服务器配置
209
+ - OAuth 凭证管理池
209
210
  - 各平台原生配置(Claude / Codex / Gemini / OpenCode)
210
211
  - UI 配置(主题、面板显示、排序等)
211
212
  - Prompts 预设
@@ -857,9 +858,9 @@ function exportAllConfigs() {
857
858
  // 获取 Plugins 配置
858
859
  const plugins = exportPluginsSnapshot();
859
860
  const nativeConfigs = exportNativeConfigs();
861
+ const oauthCredentials = readJsonFileSafe(PATHS.oauthCredentials);
860
862
 
861
863
  // 读取 Markdown 配置文件
862
- const { PATHS } = require('../../config/paths');
863
864
  const markdownFiles = {};
864
865
  const mdFileNames = ['CLAUDE.md', 'AGENTS.md', 'GEMINI.md'];
865
866
 
@@ -914,6 +915,7 @@ function exportAllConfigs() {
914
915
  security: security,
915
916
  appConfig: appConfig,
916
917
  nativeConfigs,
918
+ oauthCredentials,
917
919
  claudeHooks: claudeHooks
918
920
  }
919
921
  };
@@ -979,6 +981,7 @@ async function importConfigs(importData, options = {}) {
979
981
  security: { success: 0, failed: 0, skipped: 0 },
980
982
  appConfig: { success: 0, failed: 0, skipped: 0 },
981
983
  nativeConfigs: { success: 0, failed: 0, skipped: 0 },
984
+ oauthCredentials: { success: 0, failed: 0, skipped: 0 },
982
985
  claudeHooks: { success: 0, failed: 0, skipped: 0 }
983
986
  };
984
987
 
@@ -1007,6 +1010,7 @@ async function importConfigs(importData, options = {}) {
1007
1010
  security = null,
1008
1011
  appConfig = null,
1009
1012
  nativeConfigs = {},
1013
+ oauthCredentials = null,
1010
1014
  claudeHooks = null
1011
1015
  } = importData.data;
1012
1016
 
@@ -1550,6 +1554,22 @@ async function importConfigs(importData, options = {}) {
1550
1554
  }
1551
1555
  }
1552
1556
 
1557
+ if (oauthCredentials && typeof oauthCredentials === 'object') {
1558
+ try {
1559
+ const status = writeJsonFileAbsolute(PATHS.oauthCredentials, oauthCredentials, overwrite, { mode: 0o600 });
1560
+ if (status === 'success') {
1561
+ results.oauthCredentials.success++;
1562
+ } else if (status === 'skipped') {
1563
+ results.oauthCredentials.skipped++;
1564
+ } else {
1565
+ results.oauthCredentials.failed++;
1566
+ }
1567
+ } catch (err) {
1568
+ console.error('[ConfigImport] 导入 OAuth 凭证失败:', err);
1569
+ results.oauthCredentials.failed++;
1570
+ }
1571
+ }
1572
+
1553
1573
  // 导入 Claude Hooks 配置
1554
1574
  if (claudeHooks && typeof claudeHooks === 'object') {
1555
1575
  let didWrite = false;
@@ -1642,6 +1662,7 @@ function generateImportSummary(results) {
1642
1662
  { key: 'security', label: '安全配置' },
1643
1663
  { key: 'appConfig', label: '高级配置' },
1644
1664
  { key: 'nativeConfigs', label: '原生配置' },
1665
+ { key: 'oauthCredentials', label: 'OAuth凭证' },
1645
1666
  { key: 'claudeHooks', label: 'Claude Hooks' }
1646
1667
  ];
1647
1668
 
@@ -2,6 +2,7 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const crypto = require('crypto');
4
4
  const { PATHS, NATIVE_PATHS } = require('../../config/paths');
5
+ const { clearNativeOAuth } = require('./native-oauth-adapters');
5
6
 
6
7
  /**
7
8
  * Gemini 渠道管理服务(多渠道架构)
@@ -252,14 +253,6 @@ function updateChannel(channelId, updates) {
252
253
  console.log(`[Gemini Single-channel mode] Enabled "${nextChannel.name}", disabled all others`);
253
254
  }
254
255
 
255
- // Prevent disabling last enabled channel when proxy is OFF
256
- if (!isProxyRunning && !nextChannel.enabled && oldChannel.enabled) {
257
- const enabledCount = data.channels.filter(ch => ch.enabled).length;
258
- if (enabledCount === 0) {
259
- throw new Error('无法禁用最后一个启用的渠道。请先启用其他渠道或启动动态切换。');
260
- }
261
- }
262
-
263
256
  saveChannels(data);
264
257
 
265
258
  // Only sync .env when proxy is OFF.
@@ -299,6 +292,8 @@ function applyChannelToSettings(channelId, channels = null) {
299
292
  saveChannels(data);
300
293
  }
301
294
 
295
+ clearNativeOAuth('gemini');
296
+
302
297
  const geminiDir = getGeminiDir();
303
298
 
304
299
  if (!fs.existsSync(geminiDir)) {
@@ -455,6 +450,12 @@ function saveChannelOrder(order) {
455
450
  saveChannels(data);
456
451
  }
457
452
 
453
+ function disableAllChannels() {
454
+ const data = loadChannels();
455
+ data.channels.forEach(ch => { ch.enabled = false; });
456
+ saveChannels(data);
457
+ }
458
+
458
459
  module.exports = {
459
460
  getChannels,
460
461
  createChannel,
@@ -465,5 +466,6 @@ module.exports = {
465
466
  saveChannelOrder,
466
467
  isProxyConfig,
467
468
  getGeminiDir,
468
- applyChannelToSettings
469
+ applyChannelToSettings,
470
+ disableAllChannels
469
471
  };
@@ -290,15 +290,16 @@ function writeJsonFile(filePath, data) {
290
290
  * 安全读取 TOML 文件
291
291
  */
292
292
  function readTomlFile(filePath, defaultValue = {}) {
293
+ if (!fs.existsSync(filePath)) {
294
+ return defaultValue;
295
+ }
296
+
293
297
  try {
294
- if (fs.existsSync(filePath)) {
295
- const content = fs.readFileSync(filePath, 'utf-8');
296
- return toml.parse(content);
297
- }
298
+ const content = fs.readFileSync(filePath, 'utf-8');
299
+ return toml.parse(content);
298
300
  } catch (err) {
299
- console.error(`[MCP] Failed to read ${filePath}:`, err.message);
301
+ throw new Error(`Failed to parse ${filePath}: ${err.message}`);
300
302
  }
301
- return defaultValue;
302
303
  }
303
304
 
304
305
  /**
@@ -593,14 +594,14 @@ async function saveServer(server, options = {}) {
593
594
  server.apps = normalizeServerApps(server.apps, previousApps || DEFAULT_SERVER_APPS);
594
595
  }
595
596
 
596
- servers[server.id] = server;
597
- writeJsonFile(MCP_SERVERS_FILE, servers);
598
-
599
597
  // 同步到各平台配置
600
598
  if (syncPlatforms) {
601
599
  await syncServerToAllPlatforms(server, previousApps);
602
600
  }
603
601
 
602
+ servers[server.id] = server;
603
+ writeJsonFile(MCP_SERVERS_FILE, servers);
604
+
604
605
  return server;
605
606
  }
606
607
 
@@ -615,12 +616,12 @@ async function deleteServer(id) {
615
616
  return false;
616
617
  }
617
618
 
618
- delete servers[id];
619
- writeJsonFile(MCP_SERVERS_FILE, servers);
620
-
621
619
  // 从所有平台配置中移除
622
620
  await removeServerFromAllPlatforms(id);
623
621
 
622
+ delete servers[id];
623
+ writeJsonFile(MCP_SERVERS_FILE, servers);
624
+
624
625
  return true;
625
626
  }
626
627
 
@@ -642,8 +643,6 @@ async function toggleServerApp(serverId, app, enabled) {
642
643
  server.apps[app] = enabled;
643
644
  server.updatedAt = Date.now();
644
645
 
645
- writeJsonFile(MCP_SERVERS_FILE, servers);
646
-
647
646
  // 同步到对应平台
648
647
  if (enabled) {
649
648
  await syncServerToPlatform(server, app);
@@ -651,6 +650,8 @@ async function toggleServerApp(serverId, app, enabled) {
651
650
  await removeServerFromPlatform(serverId, app);
652
651
  }
653
652
 
653
+ writeJsonFile(MCP_SERVERS_FILE, servers);
654
+
654
655
  return server;
655
656
  }
656
657
 
@@ -790,6 +791,7 @@ async function removeServerFromPlatform(serverId, platform) {
790
791
  console.log(`[MCP] Removed "${serverId}" from ${platform}`);
791
792
  } catch (err) {
792
793
  console.error(`[MCP] Failed to remove "${serverId}" from ${platform}:`, err.message);
794
+ throw err;
793
795
  }
794
796
  }
795
797
 
@@ -833,14 +835,22 @@ function removeFromClaudeConfig(serverId) {
833
835
  * 同步到 Codex 配置
834
836
  */
835
837
  function syncToCodexConfig(server) {
838
+ if (!fs.existsSync(CODEX_CONFIG_PATH)) {
839
+ throw new Error('Codex config.toml not found. Please run Codex CLI at least once before syncing MCP servers.');
840
+ }
841
+
836
842
  const config = readTomlFile(CODEX_CONFIG_PATH, {});
843
+ const nextSpec = convertToCodexFormat(server.server);
837
844
 
838
845
  if (!config.mcp_servers) {
839
846
  config.mcp_servers = {};
840
847
  }
841
848
 
842
- // 转换为 Codex TOML 格式
843
- config.mcp_servers[server.id] = convertToCodexFormat(server.server);
849
+ if (JSON.stringify(config.mcp_servers[server.id] || null) === JSON.stringify(nextSpec)) {
850
+ return;
851
+ }
852
+
853
+ config.mcp_servers[server.id] = nextSpec;
844
854
 
845
855
  writeTomlFile(CODEX_CONFIG_PATH, config);
846
856
  }
@@ -849,10 +859,17 @@ function syncToCodexConfig(server) {
849
859
  * 从 Codex 配置移除
850
860
  */
851
861
  function removeFromCodexConfig(serverId) {
862
+ if (!fs.existsSync(CODEX_CONFIG_PATH)) {
863
+ return;
864
+ }
865
+
852
866
  const config = readTomlFile(CODEX_CONFIG_PATH, {});
853
867
 
854
868
  if (config.mcp_servers && config.mcp_servers[serverId]) {
855
869
  delete config.mcp_servers[serverId];
870
+ if (Object.keys(config.mcp_servers).length === 0) {
871
+ delete config.mcp_servers;
872
+ }
856
873
  writeTomlFile(CODEX_CONFIG_PATH, config);
857
874
  }
858
875
  }