@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.
- package/CHANGELOG.md +15 -18
- package/README.md +8 -8
- package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
- package/dist/web/assets/ConfigTemplates-ZrK_s7ma.js +1 -0
- package/dist/web/assets/Home-B8YfhZ3c.js +1 -0
- package/dist/web/assets/Home-Di2qsylF.css +1 -0
- package/dist/web/assets/PluginManager-BD7QUZbU.js +1 -0
- package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
- package/dist/web/assets/ProjectList-C1fQb9OW.css +1 -0
- package/dist/web/assets/ProjectList-DRb1DuHV.js +1 -0
- package/dist/web/assets/SessionList-BGJWyneI.css +1 -0
- package/dist/web/assets/SessionList-lZ0LKzfT.js +1 -0
- package/dist/web/assets/SkillManager-C1xG5B4Q.js +1 -0
- package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
- package/dist/web/assets/Terminal-DGNJeVtc.css +1 -0
- package/dist/web/assets/Terminal-DksBo_lM.js +1 -0
- package/dist/web/assets/WorkspaceManager-Burx7XOo.js +1 -0
- package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
- package/dist/web/assets/icons-kcfLIMBB.js +1 -0
- package/dist/web/assets/index-Ufv5rCa5.css +1 -0
- package/dist/web/assets/index-lAkrRC3h.js +2 -0
- package/dist/web/assets/markdown-BfC0goYb.css +10 -0
- package/dist/web/assets/markdown-C9MYpaSi.js +1 -0
- package/dist/web/assets/naive-ui-CSrLusZZ.js +1 -0
- package/dist/web/assets/{vendors-D2HHw_aW.js → vendors-CO3Upi1d.js} +2 -2
- package/dist/web/assets/vue-vendor-DqyWIXEb.js +45 -0
- package/dist/web/assets/xterm-6GBZ9nXN.css +32 -0
- package/dist/web/assets/xterm-BJzAjXCH.js +13 -0
- package/dist/web/index.html +8 -6
- package/package.json +4 -2
- package/src/commands/channels.js +48 -1
- package/src/commands/cli-type.js +4 -2
- package/src/commands/daemon.js +81 -12
- package/src/commands/doctor.js +10 -9
- package/src/commands/list.js +1 -1
- package/src/commands/logs.js +6 -4
- package/src/commands/port-config.js +24 -4
- package/src/commands/proxy-control.js +12 -6
- package/src/commands/search.js +1 -1
- package/src/commands/security.js +3 -2
- package/src/commands/stats.js +226 -52
- package/src/commands/switch.js +1 -1
- package/src/commands/toggle-proxy.js +31 -6
- package/src/commands/update.js +97 -0
- package/src/commands/workspace.js +1 -1
- package/src/config/default.js +39 -2
- package/src/config/loader.js +74 -8
- package/src/config/paths.js +105 -33
- package/src/index.js +64 -3
- package/src/plugins/constants.js +3 -2
- package/src/plugins/plugin-api.js +1 -1
- package/src/reset-config.js +4 -2
- package/src/server/api/agents.js +57 -14
- package/src/server/api/channels.js +112 -33
- package/src/server/api/codex-channels.js +111 -18
- package/src/server/api/codex-proxy.js +14 -8
- package/src/server/api/commands.js +71 -18
- package/src/server/api/config-export.js +0 -6
- package/src/server/api/config-registry.js +11 -3
- package/src/server/api/config.js +376 -5
- package/src/server/api/convert.js +133 -0
- package/src/server/api/dashboard.js +22 -6
- package/src/server/api/gemini-channels.js +107 -18
- package/src/server/api/gemini-proxy.js +14 -8
- package/src/server/api/gemini-sessions.js +1 -1
- package/src/server/api/health-check.js +4 -3
- package/src/server/api/mcp.js +3 -3
- package/src/server/api/opencode-channels.js +419 -0
- package/src/server/api/opencode-projects.js +99 -0
- package/src/server/api/opencode-proxy.js +198 -0
- package/src/server/api/opencode-sessions.js +403 -0
- package/src/server/api/opencode-statistics.js +57 -0
- package/src/server/api/plugins.js +66 -19
- package/src/server/api/prompts.js +2 -2
- package/src/server/api/proxy.js +7 -4
- package/src/server/api/sessions.js +3 -0
- package/src/server/api/skills.js +69 -18
- package/src/server/api/workspaces.js +78 -6
- package/src/server/codex-proxy-server.js +30 -18
- package/src/server/dev-server.js +1 -1
- package/src/server/gemini-proxy-server.js +15 -3
- package/src/server/index.js +165 -58
- package/src/server/opencode-proxy-server.js +4375 -0
- package/src/server/proxy-server.js +27 -18
- package/src/server/services/agents-service.js +61 -24
- package/src/server/services/channel-scheduler.js +9 -5
- package/src/server/services/channels.js +64 -37
- package/src/server/services/codex-channels.js +56 -43
- package/src/server/services/codex-settings-manager.js +271 -49
- package/src/server/services/codex-statistics-service.js +2 -2
- package/src/server/services/commands-service.js +84 -25
- package/src/server/services/config-export-service.js +7 -45
- package/src/server/services/config-registry-service.js +63 -17
- package/src/server/services/config-sync-manager.js +160 -7
- package/src/server/services/config-templates-service.js +204 -51
- package/src/server/services/env-checker.js +26 -12
- package/src/server/services/env-manager.js +126 -18
- package/src/server/services/favorites.js +5 -3
- package/src/server/services/gemini-channels.js +33 -44
- package/src/server/services/gemini-statistics-service.js +2 -2
- package/src/server/services/mcp-service.js +350 -9
- package/src/server/services/model-detector.js +707 -221
- package/src/server/services/network-access.js +80 -0
- package/src/server/services/opencode-channels.js +206 -0
- package/src/server/services/opencode-gateway-converter.js +639 -0
- package/src/server/services/opencode-sessions.js +663 -0
- package/src/server/services/opencode-settings-manager.js +342 -0
- package/src/server/services/opencode-statistics-service.js +255 -0
- package/src/server/services/plugins-service.js +479 -22
- package/src/server/services/prompts-service.js +53 -11
- package/src/server/services/proxy-runtime.js +1 -1
- package/src/server/services/repo-scanner-base.js +1 -1
- package/src/server/services/security-config.js +1 -1
- package/src/server/services/session-cache.js +1 -1
- package/src/server/services/skill-service.js +300 -46
- package/src/server/services/speed-test.js +464 -186
- package/src/server/services/statistics-service.js +2 -2
- package/src/server/services/terminal-commands.js +10 -3
- package/src/server/services/terminal-config.js +1 -1
- package/src/server/services/ui-config.js +1 -1
- package/src/server/services/workspace-service.js +57 -100
- package/src/server/websocket-server.js +132 -3
- package/src/ui/menu.js +49 -40
- package/src/utils/port-helper.js +22 -8
- package/src/utils/session.js +5 -4
- package/dist/web/assets/icons-CO_2OFES.js +0 -1
- package/dist/web/assets/index-DI8QOi-E.js +0 -14
- package/dist/web/assets/index-uLHGdeZh.css +0 -41
- package/dist/web/assets/naive-ui-B1re3c-e.js +0 -1
- package/dist/web/assets/vue-vendor-6JaYHOiI.js +0 -44
- package/src/server/api/oauth.js +0 -294
- package/src/server/api/permissions.js +0 -385
- package/src/server/config/oauth-providers.js +0 -68
- package/src/server/services/oauth-callback-server.js +0 -284
- package/src/server/services/oauth-service.js +0 -378
- package/src/server/services/oauth-token-storage.js +0 -135
- 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(), '.
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
280
|
-
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
186
|
-
|
|
187
|
-
|
|
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
|
|
205
|
-
|
|
206
|
-
const
|
|
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
|
|
216
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
);
|
|
472
|
+
if (envLineRegex.test(trimmedLine)) {
|
|
473
|
+
changed = true;
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
254
476
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
new RegExp(`^export ${envName}=.*\\n?`, 'gm'),
|
|
258
|
-
''
|
|
259
|
-
);
|
|
477
|
+
cleanedLines.push(currentLine);
|
|
478
|
+
}
|
|
260
479
|
|
|
261
|
-
|
|
262
|
-
|
|
480
|
+
if (!changed) {
|
|
481
|
+
return { success: true };
|
|
482
|
+
}
|
|
263
483
|
|
|
264
|
-
|
|
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 =
|
|
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')
|
|
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
|
-
* ~/.
|
|
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(), '.
|
|
18
|
+
const dir = path.join(os.homedir(), '.cc-tool');
|
|
19
19
|
if (!fs.existsSync(dir)) {
|
|
20
20
|
fs.mkdirSync(dir, { recursive: true });
|
|
21
21
|
}
|