@adversity/coding-tool-x 3.0.6 → 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 +38 -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 +92 -13
- 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/ui.js +8 -1
- 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 +67 -4
- 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 +32 -19
- package/src/server/dev-server.js +1 -1
- package/src/server/gemini-proxy-server.js +17 -3
- package/src/server/index.js +164 -48
- package/src/server/opencode-proxy-server.js +4375 -0
- package/src/server/proxy-server.js +30 -19
- package/src/server/services/agents-service.js +61 -24
- package/src/server/services/channel-scheduler.js +9 -5
- package/src/server/services/channels.js +70 -12
- package/src/server/services/codex-channels.js +61 -23
- 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 +37 -15
- 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-BxudHPiX.js +0 -1
- package/dist/web/assets/index-D2VfwJBa.js +0 -14
- package/dist/web/assets/index-oXBzu0bd.css +0 -41
- package/dist/web/assets/naive-ui-DT-Uur8K.js +0 -1
- package/dist/web/assets/vue-vendor-6JaYHOiI.js +0 -44
- package/src/server/api/permissions.js +0 -385
- package/src/server/services/permission-templates-service.js +0 -308
|
@@ -13,6 +13,7 @@ const { recordRequest } = require('./services/statistics-service');
|
|
|
13
13
|
const { saveProxyStartTime, clearProxyStartTime, getProxyStartTime, getProxyRuntime } = require('./services/proxy-runtime');
|
|
14
14
|
const eventBus = require('../plugins/event-bus');
|
|
15
15
|
const { CLAUDE_MODEL_PRICING } = require('../config/model-pricing');
|
|
16
|
+
const { getEffectiveApiKey } = require('./services/channels');
|
|
16
17
|
|
|
17
18
|
let proxyServer = null;
|
|
18
19
|
let proxyApp = null;
|
|
@@ -159,7 +160,7 @@ async function startProxyServer(options = {}) {
|
|
|
159
160
|
|
|
160
161
|
try {
|
|
161
162
|
const config = loadConfig();
|
|
162
|
-
const port = config.ports?.proxy ||
|
|
163
|
+
const port = config.ports?.proxy || 20088;
|
|
163
164
|
currentPort = port;
|
|
164
165
|
|
|
165
166
|
proxyApp = express();
|
|
@@ -185,9 +186,10 @@ async function startProxyServer(options = {}) {
|
|
|
185
186
|
});
|
|
186
187
|
|
|
187
188
|
proxyReq.removeHeader('x-api-key');
|
|
188
|
-
|
|
189
|
+
const effectiveKey = req.effectiveApiKey;
|
|
190
|
+
proxyReq.setHeader('x-api-key', effectiveKey);
|
|
189
191
|
proxyReq.removeHeader('authorization');
|
|
190
|
-
proxyReq.setHeader('authorization', `Bearer ${
|
|
192
|
+
proxyReq.setHeader('authorization', `Bearer ${effectiveKey}`);
|
|
191
193
|
|
|
192
194
|
if (!proxyReq.getHeader('anthropic-version')) {
|
|
193
195
|
proxyReq.setHeader('anthropic-version', '2023-06-01');
|
|
@@ -219,6 +221,30 @@ async function startProxyServer(options = {}) {
|
|
|
219
221
|
|
|
220
222
|
req.selectedChannel = channel;
|
|
221
223
|
req.sessionId = sessionId || null;
|
|
224
|
+
let released = false;
|
|
225
|
+
|
|
226
|
+
const release = () => {
|
|
227
|
+
if (released) return;
|
|
228
|
+
released = true;
|
|
229
|
+
releaseChannel(channel.id, 'claude');
|
|
230
|
+
// 广播调度状态(请求结束)
|
|
231
|
+
broadcastSchedulerState('claude', getSchedulerState('claude'));
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
req.__releaseChannel = release;
|
|
235
|
+
|
|
236
|
+
res.on('close', release);
|
|
237
|
+
res.on('error', release);
|
|
238
|
+
|
|
239
|
+
const effectiveKey = getEffectiveApiKey(channel);
|
|
240
|
+
if (!effectiveKey) {
|
|
241
|
+
release();
|
|
242
|
+
return res.status(401).json({
|
|
243
|
+
error: 'API key not configured or expired. Please update your channel key.',
|
|
244
|
+
type: 'authentication_error'
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
req.effectiveApiKey = effectiveKey;
|
|
222
248
|
|
|
223
249
|
// 应用模型重定向(当 proxy 开启时)
|
|
224
250
|
if (req.body && req.body.model) {
|
|
@@ -240,21 +266,6 @@ async function startProxyServer(options = {}) {
|
|
|
240
266
|
}
|
|
241
267
|
}
|
|
242
268
|
|
|
243
|
-
let released = false;
|
|
244
|
-
|
|
245
|
-
const release = () => {
|
|
246
|
-
if (released) return;
|
|
247
|
-
released = true;
|
|
248
|
-
releaseChannel(channel.id, 'claude');
|
|
249
|
-
// 广播调度状态(请求结束)
|
|
250
|
-
broadcastSchedulerState('claude', getSchedulerState('claude'));
|
|
251
|
-
};
|
|
252
|
-
|
|
253
|
-
req.__releaseChannel = release;
|
|
254
|
-
|
|
255
|
-
res.on('close', release);
|
|
256
|
-
res.on('error', release);
|
|
257
|
-
|
|
258
269
|
const proxyOptions = {
|
|
259
270
|
target: channel.baseUrl,
|
|
260
271
|
changeOrigin: true,
|
|
@@ -531,7 +542,7 @@ function getProxyStatus() {
|
|
|
531
542
|
return {
|
|
532
543
|
running: !!proxyServer,
|
|
533
544
|
port: currentPort,
|
|
534
|
-
defaultPort: config.ports?.proxy ||
|
|
545
|
+
defaultPort: config.ports?.proxy || 20088,
|
|
535
546
|
startTime,
|
|
536
547
|
runtime
|
|
537
548
|
};
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Agents 服务
|
|
3
3
|
*
|
|
4
|
-
* 管理 Claude
|
|
5
|
-
* 代理目录:
|
|
6
|
-
* - 用户级: ~/.claude/agents/
|
|
7
|
-
* - 项目级: .claude/agents/
|
|
8
|
-
*
|
|
4
|
+
* 管理 Claude/OpenCode 自定义代理的 CRUD 操作
|
|
9
5
|
* 支持从 GitHub 仓库扫描和安装代理
|
|
10
6
|
*/
|
|
11
7
|
|
|
@@ -13,12 +9,37 @@ const fs = require('fs');
|
|
|
13
9
|
const path = require('path');
|
|
14
10
|
const os = require('os');
|
|
15
11
|
const { RepoScannerBase } = require('./repo-scanner-base');
|
|
16
|
-
|
|
17
|
-
// 代理目录路径
|
|
18
|
-
const USER_AGENTS_DIR = path.join(os.homedir(), '.claude', 'agents');
|
|
12
|
+
const { NATIVE_PATHS } = require('../../config/paths');
|
|
19
13
|
|
|
20
14
|
// 默认仓库源
|
|
21
15
|
const DEFAULT_REPOS = [];
|
|
16
|
+
const SUPPORTED_PLATFORMS = ['claude', 'opencode'];
|
|
17
|
+
const OPENCODE_CONFIG_DIR = NATIVE_PATHS.opencode.config;
|
|
18
|
+
|
|
19
|
+
const PLATFORM_CONFIG = {
|
|
20
|
+
claude: {
|
|
21
|
+
userAgentsDir: path.join(os.homedir(), '.claude', 'agents'),
|
|
22
|
+
projectAgentsDir: (projectPath) => path.join(projectPath, '.claude', 'agents'),
|
|
23
|
+
repoType: 'agents'
|
|
24
|
+
},
|
|
25
|
+
opencode: {
|
|
26
|
+
userAgentsDir: path.join(OPENCODE_CONFIG_DIR, 'agents'),
|
|
27
|
+
legacyUserAgentsDir: path.join(OPENCODE_CONFIG_DIR, 'agent'),
|
|
28
|
+
projectAgentsDir: (projectPath) => {
|
|
29
|
+
const modern = path.join(projectPath, '.opencode', 'agents');
|
|
30
|
+
const legacy = path.join(projectPath, '.opencode', 'agent');
|
|
31
|
+
if (fs.existsSync(legacy) && !fs.existsSync(modern)) {
|
|
32
|
+
return legacy;
|
|
33
|
+
}
|
|
34
|
+
return modern;
|
|
35
|
+
},
|
|
36
|
+
repoType: 'opencode-agents'
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function normalizePlatform(platform) {
|
|
41
|
+
return SUPPORTED_PLATFORMS.includes(platform) ? platform : 'claude';
|
|
42
|
+
}
|
|
22
43
|
|
|
23
44
|
/**
|
|
24
45
|
* 确保目录存在
|
|
@@ -74,11 +95,11 @@ function parseFrontmatter(content) {
|
|
|
74
95
|
/**
|
|
75
96
|
* 生成 frontmatter 字符串
|
|
76
97
|
*/
|
|
77
|
-
function generateFrontmatter(data) {
|
|
98
|
+
function generateFrontmatter(data, platform = 'claude') {
|
|
78
99
|
const lines = ['---'];
|
|
79
100
|
|
|
80
|
-
//
|
|
81
|
-
if (data.name) {
|
|
101
|
+
// Claude 下写入 name,OpenCode 以文件名作为 agent id
|
|
102
|
+
if (platform !== 'opencode' && data.name) {
|
|
82
103
|
lines.push(`name: ${data.name}`);
|
|
83
104
|
}
|
|
84
105
|
if (data.description) {
|
|
@@ -164,10 +185,10 @@ function scanAgentsDir(dir, basePath, scope) {
|
|
|
164
185
|
* Agents 仓库扫描器
|
|
165
186
|
*/
|
|
166
187
|
class AgentsRepoScanner extends RepoScannerBase {
|
|
167
|
-
constructor() {
|
|
188
|
+
constructor(platform, installDir) {
|
|
168
189
|
super({
|
|
169
|
-
type: 'agents',
|
|
170
|
-
installDir
|
|
190
|
+
type: PLATFORM_CONFIG[platform]?.repoType || 'agents',
|
|
191
|
+
installDir,
|
|
171
192
|
markerFile: null, // 直接扫描 .md 文件
|
|
172
193
|
fileExtension: '.md',
|
|
173
194
|
defaultRepos: DEFAULT_REPOS
|
|
@@ -248,12 +269,28 @@ class AgentsRepoScanner extends RepoScannerBase {
|
|
|
248
269
|
* Agents 服务类
|
|
249
270
|
*/
|
|
250
271
|
class AgentsService {
|
|
251
|
-
constructor() {
|
|
252
|
-
this.
|
|
253
|
-
|
|
272
|
+
constructor(platform = 'claude') {
|
|
273
|
+
this.platform = normalizePlatform(platform);
|
|
274
|
+
const config = PLATFORM_CONFIG[this.platform];
|
|
275
|
+
|
|
276
|
+
this.userAgentsDir = config.userAgentsDir;
|
|
277
|
+
if (this.platform === 'opencode') {
|
|
278
|
+
const legacyUserDir = config.legacyUserAgentsDir;
|
|
279
|
+
if (legacyUserDir && fs.existsSync(legacyUserDir) && !fs.existsSync(this.userAgentsDir)) {
|
|
280
|
+
this.userAgentsDir = legacyUserDir;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
this.projectAgentsDir = config.projectAgentsDir;
|
|
285
|
+
this.repoScanner = new AgentsRepoScanner(this.platform, this.userAgentsDir);
|
|
254
286
|
ensureDir(this.userAgentsDir);
|
|
255
287
|
}
|
|
256
288
|
|
|
289
|
+
getProjectAgentsDir(projectPath) {
|
|
290
|
+
if (!projectPath) return null;
|
|
291
|
+
return this.projectAgentsDir(projectPath);
|
|
292
|
+
}
|
|
293
|
+
|
|
257
294
|
/**
|
|
258
295
|
* 获取所有代理列表
|
|
259
296
|
* @param {string} projectPath - 项目路径(可选,用于获取项目级代理)
|
|
@@ -267,7 +304,7 @@ class AgentsService {
|
|
|
267
304
|
|
|
268
305
|
// 获取项目级代理(如果提供了项目路径)
|
|
269
306
|
if (projectPath) {
|
|
270
|
-
const projectAgentsDir =
|
|
307
|
+
const projectAgentsDir = this.getProjectAgentsDir(projectPath);
|
|
271
308
|
const projectAgents = scanAgentsDir(projectAgentsDir, projectAgentsDir, 'project');
|
|
272
309
|
agents.push(...projectAgents);
|
|
273
310
|
}
|
|
@@ -331,7 +368,7 @@ class AgentsService {
|
|
|
331
368
|
getAgent(fileName, scope, projectPath = null) {
|
|
332
369
|
const baseDir = scope === 'user'
|
|
333
370
|
? this.userAgentsDir
|
|
334
|
-
:
|
|
371
|
+
: this.getProjectAgentsDir(projectPath);
|
|
335
372
|
|
|
336
373
|
const filePath = path.join(baseDir, `${fileName}.md`);
|
|
337
374
|
|
|
@@ -382,7 +419,7 @@ class AgentsService {
|
|
|
382
419
|
|
|
383
420
|
const baseDir = scope === 'user'
|
|
384
421
|
? this.userAgentsDir
|
|
385
|
-
:
|
|
422
|
+
: this.getProjectAgentsDir(projectPath);
|
|
386
423
|
|
|
387
424
|
ensureDir(baseDir);
|
|
388
425
|
|
|
@@ -400,7 +437,7 @@ class AgentsService {
|
|
|
400
437
|
if (permissionMode) frontmatterData.permissionMode = permissionMode;
|
|
401
438
|
if (skills) frontmatterData.skills = skills;
|
|
402
439
|
|
|
403
|
-
const content = generateFrontmatter(frontmatterData) + '\n\n' + (systemPrompt || '');
|
|
440
|
+
const content = generateFrontmatter(frontmatterData, this.platform) + '\n\n' + (systemPrompt || '');
|
|
404
441
|
|
|
405
442
|
fs.writeFileSync(filePath, content, 'utf-8');
|
|
406
443
|
|
|
@@ -413,7 +450,7 @@ class AgentsService {
|
|
|
413
450
|
updateAgent({ fileName, scope, projectPath, name, description, tools, model, permissionMode, skills, systemPrompt }) {
|
|
414
451
|
const baseDir = scope === 'user'
|
|
415
452
|
? this.userAgentsDir
|
|
416
|
-
:
|
|
453
|
+
: this.getProjectAgentsDir(projectPath);
|
|
417
454
|
|
|
418
455
|
const filePath = path.join(baseDir, `${fileName}.md`);
|
|
419
456
|
|
|
@@ -431,7 +468,7 @@ class AgentsService {
|
|
|
431
468
|
if (permissionMode) frontmatterData.permissionMode = permissionMode;
|
|
432
469
|
if (skills) frontmatterData.skills = skills;
|
|
433
470
|
|
|
434
|
-
const content = generateFrontmatter(frontmatterData) + '\n\n' + (systemPrompt || '');
|
|
471
|
+
const content = generateFrontmatter(frontmatterData, this.platform) + '\n\n' + (systemPrompt || '');
|
|
435
472
|
|
|
436
473
|
fs.writeFileSync(filePath, content, 'utf-8');
|
|
437
474
|
|
|
@@ -444,7 +481,7 @@ class AgentsService {
|
|
|
444
481
|
deleteAgent(fileName, scope, projectPath = null) {
|
|
445
482
|
const baseDir = scope === 'user'
|
|
446
483
|
? this.userAgentsDir
|
|
447
|
-
:
|
|
484
|
+
: this.getProjectAgentsDir(projectPath);
|
|
448
485
|
|
|
449
486
|
const filePath = path.join(baseDir, `${fileName}.md`);
|
|
450
487
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const { getAllChannels } = require('./channels');
|
|
2
2
|
const { getChannels: getCodexChannels } = require('./codex-channels');
|
|
3
3
|
const { getChannels: getGeminiChannels } = require('./gemini-channels');
|
|
4
|
+
const { getChannels: getOpenCodeChannels } = require('./opencode-channels');
|
|
4
5
|
const { isChannelAvailable, getChannelHealthStatus, setOnChannelFrozen } = require('./channel-health');
|
|
5
6
|
|
|
6
7
|
const channelProviders = {
|
|
@@ -12,6 +13,10 @@ const channelProviders = {
|
|
|
12
13
|
gemini: () => {
|
|
13
14
|
const data = getGeminiChannels();
|
|
14
15
|
return Array.isArray(data?.channels) ? data.channels : [];
|
|
16
|
+
},
|
|
17
|
+
opencode: () => {
|
|
18
|
+
const data = getOpenCodeChannels();
|
|
19
|
+
return Array.isArray(data?.channels) ? data.channels : [];
|
|
15
20
|
}
|
|
16
21
|
};
|
|
17
22
|
|
|
@@ -27,7 +32,8 @@ function createState() {
|
|
|
27
32
|
const schedulerStates = {
|
|
28
33
|
claude: createState(),
|
|
29
34
|
codex: createState(),
|
|
30
|
-
gemini: createState()
|
|
35
|
+
gemini: createState(),
|
|
36
|
+
opencode: createState()
|
|
31
37
|
};
|
|
32
38
|
|
|
33
39
|
function getState(source = 'claude') {
|
|
@@ -69,10 +75,8 @@ function refreshChannels(source = 'claude') {
|
|
|
69
75
|
state.channels = raw
|
|
70
76
|
.filter(ch => ch.enabled !== false)
|
|
71
77
|
.map(ch => ({
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
baseUrl: ch.baseUrl,
|
|
75
|
-
apiKey: ch.apiKey,
|
|
78
|
+
// 保留渠道完整字段,避免 proxy 等运行时配置在调度层丢失
|
|
79
|
+
...ch,
|
|
76
80
|
weight: Math.max(1, Number(ch.weight) || 1),
|
|
77
81
|
maxConcurrency: ch.maxConcurrency ?? null,
|
|
78
82
|
modelConfig: ch.modelConfig || null,
|
|
@@ -4,7 +4,7 @@ const os = require('os');
|
|
|
4
4
|
const { isProxyConfig } = require('./settings-manager');
|
|
5
5
|
|
|
6
6
|
function getChannelsFilePath() {
|
|
7
|
-
const dir = path.join(os.homedir(), '.
|
|
7
|
+
const dir = path.join(os.homedir(), '.cc-tool');
|
|
8
8
|
if (!fs.existsSync(dir)) {
|
|
9
9
|
fs.mkdirSync(dir, { recursive: true });
|
|
10
10
|
}
|
|
@@ -12,7 +12,7 @@ function getChannelsFilePath() {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
function getActiveChannelIdPath() {
|
|
15
|
-
const dir = path.join(os.homedir(), '.
|
|
15
|
+
const dir = path.join(os.homedir(), '.cc-tool');
|
|
16
16
|
if (!fs.existsSync(dir)) {
|
|
17
17
|
fs.mkdirSync(dir, { recursive: true });
|
|
18
18
|
}
|
|
@@ -57,6 +57,38 @@ function normalizeNumber(value, defaultValue, max = null) {
|
|
|
57
57
|
return num;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
function normalizeGatewaySourceType(value, fallback = 'claude') {
|
|
61
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
62
|
+
if (normalized === 'claude') return 'claude';
|
|
63
|
+
if (normalized === 'codex') return 'codex';
|
|
64
|
+
if (normalized === 'gemini') return 'gemini';
|
|
65
|
+
return fallback;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function extractApiKeyFromHelper(apiKeyHelper) {
|
|
69
|
+
if (typeof apiKeyHelper !== 'string' || !apiKeyHelper.trim()) {
|
|
70
|
+
return '';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const helper = apiKeyHelper.trim();
|
|
74
|
+
let match = helper.match(/^echo\s+["']([^"']+)["']$/);
|
|
75
|
+
if (match && match[1]) {
|
|
76
|
+
return match[1];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
match = helper.match(/^printf\s+["'][^"']*["']\s+["']([^"']+)["']$/);
|
|
80
|
+
if (match && match[1]) {
|
|
81
|
+
return match[1];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return '';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function buildApiKeyHelperCommand() {
|
|
88
|
+
// 避免把明文 API Key 写入可执行命令,降低注入风险
|
|
89
|
+
return 'printf "%s" "${ANTHROPIC_AUTH_TOKEN:-${ANTHROPIC_API_KEY:-}}"';
|
|
90
|
+
}
|
|
91
|
+
|
|
60
92
|
function applyChannelDefaults(channel) {
|
|
61
93
|
const normalized = { ...channel };
|
|
62
94
|
if (normalized.enabled === undefined) {
|
|
@@ -75,6 +107,8 @@ function applyChannelDefaults(channel) {
|
|
|
75
107
|
normalized.maxConcurrency = normalizeNumber(normalized.maxConcurrency, 1, 100);
|
|
76
108
|
}
|
|
77
109
|
|
|
110
|
+
normalized.gatewaySourceType = normalizeGatewaySourceType(normalized.gatewaySourceType, 'claude');
|
|
111
|
+
|
|
78
112
|
return normalized;
|
|
79
113
|
}
|
|
80
114
|
|
|
@@ -139,10 +173,7 @@ function getCurrentSettings() {
|
|
|
139
173
|
'';
|
|
140
174
|
|
|
141
175
|
if (!apiKey && settings.apiKeyHelper) {
|
|
142
|
-
|
|
143
|
-
if (match && match[1]) {
|
|
144
|
-
apiKey = match[1];
|
|
145
|
-
}
|
|
176
|
+
apiKey = extractApiKeyFromHelper(settings.apiKeyHelper);
|
|
146
177
|
}
|
|
147
178
|
|
|
148
179
|
if (!baseUrl && !apiKey) {
|
|
@@ -173,6 +204,23 @@ function getAllChannels() {
|
|
|
173
204
|
return data.channels;
|
|
174
205
|
}
|
|
175
206
|
|
|
207
|
+
function getCurrentChannel() {
|
|
208
|
+
const channels = getAllChannels();
|
|
209
|
+
if (!Array.isArray(channels) || channels.length === 0) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const activeChannelId = loadActiveChannelId();
|
|
214
|
+
if (activeChannelId) {
|
|
215
|
+
const matched = channels.find(ch => ch.id === activeChannelId);
|
|
216
|
+
if (matched) {
|
|
217
|
+
return matched;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return channels.find(ch => ch.enabled !== false) || channels[0];
|
|
222
|
+
}
|
|
223
|
+
|
|
176
224
|
function createChannel(name, baseUrl, apiKey, websiteUrl, extraConfig = {}) {
|
|
177
225
|
const data = loadChannels();
|
|
178
226
|
const newChannel = applyChannelDefaults({
|
|
@@ -189,7 +237,8 @@ function createChannel(name, baseUrl, apiKey, websiteUrl, extraConfig = {}) {
|
|
|
189
237
|
modelConfig: extraConfig.modelConfig || null,
|
|
190
238
|
modelRedirects: extraConfig.modelRedirects || [],
|
|
191
239
|
proxyUrl: extraConfig.proxyUrl || '',
|
|
192
|
-
speedTestModel: extraConfig.speedTestModel || null
|
|
240
|
+
speedTestModel: extraConfig.speedTestModel || null,
|
|
241
|
+
gatewaySourceType: normalizeGatewaySourceType(extraConfig.gatewaySourceType, 'claude')
|
|
193
242
|
});
|
|
194
243
|
|
|
195
244
|
data.channels.push(newChannel);
|
|
@@ -209,7 +258,7 @@ function updateChannel(id, updates) {
|
|
|
209
258
|
const oldChannel = { ...data.channels[index] };
|
|
210
259
|
|
|
211
260
|
const merged = { ...data.channels[index], ...updates };
|
|
212
|
-
|
|
261
|
+
const nextChannel = applyChannelDefaults({
|
|
213
262
|
...merged,
|
|
214
263
|
weight: merged.weight,
|
|
215
264
|
maxConcurrency: merged.maxConcurrency,
|
|
@@ -219,8 +268,10 @@ function updateChannel(id, updates) {
|
|
|
219
268
|
modelRedirects: merged.modelRedirects || [],
|
|
220
269
|
proxyUrl: merged.proxyUrl,
|
|
221
270
|
speedTestModel: merged.speedTestModel,
|
|
271
|
+
gatewaySourceType: normalizeGatewaySourceType(merged.gatewaySourceType, 'claude'),
|
|
222
272
|
updatedAt: Date.now()
|
|
223
273
|
});
|
|
274
|
+
data.channels[index] = nextChannel;
|
|
224
275
|
|
|
225
276
|
// Get proxy status
|
|
226
277
|
const { getProxyStatus } = require('../proxy-server');
|
|
@@ -264,7 +315,7 @@ function updateChannel(id, updates) {
|
|
|
264
315
|
return data.channels[index];
|
|
265
316
|
}
|
|
266
317
|
|
|
267
|
-
function deleteChannel(id) {
|
|
318
|
+
async function deleteChannel(id) {
|
|
268
319
|
const data = loadChannels();
|
|
269
320
|
const index = data.channels.findIndex(ch => ch.id === id);
|
|
270
321
|
|
|
@@ -274,6 +325,7 @@ function deleteChannel(id) {
|
|
|
274
325
|
|
|
275
326
|
data.channels.splice(index, 1);
|
|
276
327
|
saveChannels(data);
|
|
328
|
+
|
|
277
329
|
return { success: true };
|
|
278
330
|
}
|
|
279
331
|
|
|
@@ -347,7 +399,7 @@ function updateClaudeSettingsWithModelConfig(channel) {
|
|
|
347
399
|
delete settings.env.NO_PROXY;
|
|
348
400
|
}
|
|
349
401
|
|
|
350
|
-
settings.apiKeyHelper =
|
|
402
|
+
settings.apiKeyHelper = buildApiKeyHelperCommand();
|
|
351
403
|
|
|
352
404
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
|
|
353
405
|
}
|
|
@@ -376,13 +428,18 @@ function updateClaudeSettings(baseUrl, apiKey) {
|
|
|
376
428
|
settings.env.ANTHROPIC_API_KEY = apiKey;
|
|
377
429
|
}
|
|
378
430
|
|
|
379
|
-
settings.apiKeyHelper =
|
|
431
|
+
settings.apiKeyHelper = buildApiKeyHelperCommand();
|
|
380
432
|
|
|
381
433
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
|
|
382
434
|
}
|
|
383
435
|
|
|
436
|
+
function getEffectiveApiKey(channel) {
|
|
437
|
+
return channel.apiKey || null;
|
|
438
|
+
}
|
|
439
|
+
|
|
384
440
|
module.exports = {
|
|
385
441
|
getAllChannels,
|
|
442
|
+
getCurrentChannel,
|
|
386
443
|
getCurrentSettings,
|
|
387
444
|
createChannel,
|
|
388
445
|
updateChannel,
|
|
@@ -390,5 +447,6 @@ module.exports = {
|
|
|
390
447
|
applyChannelToSettings,
|
|
391
448
|
getBestChannelForRestore,
|
|
392
449
|
updateClaudeSettings,
|
|
393
|
-
updateClaudeSettingsWithModelConfig
|
|
450
|
+
updateClaudeSettingsWithModelConfig,
|
|
451
|
+
getEffectiveApiKey
|
|
394
452
|
};
|
|
@@ -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,14 +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
|
+
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
|
+
});
|
|
54
65
|
}
|
|
55
66
|
return data;
|
|
56
67
|
} catch (err) {
|
|
@@ -110,6 +121,7 @@ function initializeFromConfig() {
|
|
|
110
121
|
enabled: config.model_provider === providerKey, // 当前激活的渠道启用
|
|
111
122
|
weight: 1,
|
|
112
123
|
maxConcurrency: null,
|
|
124
|
+
gatewaySourceType: 'codex',
|
|
113
125
|
createdAt: Date.now(),
|
|
114
126
|
updatedAt: Date.now()
|
|
115
127
|
});
|
|
@@ -179,6 +191,7 @@ function createChannel(name, providerKey, baseUrl, apiKey, wireApi = 'responses'
|
|
|
179
191
|
maxConcurrency: extraConfig.maxConcurrency || null,
|
|
180
192
|
modelRedirects: extraConfig.modelRedirects || [],
|
|
181
193
|
speedTestModel: extraConfig.speedTestModel || null,
|
|
194
|
+
gatewaySourceType: normalizeGatewaySourceType(extraConfig.gatewaySourceType, 'codex'),
|
|
182
195
|
createdAt: Date.now(),
|
|
183
196
|
updatedAt: Date.now()
|
|
184
197
|
};
|
|
@@ -187,8 +200,8 @@ function createChannel(name, providerKey, baseUrl, apiKey, wireApi = 'responses'
|
|
|
187
200
|
saveChannels(data);
|
|
188
201
|
|
|
189
202
|
// 注入该渠道的环境变量(用于直接使用 codex 命令)
|
|
190
|
-
if (apiKey && envKey) {
|
|
191
|
-
const injectResult = injectEnvToShell(envKey, apiKey);
|
|
203
|
+
if (newChannel.enabled !== false && newChannel.apiKey && envKey) {
|
|
204
|
+
const injectResult = injectEnvToShell(envKey, newChannel.apiKey);
|
|
192
205
|
if (injectResult.success) {
|
|
193
206
|
console.log(`[Codex Channels] Environment variable ${envKey} injected for new channel`);
|
|
194
207
|
} else {
|
|
@@ -228,6 +241,7 @@ function updateChannel(channelId, updates) {
|
|
|
228
241
|
createdAt: oldChannel.createdAt, // 保持创建时间
|
|
229
242
|
modelRedirects: merged.modelRedirects || [],
|
|
230
243
|
speedTestModel: merged.speedTestModel !== undefined ? merged.speedTestModel : (oldChannel.speedTestModel || null),
|
|
244
|
+
gatewaySourceType: normalizeGatewaySourceType(merged.gatewaySourceType, 'codex'),
|
|
231
245
|
updatedAt: Date.now()
|
|
232
246
|
};
|
|
233
247
|
|
|
@@ -269,19 +283,23 @@ function updateChannel(channelId, updates) {
|
|
|
269
283
|
// 如果 envKey 或 apiKey 变化,需要更新环境变量
|
|
270
284
|
const oldEnvKey = oldChannel.envKey;
|
|
271
285
|
const newEnvKey = newChannel.envKey;
|
|
272
|
-
const oldApiKey = oldChannel.apiKey;
|
|
273
286
|
const newApiKey = newChannel.apiKey;
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
287
|
+
const shouldRemoveOldEnv =
|
|
288
|
+
!!oldEnvKey && (
|
|
289
|
+
oldEnvKey !== newEnvKey ||
|
|
290
|
+
!newApiKey ||
|
|
291
|
+
newChannel.enabled === false
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
// 禁用或 key 变化时都要清理旧环境变量,避免残留
|
|
295
|
+
if (shouldRemoveOldEnv) {
|
|
277
296
|
const removeResult = removeEnvFromShell(oldEnvKey);
|
|
278
297
|
if (removeResult.success) {
|
|
279
298
|
console.log(`[Codex Channels] Old environment variable ${oldEnvKey} removed`);
|
|
280
299
|
}
|
|
281
300
|
}
|
|
282
301
|
|
|
283
|
-
|
|
284
|
-
if (newApiKey && newEnvKey) {
|
|
302
|
+
if (newChannel.enabled !== false && newApiKey && newEnvKey) {
|
|
285
303
|
const injectResult = injectEnvToShell(newEnvKey, newApiKey);
|
|
286
304
|
if (injectResult.success) {
|
|
287
305
|
console.log(`[Codex Channels] Environment variable ${newEnvKey} updated`);
|
|
@@ -295,7 +313,7 @@ function updateChannel(channelId, updates) {
|
|
|
295
313
|
}
|
|
296
314
|
|
|
297
315
|
// 删除渠道
|
|
298
|
-
function deleteChannel(channelId) {
|
|
316
|
+
async function deleteChannel(channelId) {
|
|
299
317
|
const data = loadChannels();
|
|
300
318
|
|
|
301
319
|
const index = data.channels.findIndex(c => c.id === channelId);
|
|
@@ -397,7 +415,7 @@ function writeCodexConfigForMultiChannel(allChannels) {
|
|
|
397
415
|
// 回退默认的代理配置(使用默认端口),确保 provider 存在
|
|
398
416
|
config.model_providers['cc-proxy'] = {
|
|
399
417
|
name: 'cc-proxy',
|
|
400
|
-
base_url: 'http://127.0.0.1:
|
|
418
|
+
base_url: 'http://127.0.0.1:20089/v1',
|
|
401
419
|
wire_api: 'responses',
|
|
402
420
|
env_key: 'CC_PROXY_KEY'
|
|
403
421
|
};
|
|
@@ -448,6 +466,12 @@ ${tomlContent}`;
|
|
|
448
466
|
}
|
|
449
467
|
|
|
450
468
|
// 更新所有渠道的 API Key
|
|
469
|
+
for (const channel of allChannels) {
|
|
470
|
+
if (channel.envKey && !channel.apiKey) {
|
|
471
|
+
delete auth[channel.envKey];
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
451
475
|
for (const channel of allChannels) {
|
|
452
476
|
if (channel.apiKey) {
|
|
453
477
|
auth[channel.envKey] = channel.apiKey;
|
|
@@ -508,7 +532,10 @@ function syncAllChannelEnvVars() {
|
|
|
508
532
|
const results = [];
|
|
509
533
|
|
|
510
534
|
for (const channel of channels) {
|
|
511
|
-
if (channel.
|
|
535
|
+
if (!channel.envKey) continue;
|
|
536
|
+
|
|
537
|
+
const shouldInject = channel.enabled !== false && !!channel.apiKey;
|
|
538
|
+
if (shouldInject) {
|
|
512
539
|
const injectResult = injectEnvToShell(channel.envKey, channel.apiKey);
|
|
513
540
|
if (injectResult.success) {
|
|
514
541
|
syncedCount++;
|
|
@@ -516,7 +543,11 @@ function syncAllChannelEnvVars() {
|
|
|
516
543
|
} else {
|
|
517
544
|
results.push({ envKey: channel.envKey, success: false, error: injectResult.error });
|
|
518
545
|
}
|
|
546
|
+
continue;
|
|
519
547
|
}
|
|
548
|
+
|
|
549
|
+
// 清理已停用或缺失 key 的渠道环境变量,避免残留
|
|
550
|
+
removeEnvFromShell(channel.envKey);
|
|
520
551
|
}
|
|
521
552
|
|
|
522
553
|
console.log(`[Codex Channels] Synced ${syncedCount} environment variables`);
|
|
@@ -620,19 +651,21 @@ ${tomlContent}`;
|
|
|
620
651
|
}
|
|
621
652
|
}
|
|
622
653
|
|
|
623
|
-
// 添加当前渠道的 API Key
|
|
624
654
|
if (channel.apiKey && channel.envKey) {
|
|
625
655
|
auth[channel.envKey] = channel.apiKey;
|
|
656
|
+
} else if (channel.envKey) {
|
|
657
|
+
delete auth[channel.envKey];
|
|
626
658
|
}
|
|
627
659
|
|
|
628
660
|
fs.writeFileSync(authPath, JSON.stringify(auth, null, 2), 'utf8');
|
|
629
661
|
|
|
630
|
-
// 注入环境变量到 shell 配置文件
|
|
631
662
|
if (channel.apiKey && channel.envKey) {
|
|
632
663
|
const injectResult = injectEnvToShell(channel.envKey, channel.apiKey);
|
|
633
664
|
if (injectResult.success) {
|
|
634
665
|
console.log(`[Codex Channels] Environment variable ${channel.envKey} injected`);
|
|
635
666
|
}
|
|
667
|
+
} else if (channel.envKey) {
|
|
668
|
+
removeEnvFromShell(channel.envKey);
|
|
636
669
|
}
|
|
637
670
|
|
|
638
671
|
return channel;
|
|
@@ -649,6 +682,10 @@ try {
|
|
|
649
682
|
console.warn('[Codex Channels] Auto sync env vars failed:', err.message);
|
|
650
683
|
}
|
|
651
684
|
|
|
685
|
+
function getEffectiveApiKey(channel) {
|
|
686
|
+
return channel.apiKey || null;
|
|
687
|
+
}
|
|
688
|
+
|
|
652
689
|
module.exports = {
|
|
653
690
|
getChannels,
|
|
654
691
|
createChannel,
|
|
@@ -658,5 +695,6 @@ module.exports = {
|
|
|
658
695
|
saveChannelOrder,
|
|
659
696
|
syncAllChannelEnvVars,
|
|
660
697
|
writeCodexConfigForMultiChannel,
|
|
661
|
-
applyChannelToSettings
|
|
698
|
+
applyChannelToSettings,
|
|
699
|
+
getEffectiveApiKey
|
|
662
700
|
};
|