@adversity/coding-tool-x 2.2.0
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 +333 -0
- package/LICENSE +21 -0
- package/README.md +404 -0
- package/bin/ctx.js +8 -0
- package/dist/web/assets/index-D1AYlFLZ.js +3220 -0
- package/dist/web/assets/index-aL3cKxSK.css +41 -0
- package/dist/web/favicon.ico +0 -0
- package/dist/web/index.html +14 -0
- package/dist/web/logo.png +0 -0
- package/docs/CHANGELOG.md +582 -0
- package/docs/DIRECTORY_MIGRATION.md +112 -0
- package/docs/PROJECT_STRUCTURE.md +396 -0
- package/docs/bannel.png +0 -0
- package/docs/home.png +0 -0
- package/docs/logo.png +0 -0
- package/docs/multi-channel-load-balancing.md +249 -0
- package/package.json +73 -0
- package/src/commands/channels.js +504 -0
- package/src/commands/cli-type.js +99 -0
- package/src/commands/daemon.js +286 -0
- package/src/commands/doctor.js +332 -0
- package/src/commands/list.js +222 -0
- package/src/commands/logs.js +259 -0
- package/src/commands/port-config.js +115 -0
- package/src/commands/proxy-control.js +258 -0
- package/src/commands/proxy.js +152 -0
- package/src/commands/resume.js +137 -0
- package/src/commands/search.js +190 -0
- package/src/commands/stats.js +224 -0
- package/src/commands/switch.js +48 -0
- package/src/commands/toggle-proxy.js +222 -0
- package/src/commands/ui.js +92 -0
- package/src/commands/workspace.js +454 -0
- package/src/config/default.js +40 -0
- package/src/config/loader.js +75 -0
- package/src/config/paths.js +121 -0
- package/src/index.js +373 -0
- package/src/reset-config.js +92 -0
- package/src/server/api/agents.js +248 -0
- package/src/server/api/aliases.js +36 -0
- package/src/server/api/channels.js +258 -0
- package/src/server/api/claude-hooks.js +480 -0
- package/src/server/api/codex-channels.js +312 -0
- package/src/server/api/codex-projects.js +91 -0
- package/src/server/api/codex-proxy.js +182 -0
- package/src/server/api/codex-sessions.js +491 -0
- package/src/server/api/codex-statistics.js +57 -0
- package/src/server/api/commands.js +245 -0
- package/src/server/api/config-templates.js +182 -0
- package/src/server/api/config.js +147 -0
- package/src/server/api/convert.js +127 -0
- package/src/server/api/dashboard.js +125 -0
- package/src/server/api/env.js +144 -0
- package/src/server/api/favorites.js +77 -0
- package/src/server/api/gemini-channels.js +261 -0
- package/src/server/api/gemini-projects.js +91 -0
- package/src/server/api/gemini-proxy.js +160 -0
- package/src/server/api/gemini-sessions.js +397 -0
- package/src/server/api/gemini-statistics.js +57 -0
- package/src/server/api/health-check.js +118 -0
- package/src/server/api/mcp.js +336 -0
- package/src/server/api/pm2-autostart.js +269 -0
- package/src/server/api/projects.js +124 -0
- package/src/server/api/prompts.js +279 -0
- package/src/server/api/proxy.js +235 -0
- package/src/server/api/rules.js +271 -0
- package/src/server/api/sessions.js +595 -0
- package/src/server/api/settings.js +61 -0
- package/src/server/api/skills.js +305 -0
- package/src/server/api/statistics.js +91 -0
- package/src/server/api/terminal.js +202 -0
- package/src/server/api/ui-config.js +64 -0
- package/src/server/api/workspaces.js +407 -0
- package/src/server/codex-proxy-server.js +538 -0
- package/src/server/dev-server.js +26 -0
- package/src/server/gemini-proxy-server.js +518 -0
- package/src/server/index.js +305 -0
- package/src/server/proxy-server.js +469 -0
- package/src/server/services/agents-service.js +354 -0
- package/src/server/services/alias.js +71 -0
- package/src/server/services/channel-health.js +234 -0
- package/src/server/services/channel-scheduler.js +234 -0
- package/src/server/services/channels.js +347 -0
- package/src/server/services/codex-channels.js +625 -0
- package/src/server/services/codex-config.js +90 -0
- package/src/server/services/codex-parser.js +322 -0
- package/src/server/services/codex-sessions.js +665 -0
- package/src/server/services/codex-settings-manager.js +397 -0
- package/src/server/services/codex-speed-test-template.json +24 -0
- package/src/server/services/codex-statistics-service.js +255 -0
- package/src/server/services/commands-service.js +360 -0
- package/src/server/services/config-templates-service.js +732 -0
- package/src/server/services/env-checker.js +307 -0
- package/src/server/services/env-manager.js +300 -0
- package/src/server/services/favorites.js +163 -0
- package/src/server/services/gemini-channels.js +333 -0
- package/src/server/services/gemini-config.js +73 -0
- package/src/server/services/gemini-sessions.js +689 -0
- package/src/server/services/gemini-settings-manager.js +263 -0
- package/src/server/services/gemini-statistics-service.js +253 -0
- package/src/server/services/health-check.js +399 -0
- package/src/server/services/mcp-service.js +1188 -0
- package/src/server/services/prompts-service.js +492 -0
- package/src/server/services/proxy-runtime.js +79 -0
- package/src/server/services/pty-manager.js +435 -0
- package/src/server/services/rules-service.js +401 -0
- package/src/server/services/session-cache.js +127 -0
- package/src/server/services/session-converter.js +577 -0
- package/src/server/services/sessions.js +757 -0
- package/src/server/services/settings-manager.js +163 -0
- package/src/server/services/skill-service.js +965 -0
- package/src/server/services/speed-test.js +545 -0
- package/src/server/services/statistics-service.js +386 -0
- package/src/server/services/terminal-commands.js +155 -0
- package/src/server/services/terminal-config.js +140 -0
- package/src/server/services/terminal-detector.js +306 -0
- package/src/server/services/ui-config.js +130 -0
- package/src/server/services/workspace-service.js +662 -0
- package/src/server/utils/pricing.js +41 -0
- package/src/server/websocket-server.js +557 -0
- package/src/ui/menu.js +129 -0
- package/src/ui/prompts.js +100 -0
- package/src/utils/format.js +43 -0
- package/src/utils/port-helper.js +94 -0
- package/src/utils/session.js +239 -0
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@adversity/coding-tool-x",
|
|
3
|
+
"version": "2.2.0",
|
|
4
|
+
"description": "Vibe Coding 增强工作助手 - 智能会话管理、动态渠道切换、全局搜索、实时监控",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ctx": "bin/ctx.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node bin/ctx.js",
|
|
11
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
12
|
+
"build:web": "cd src/web && npm run build",
|
|
13
|
+
"dev:web": "cd src/web && npm run dev",
|
|
14
|
+
"dev:server": "nodemon"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"claude",
|
|
18
|
+
"claude-code",
|
|
19
|
+
"claude-cli",
|
|
20
|
+
"session-manager",
|
|
21
|
+
"cli",
|
|
22
|
+
"interactive",
|
|
23
|
+
"conversation",
|
|
24
|
+
"history",
|
|
25
|
+
"cc switch"
|
|
26
|
+
],
|
|
27
|
+
"author": "CooperJiang",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=14.0.0"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"bin/",
|
|
34
|
+
"src/commands/",
|
|
35
|
+
"src/config/",
|
|
36
|
+
"src/server/",
|
|
37
|
+
"src/ui/",
|
|
38
|
+
"src/utils/",
|
|
39
|
+
"src/index.js",
|
|
40
|
+
"src/reset-config.js",
|
|
41
|
+
"docs/",
|
|
42
|
+
"dist/",
|
|
43
|
+
"CHANGELOG.md",
|
|
44
|
+
"README.md",
|
|
45
|
+
"LICENSE"
|
|
46
|
+
],
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@iarna/toml": "^2.2.5",
|
|
49
|
+
"@lydell/node-pty": "^1.1.0",
|
|
50
|
+
"adm-zip": "^0.5.16",
|
|
51
|
+
"chalk": "^4.1.2",
|
|
52
|
+
"express": "^4.18.2",
|
|
53
|
+
"http-proxy": "^1.18.1",
|
|
54
|
+
"https-proxy-agent": "^7.0.6",
|
|
55
|
+
"inquirer": "^8.2.7",
|
|
56
|
+
"open": "^8.4.2",
|
|
57
|
+
"ora": "^5.4.1",
|
|
58
|
+
"pm2": "^5.4.3",
|
|
59
|
+
"toml": "^3.0.0",
|
|
60
|
+
"ws": "^8.18.3"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"nodemon": "^3.0.2"
|
|
64
|
+
},
|
|
65
|
+
"repository": {
|
|
66
|
+
"type": "git",
|
|
67
|
+
"url": "git+https://github.com/CooperJiang/cc-tool.git"
|
|
68
|
+
},
|
|
69
|
+
"bugs": {
|
|
70
|
+
"url": "https://github.com/CooperJiang/cc-tool/issues"
|
|
71
|
+
},
|
|
72
|
+
"homepage": "https://github.com/CooperJiang/cc-tool#readme"
|
|
73
|
+
}
|
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
// 渠道管理命令
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const inquirer = require('inquirer');
|
|
4
|
+
const { loadConfig } = require('../config/loader');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 获取当前类型的渠道服务
|
|
8
|
+
*/
|
|
9
|
+
function getChannelServices(cliType) {
|
|
10
|
+
if (cliType === 'claude') {
|
|
11
|
+
const {
|
|
12
|
+
getAllChannels,
|
|
13
|
+
createChannel,
|
|
14
|
+
updateChannel
|
|
15
|
+
} = require('../server/services/channels');
|
|
16
|
+
const { getProxyStatus } = require('../server/proxy-server');
|
|
17
|
+
return { getAllChannels, createChannel, updateChannel, getProxyStatus };
|
|
18
|
+
} else if (cliType === 'codex') {
|
|
19
|
+
const {
|
|
20
|
+
getChannels,
|
|
21
|
+
createChannel,
|
|
22
|
+
updateChannel
|
|
23
|
+
} = require('../server/services/codex-channels');
|
|
24
|
+
const { getCodexProxyStatus } = require('../server/codex-proxy-server');
|
|
25
|
+
return {
|
|
26
|
+
getAllChannels: () => {
|
|
27
|
+
const result = getChannels();
|
|
28
|
+
return Array.isArray(result?.channels) ? result.channels : [];
|
|
29
|
+
},
|
|
30
|
+
createChannel,
|
|
31
|
+
updateChannel,
|
|
32
|
+
getProxyStatus: getCodexProxyStatus
|
|
33
|
+
};
|
|
34
|
+
} else if (cliType === 'gemini') {
|
|
35
|
+
const {
|
|
36
|
+
getChannels,
|
|
37
|
+
createChannel,
|
|
38
|
+
updateChannel
|
|
39
|
+
} = require('../server/services/gemini-channels');
|
|
40
|
+
const { getGeminiProxyStatus } = require('../server/gemini-proxy-server');
|
|
41
|
+
return {
|
|
42
|
+
getAllChannels: () => {
|
|
43
|
+
const result = getChannels();
|
|
44
|
+
return Array.isArray(result?.channels) ? result.channels : [];
|
|
45
|
+
},
|
|
46
|
+
createChannel,
|
|
47
|
+
updateChannel,
|
|
48
|
+
getProxyStatus: getGeminiProxyStatus
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 渠道管理
|
|
55
|
+
*/
|
|
56
|
+
async function handleChannelManagement() {
|
|
57
|
+
console.clear();
|
|
58
|
+
console.log(chalk.bold.cyan('\n╔═══════════════════════════════════════╗'));
|
|
59
|
+
console.log(chalk.bold.cyan('║ 渠道管理 ║'));
|
|
60
|
+
console.log(chalk.bold.cyan('╚═══════════════════════════════════════╝\n'));
|
|
61
|
+
|
|
62
|
+
const config = loadConfig();
|
|
63
|
+
const cliType = config.currentCliType || 'claude';
|
|
64
|
+
const services = getChannelServices(cliType);
|
|
65
|
+
if (!services || typeof services.getAllChannels !== 'function') {
|
|
66
|
+
console.log(chalk.red(`当前 CLI 类型 (${cliType}) 暂不支持渠道管理`));
|
|
67
|
+
await inquirer.prompt([{ type: 'input', name: 'continue', message: '按回车返回...' }]);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const channels = services.getAllChannels();
|
|
72
|
+
|
|
73
|
+
if (channels.length === 0) {
|
|
74
|
+
console.log(chalk.yellow('还没有添加任何渠道'));
|
|
75
|
+
console.log(chalk.gray('提示: 使用主菜单的"添加渠道"功能来添加新渠道\n'));
|
|
76
|
+
|
|
77
|
+
const { action } = await inquirer.prompt([
|
|
78
|
+
{
|
|
79
|
+
type: 'list',
|
|
80
|
+
name: 'action',
|
|
81
|
+
message: '请选择操作:',
|
|
82
|
+
choices: [
|
|
83
|
+
{ name: chalk.blue('返回主菜单'), value: 'back' },
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const supportsMultiToggle = typeof services.updateChannel === 'function';
|
|
92
|
+
|
|
93
|
+
if (supportsMultiToggle) {
|
|
94
|
+
await handleChannelToggle(channels, services, cliType);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
await handleLegacySwitch(channels, services);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 添加渠道
|
|
103
|
+
*/
|
|
104
|
+
async function handleAddChannel() {
|
|
105
|
+
console.clear();
|
|
106
|
+
console.log(chalk.bold.cyan('\n╔═══════════════════════════════════════╗'));
|
|
107
|
+
console.log(chalk.bold.cyan('║ 添加渠道 ║'));
|
|
108
|
+
console.log(chalk.bold.cyan('╚═══════════════════════════════════════╝\n'));
|
|
109
|
+
|
|
110
|
+
const config = loadConfig();
|
|
111
|
+
const cliType = config.currentCliType || 'claude';
|
|
112
|
+
const services = getChannelServices(cliType);
|
|
113
|
+
|
|
114
|
+
const answers = await inquirer.prompt([
|
|
115
|
+
{
|
|
116
|
+
type: 'input',
|
|
117
|
+
name: 'name',
|
|
118
|
+
message: '渠道名称:',
|
|
119
|
+
validate: (input) => {
|
|
120
|
+
if (!input.trim()) {
|
|
121
|
+
return '渠道名称不能为空';
|
|
122
|
+
}
|
|
123
|
+
return true;
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
type: 'input',
|
|
128
|
+
name: 'baseUrl',
|
|
129
|
+
message: 'Base URL:',
|
|
130
|
+
validate: (input) => {
|
|
131
|
+
if (!input.trim()) {
|
|
132
|
+
return 'Base URL 不能为空';
|
|
133
|
+
}
|
|
134
|
+
// 简单的 URL 验证
|
|
135
|
+
if (!input.startsWith('http://') && !input.startsWith('https://')) {
|
|
136
|
+
return 'Base URL 必须以 http:// 或 https:// 开头';
|
|
137
|
+
}
|
|
138
|
+
return true;
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
type: 'input',
|
|
143
|
+
name: 'apiKey',
|
|
144
|
+
message: 'API Key:',
|
|
145
|
+
validate: (input) => {
|
|
146
|
+
if (!input.trim()) {
|
|
147
|
+
return 'API Key 不能为空';
|
|
148
|
+
}
|
|
149
|
+
return true;
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
type: 'input',
|
|
154
|
+
name: 'websiteUrl',
|
|
155
|
+
message: '网站地址(可选,直接回车跳过):',
|
|
156
|
+
},
|
|
157
|
+
]);
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
let channel;
|
|
161
|
+
|
|
162
|
+
// Claude 类型的参数: (name, baseUrl, apiKey, websiteUrl, extraConfig)
|
|
163
|
+
if (cliType === 'claude') {
|
|
164
|
+
channel = services.createChannel(
|
|
165
|
+
answers.name.trim(),
|
|
166
|
+
answers.baseUrl.trim(),
|
|
167
|
+
answers.apiKey.trim(),
|
|
168
|
+
answers.websiteUrl.trim() || undefined
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
// Codex 类型的参数: (name, providerKey, baseUrl, apiKey, wireApi, extraConfig)
|
|
172
|
+
else if (cliType === 'codex') {
|
|
173
|
+
// Codex 需要额外的 providerKey 和 wireApi 参数
|
|
174
|
+
// 在这里简化为使用 name 作为 providerKey,wireApi 固定为 'responses'
|
|
175
|
+
channel = services.createChannel(
|
|
176
|
+
answers.name.trim(),
|
|
177
|
+
answers.name.trim().toLowerCase().replace(/\s+/g, '-'), // 生成 providerKey
|
|
178
|
+
answers.baseUrl.trim(),
|
|
179
|
+
answers.apiKey.trim(),
|
|
180
|
+
'responses', // wireApi 固定值
|
|
181
|
+
{ websiteUrl: answers.websiteUrl.trim() || undefined }
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
// Gemini 类型的参数: (name, baseUrl, apiKey, model, extraConfig)
|
|
185
|
+
else if (cliType === 'gemini') {
|
|
186
|
+
// Gemini 需要 model 参数
|
|
187
|
+
const modelAnswer = await inquirer.prompt([
|
|
188
|
+
{
|
|
189
|
+
type: 'input',
|
|
190
|
+
name: 'model',
|
|
191
|
+
message: '模型名称 (默认: gemini-2.5-pro):',
|
|
192
|
+
default: 'gemini-2.5-pro'
|
|
193
|
+
}
|
|
194
|
+
]);
|
|
195
|
+
|
|
196
|
+
channel = services.createChannel(
|
|
197
|
+
answers.name.trim(),
|
|
198
|
+
answers.baseUrl.trim(),
|
|
199
|
+
answers.apiKey.trim(),
|
|
200
|
+
modelAnswer.model.trim(),
|
|
201
|
+
{ websiteUrl: answers.websiteUrl.trim() || undefined }
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
console.log(chalk.green(`\n✅ 渠道添加成功: ${channel.name}\n`));
|
|
206
|
+
console.log(chalk.gray('提示: 使用"渠道管理"功能来启用此渠道\n'));
|
|
207
|
+
broadcastSchedulerSnapshot(cliType);
|
|
208
|
+
|
|
209
|
+
await inquirer.prompt([
|
|
210
|
+
{
|
|
211
|
+
type: 'input',
|
|
212
|
+
name: 'continue',
|
|
213
|
+
message: '按回车继续...',
|
|
214
|
+
},
|
|
215
|
+
]);
|
|
216
|
+
} catch (error) {
|
|
217
|
+
console.log(chalk.red(`\n❌ 添加失败: ${error.message}\n`));
|
|
218
|
+
|
|
219
|
+
await inquirer.prompt([
|
|
220
|
+
{
|
|
221
|
+
type: 'input',
|
|
222
|
+
name: 'continue',
|
|
223
|
+
message: '按回车继续...',
|
|
224
|
+
},
|
|
225
|
+
]);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function handleChannelStatus() {
|
|
230
|
+
console.clear();
|
|
231
|
+
console.log(chalk.bold.cyan('\n╔═══════════════════════════════════════╗'));
|
|
232
|
+
console.log(chalk.bold.cyan('║ 调度状态查看 ║'));
|
|
233
|
+
console.log(chalk.bold.cyan('╚═══════════════════════════════════════╝\n'));
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
const { getSchedulerState } = require('../server/services/channel-scheduler');
|
|
237
|
+
const sources = [
|
|
238
|
+
{ key: 'claude', label: 'Claude (Claude Code)' },
|
|
239
|
+
{ key: 'codex', label: 'Codex (OpenAI)' },
|
|
240
|
+
{ key: 'gemini', label: 'Gemini' }
|
|
241
|
+
];
|
|
242
|
+
|
|
243
|
+
sources.forEach((source) => {
|
|
244
|
+
const state = getSchedulerState(source.key);
|
|
245
|
+
console.log(chalk.bold(`\n[${source.label}]`));
|
|
246
|
+
console.log(chalk.gray(`排队中: ${state.pending || 0}`));
|
|
247
|
+
|
|
248
|
+
if (!state.channels || state.channels.length === 0) {
|
|
249
|
+
console.log(chalk.yellow('暂无调度数据或未启用渠道。'));
|
|
250
|
+
} else {
|
|
251
|
+
state.channels.forEach((channel, index) => {
|
|
252
|
+
const concurrency = channel.maxConcurrency ?? '∞';
|
|
253
|
+
const healthText = channel.health?.statusText || '健康';
|
|
254
|
+
const healthColor = channel.health?.statusColor || '#18a058';
|
|
255
|
+
console.log(
|
|
256
|
+
`${chalk.cyan(String(index + 1).padStart(2, '0'))}. ${chalk.bold(channel.name)} ` +
|
|
257
|
+
chalk.gray(`并发 ${channel.inflight}/${concurrency} | 权重 ${channel.weight || 1}`)
|
|
258
|
+
);
|
|
259
|
+
console.log(` 健康状态: ${chalk.hex(healthColor)(healthText)}`);
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
} catch (error) {
|
|
264
|
+
console.log(chalk.red('无法读取调度状态: ' + error.message));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
await inquirer.prompt([{ type: 'input', name: 'continue', message: '按回车返回...' }]);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
module.exports = {
|
|
271
|
+
handleChannelManagement,
|
|
272
|
+
handleAddChannel,
|
|
273
|
+
handleChannelStatus
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* 统一的多渠道选择处理
|
|
278
|
+
*/
|
|
279
|
+
async function handleChannelToggle(channels, services, cliType) {
|
|
280
|
+
const choices = channels.map(channel => {
|
|
281
|
+
const enabled = channel.enabled !== false;
|
|
282
|
+
const detailParts = [];
|
|
283
|
+
|
|
284
|
+
if (channel.baseUrl) {
|
|
285
|
+
const cleaned = channel.baseUrl.replace('https://', '').replace('http://', '');
|
|
286
|
+
detailParts.push(cleaned);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (cliType === 'codex' && channel.providerKey) {
|
|
290
|
+
detailParts.push(`provider ${channel.providerKey}`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (cliType === 'gemini' && channel.model) {
|
|
294
|
+
detailParts.push(`model ${channel.model}`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (channel.maxConcurrency) {
|
|
298
|
+
detailParts.push(`并发 ${channel.maxConcurrency}`);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (channel.weight) {
|
|
302
|
+
detailParts.push(`权重 ${channel.weight}`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const detail = detailParts.length ? chalk.gray(` [${detailParts.join(' | ')}]`) : '';
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
name: `${chalk.bold(channel.name)}${detail}`,
|
|
309
|
+
value: channel.id,
|
|
310
|
+
checked: enabled,
|
|
311
|
+
short: channel.name
|
|
312
|
+
};
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const { selectedIds } = await inquirer.prompt([
|
|
316
|
+
{
|
|
317
|
+
type: 'checkbox',
|
|
318
|
+
name: 'selectedIds',
|
|
319
|
+
message: '选择需要启用的渠道(可多选,至少一个):',
|
|
320
|
+
pageSize: 15,
|
|
321
|
+
choices
|
|
322
|
+
}
|
|
323
|
+
]);
|
|
324
|
+
|
|
325
|
+
if (!selectedIds.length) {
|
|
326
|
+
console.log(chalk.red('\n❌ 至少需要启用一个渠道。\n'));
|
|
327
|
+
await inquirer.prompt([{ type: 'input', name: 'continue', message: '按回车返回...' }]);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
let hasChanged = false;
|
|
332
|
+
for (const channel of channels) {
|
|
333
|
+
const shouldEnable = selectedIds.includes(channel.id);
|
|
334
|
+
const currentEnabled = channel.enabled !== false;
|
|
335
|
+
if (shouldEnable !== currentEnabled) {
|
|
336
|
+
await services.updateChannel(channel.id, { enabled: shouldEnable });
|
|
337
|
+
console.log(
|
|
338
|
+
`${shouldEnable ? chalk.green('✅ 启用') : chalk.yellow('⏸ 停用')} ${chalk.bold(channel.name)}`
|
|
339
|
+
);
|
|
340
|
+
hasChanged = true;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (hasChanged) {
|
|
345
|
+
broadcastSchedulerSnapshot(cliType);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
await handleAdvancedConfig(services, cliType);
|
|
349
|
+
|
|
350
|
+
console.log(
|
|
351
|
+
chalk.cyan(
|
|
352
|
+
`\n提示: 多个渠道启用后将由调度器根据权重和并发自动分配请求,无需再手动切换默认渠道。\n`
|
|
353
|
+
)
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
await inquirer.prompt([{ type: 'input', name: 'continue', message: '按回车返回...' }]);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async function handleAdvancedConfig(services, cliType) {
|
|
360
|
+
if (typeof services.updateChannel !== 'function' || typeof services.getAllChannels !== 'function') {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const { adjust } = await inquirer.prompt([
|
|
365
|
+
{
|
|
366
|
+
type: 'confirm',
|
|
367
|
+
name: 'adjust',
|
|
368
|
+
message: '是否需要调整渠道的权重或最大并发?',
|
|
369
|
+
default: false
|
|
370
|
+
}
|
|
371
|
+
]);
|
|
372
|
+
|
|
373
|
+
if (!adjust) return;
|
|
374
|
+
|
|
375
|
+
while (true) {
|
|
376
|
+
const latestChannels = services.getAllChannels();
|
|
377
|
+
if (!latestChannels.length) {
|
|
378
|
+
console.log(chalk.red('暂无渠道可供调整'));
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const channelChoices = latestChannels.map(channel => {
|
|
383
|
+
const label = `${chalk.bold(channel.name)}${chalk.gray(
|
|
384
|
+
` (并发 ${channel.maxConcurrency ?? '∞'} | 权重 ${channel.weight || 1})`
|
|
385
|
+
)}`;
|
|
386
|
+
return { name: label, value: channel.id };
|
|
387
|
+
});
|
|
388
|
+
channelChoices.push(new inquirer.Separator(chalk.gray('─'.repeat(14))));
|
|
389
|
+
channelChoices.push({ name: chalk.blue('完成调整'), value: 'done' });
|
|
390
|
+
|
|
391
|
+
const { channelId } = await inquirer.prompt([
|
|
392
|
+
{
|
|
393
|
+
type: 'list',
|
|
394
|
+
name: 'channelId',
|
|
395
|
+
message: '请选择要调整的渠道:',
|
|
396
|
+
pageSize: 12,
|
|
397
|
+
choices: channelChoices
|
|
398
|
+
}
|
|
399
|
+
]);
|
|
400
|
+
|
|
401
|
+
if (channelId === 'done') {
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const target = latestChannels.find(ch => ch.id === channelId);
|
|
406
|
+
if (!target) {
|
|
407
|
+
console.log(chalk.red('未找到指定渠道'));
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const answers = await inquirer.prompt([
|
|
412
|
+
{
|
|
413
|
+
type: 'input',
|
|
414
|
+
name: 'maxConcurrency',
|
|
415
|
+
message: '最大并发(输入 0 表示不限):',
|
|
416
|
+
default: target.maxConcurrency ?? 0,
|
|
417
|
+
validate: (input) => {
|
|
418
|
+
const value = Number(input);
|
|
419
|
+
if (Number.isNaN(value) || value < 0) {
|
|
420
|
+
return '请输入大于等于 0 的数字';
|
|
421
|
+
}
|
|
422
|
+
return true;
|
|
423
|
+
}
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
type: 'input',
|
|
427
|
+
name: 'weight',
|
|
428
|
+
message: '调度权重(至少 1,影响被选中的概率):',
|
|
429
|
+
default: target.weight || 1,
|
|
430
|
+
validate: (input) => {
|
|
431
|
+
const value = Number(input);
|
|
432
|
+
if (Number.isNaN(value) || value < 1) {
|
|
433
|
+
return '请输入大于等于 1 的数字';
|
|
434
|
+
}
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
]);
|
|
439
|
+
|
|
440
|
+
const maxConcurrencyValue = Number(answers.maxConcurrency);
|
|
441
|
+
const weightValue = Number(answers.weight);
|
|
442
|
+
|
|
443
|
+
const payload = {
|
|
444
|
+
maxConcurrency: maxConcurrencyValue <= 0 ? null : Math.round(maxConcurrencyValue),
|
|
445
|
+
weight: Math.max(1, Math.round(weightValue))
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
try {
|
|
449
|
+
await services.updateChannel(target.id, payload);
|
|
450
|
+
console.log(chalk.green(`已更新 ${target.name} 的并发/权重设置`));
|
|
451
|
+
broadcastSchedulerSnapshot(cliType);
|
|
452
|
+
} catch (error) {
|
|
453
|
+
console.log(chalk.red(`更新失败: ${error.message}`));
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function broadcastSchedulerSnapshot(source = 'claude') {
|
|
459
|
+
try {
|
|
460
|
+
const { getSchedulerState } = require('../server/services/channel-scheduler');
|
|
461
|
+
const { broadcastSchedulerState } = require('../server/websocket-server');
|
|
462
|
+
broadcastSchedulerState(source, getSchedulerState(source));
|
|
463
|
+
} catch (err) {
|
|
464
|
+
// ignore when scheduler not available
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
async function handleLegacySwitch(channels, services) {
|
|
469
|
+
const choices = channels.map(channel => {
|
|
470
|
+
let name = channel.enabled !== false ? chalk.green('● ') : ' ';
|
|
471
|
+
name += chalk.bold(channel.name);
|
|
472
|
+
return {
|
|
473
|
+
name,
|
|
474
|
+
value: channel.id,
|
|
475
|
+
short: channel.name
|
|
476
|
+
};
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
choices.push(new inquirer.Separator(chalk.gray('─'.repeat(14))));
|
|
480
|
+
choices.push({ name: chalk.blue('↩️ 返回主菜单'), value: 'back' });
|
|
481
|
+
|
|
482
|
+
const { channelId } = await inquirer.prompt([
|
|
483
|
+
{
|
|
484
|
+
type: 'list',
|
|
485
|
+
name: 'channelId',
|
|
486
|
+
message: '选择要切换的渠道(需要重启生效):',
|
|
487
|
+
pageSize: 15,
|
|
488
|
+
choices
|
|
489
|
+
}
|
|
490
|
+
]);
|
|
491
|
+
|
|
492
|
+
if (channelId === 'back') {
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
try {
|
|
497
|
+
await services.activateChannel(channelId);
|
|
498
|
+
console.log(chalk.green('\n✅ 渠道已切换\n'));
|
|
499
|
+
} catch (error) {
|
|
500
|
+
console.log(chalk.red(`\n❌ 操作失败: ${error.message}\n`));
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
await inquirer.prompt([{ type: 'input', name: 'continue', message: '按回车返回...' }]);
|
|
504
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// CLI类型切换命令
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const inquirer = require('inquirer');
|
|
4
|
+
const { loadConfig, saveConfig } = require('../config/loader');
|
|
5
|
+
|
|
6
|
+
const CLI_TYPES = {
|
|
7
|
+
claude: { name: 'Claude Code', color: 'cyan' },
|
|
8
|
+
codex: { name: 'Codex', color: 'green' },
|
|
9
|
+
gemini: { name: 'Gemini', color: 'magenta' }
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 切换CLI类型
|
|
14
|
+
*/
|
|
15
|
+
async function handleSwitchCliType() {
|
|
16
|
+
console.clear();
|
|
17
|
+
console.log(chalk.bold.cyan('\n╔═══════════════════════════════════════╗'));
|
|
18
|
+
console.log(chalk.bold.cyan('║ 切换 CLI 类型 ║'));
|
|
19
|
+
console.log(chalk.bold.cyan('╚═══════════════════════════════════════╝\n'));
|
|
20
|
+
|
|
21
|
+
const config = loadConfig();
|
|
22
|
+
const currentType = config.currentCliType || 'claude';
|
|
23
|
+
|
|
24
|
+
console.log(chalk.gray(`当前类型: ${CLI_TYPES[currentType].name}\n`));
|
|
25
|
+
|
|
26
|
+
// 构建类型选项
|
|
27
|
+
const choices = Object.entries(CLI_TYPES).map(([type, info]) => {
|
|
28
|
+
let name = '';
|
|
29
|
+
|
|
30
|
+
// 如果是当前类型,添加✓标记
|
|
31
|
+
if (type === currentType) {
|
|
32
|
+
name += chalk.green('✓ ');
|
|
33
|
+
} else {
|
|
34
|
+
name += ' ';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 类型名称
|
|
38
|
+
name += chalk[info.color](info.name);
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
name,
|
|
42
|
+
value: type
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// 添加返回选项
|
|
47
|
+
choices.push(new inquirer.Separator(chalk.gray('─'.repeat(14))));
|
|
48
|
+
choices.push({ name: chalk.gray('返回主菜单'), value: 'back' });
|
|
49
|
+
|
|
50
|
+
const { selectedType } = await inquirer.prompt([
|
|
51
|
+
{
|
|
52
|
+
type: 'list',
|
|
53
|
+
name: 'selectedType',
|
|
54
|
+
message: '请选择 CLI 类型:',
|
|
55
|
+
choices: choices,
|
|
56
|
+
},
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
if (selectedType === 'back') {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (selectedType === currentType) {
|
|
64
|
+
console.log(chalk.yellow('\n已经是当前类型了\n'));
|
|
65
|
+
await inquirer.prompt([
|
|
66
|
+
{
|
|
67
|
+
type: 'list',
|
|
68
|
+
name: 'action',
|
|
69
|
+
message: '请选择操作:',
|
|
70
|
+
choices: [
|
|
71
|
+
{ name: chalk.blue('返回主菜单'), value: 'back' },
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
]);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 保存新的类型
|
|
79
|
+
config.currentCliType = selectedType;
|
|
80
|
+
saveConfig(config);
|
|
81
|
+
|
|
82
|
+
console.log(chalk.green(`\n✅ 已切换到 ${CLI_TYPES[selectedType].name}\n`));
|
|
83
|
+
console.log(chalk.gray('下次启动时将使用新的类型\n'));
|
|
84
|
+
|
|
85
|
+
await inquirer.prompt([
|
|
86
|
+
{
|
|
87
|
+
type: 'list',
|
|
88
|
+
name: 'action',
|
|
89
|
+
message: '请选择操作:',
|
|
90
|
+
choices: [
|
|
91
|
+
{ name: chalk.blue('返回主菜单'), value: 'back' },
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
]);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module.exports = {
|
|
98
|
+
handleSwitchCliType
|
|
99
|
+
};
|