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.
- package/CHANGELOG.md +5 -0
- package/dist/web/assets/{Analytics-IW6eAy9u.js → Analytics-DLpoDZ2M.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-BPtkTMSc.js → ConfigTemplates-D_hRb55W.js} +1 -1
- package/dist/web/assets/Home-BMoFdAwy.css +1 -0
- package/dist/web/assets/Home-DNwp-0J-.js +1 -0
- package/dist/web/assets/{PluginManager-BGx9MSDV.js → PluginManager-JXsyym1s.js} +1 -1
- package/dist/web/assets/{ProjectList-BCn-mrCx.js → ProjectList-DZWSeb-q.js} +1 -1
- package/dist/web/assets/{SessionList-CzLfebJQ.js → SessionList-Cs624DR3.js} +1 -1
- package/dist/web/assets/{SkillManager-CXz2vBQx.js → SkillManager-bEliz7qz.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-CHtgMfKc.js → WorkspaceManager-J3RecFGn.js} +1 -1
- package/dist/web/assets/{icons-B29onFfZ.js → icons-Cuc23WS7.js} +1 -1
- package/dist/web/assets/index-BXeSvAwU.js +2 -0
- package/dist/web/assets/index-DWAC3Tdv.css +1 -0
- package/dist/web/index.html +3 -3
- package/package.json +3 -2
- package/src/commands/toggle-proxy.js +100 -5
- package/src/config/paths.js +102 -19
- package/src/server/api/channels.js +9 -0
- package/src/server/api/codex-channels.js +9 -0
- package/src/server/api/codex-proxy.js +22 -11
- package/src/server/api/gemini-proxy.js +22 -11
- package/src/server/api/oauth-credentials.js +163 -0
- package/src/server/api/opencode-proxy.js +22 -10
- package/src/server/api/plugins.js +3 -1
- package/src/server/api/proxy.js +39 -44
- package/src/server/api/skills.js +91 -13
- package/src/server/codex-proxy-server.js +1 -11
- package/src/server/index.js +1 -0
- package/src/server/services/channels.js +18 -22
- package/src/server/services/codex-channels.js +124 -175
- package/src/server/services/codex-config.js +2 -5
- package/src/server/services/codex-settings-manager.js +12 -348
- package/src/server/services/config-export-service.js +23 -2
- package/src/server/services/gemini-channels.js +11 -9
- package/src/server/services/mcp-service.js +33 -16
- package/src/server/services/native-keychain.js +243 -0
- package/src/server/services/native-oauth-adapters.js +890 -0
- package/src/server/services/oauth-credentials-service.js +786 -0
- package/src/server/services/oauth-utils.js +49 -0
- package/src/server/services/opencode-channels.js +13 -9
- package/src/server/services/opencode-settings-manager.js +169 -16
- package/src/server/services/plugins-service.js +22 -1
- package/src/server/services/settings-manager.js +13 -0
- package/src/server/services/skill-service.js +712 -332
- package/dist/web/assets/Home-BsSioaaB.css +0 -1
- package/dist/web/assets/Home-obifg_9E.js +0 -1
- package/dist/web/assets/index-C7LPdVsN.js +0 -2
- 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
|
|
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
|
|
13
|
+
return NATIVE_PATHS.codex.config;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
function getAuthPath() {
|
|
16
|
-
return
|
|
17
|
+
return NATIVE_PATHS.codex.auth;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
// 备份文件路径
|
|
20
21
|
function getConfigBackupPath() {
|
|
21
|
-
return
|
|
22
|
+
return NATIVE_PATHS.codex.configBackup;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
function getAuthBackupPath() {
|
|
25
|
-
return
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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:
|
|
578
|
-
isFirstTime:
|
|
579
|
-
shellConfigPath:
|
|
580
|
-
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.
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
return toml.parse(content);
|
|
297
|
-
}
|
|
298
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
299
|
+
return toml.parse(content);
|
|
298
300
|
} catch (err) {
|
|
299
|
-
|
|
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
|
-
|
|
843
|
-
|
|
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
|
}
|