openclawapi 1.0.0 → 1.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/README.md +120 -26
- package/cli.js +1243 -104
- package/lib/config-manager.js +405 -54
- package/package.json +3 -2
package/cli.js
CHANGED
|
@@ -2,35 +2,249 @@
|
|
|
2
2
|
|
|
3
3
|
const inquirer = require('inquirer');
|
|
4
4
|
const chalk = require('chalk');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const JSON5 = require('json5');
|
|
5
7
|
const path = require('path');
|
|
6
8
|
const os = require('os');
|
|
7
|
-
const { ConfigManager } = require('./lib/config-manager');
|
|
8
|
-
const { displayMenu, displaySuccess, displayError, displayInfo } = require('./lib/ui');
|
|
9
|
+
const { ConfigManager, getDefaultWorkspace } = require('./lib/config-manager');
|
|
10
|
+
const { displayMenu, displaySuccess, displayError, displayInfo, displayWarning } = require('./lib/ui');
|
|
9
11
|
const { testMultipleRelays, sortBySpeed, formatLatency } = require('./lib/speed-test');
|
|
10
12
|
|
|
13
|
+
const RELAY_ENDPOINT_PRESETS = {
|
|
14
|
+
'yunyi-claude': [
|
|
15
|
+
{ name: '国内节点1', url: 'https://yunyi.rdzhvip.com' },
|
|
16
|
+
{ name: '国内节点2', url: 'https://yunyi.skem.cn' },
|
|
17
|
+
{ name: 'CF国外节点1', url: 'https://yunyi.cfd' },
|
|
18
|
+
{ name: 'CF国外节点2', url: 'https://cdn1.yunyi.cfd' },
|
|
19
|
+
{ name: 'CF国外节点3', url: 'https://cdn2.yunyi.cfd' },
|
|
20
|
+
{ name: '备用节点1', url: 'http://47.99.42.193' },
|
|
21
|
+
{ name: '备用节点2', url: 'http://47.97.100.10' }
|
|
22
|
+
],
|
|
23
|
+
'yunyi-codex': [
|
|
24
|
+
{ name: '国内节点1', url: 'https://yunyi.rdzhvip.com' },
|
|
25
|
+
{ name: '国内节点2', url: 'https://yunyi.skem.cn' },
|
|
26
|
+
{ name: 'CF国外节点1', url: 'https://yunyi.cfd' },
|
|
27
|
+
{ name: 'CF国外节点2', url: 'https://cdn1.yunyi.cfd' },
|
|
28
|
+
{ name: 'CF国外节点3', url: 'https://cdn2.yunyi.cfd' },
|
|
29
|
+
{ name: '备用节点1', url: 'http://47.99.42.193' },
|
|
30
|
+
{ name: '备用节点2', url: 'http://47.97.100.10' }
|
|
31
|
+
]
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const MODEL_PRESETS = {
|
|
35
|
+
'yunyi-claude': [
|
|
36
|
+
{ id: 'claude-opus-4-5', name: 'Claude Opus 4.5' },
|
|
37
|
+
{ id: 'claude-sonnet-4-5', name: 'Claude Sonnet 4.5' },
|
|
38
|
+
{ id: 'claude-haiku-4-5', name: 'Claude Haiku 4.5' }
|
|
39
|
+
],
|
|
40
|
+
'yunyi-codex': [
|
|
41
|
+
{ id: 'gpt-5.2', name: 'GPT 5.2' },
|
|
42
|
+
{ id: 'gpt-5.2-codex', name: 'GPT 5.2 Codex' }
|
|
43
|
+
]
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function normalizeRelayKey(relayName) {
|
|
47
|
+
if (!relayName) return '';
|
|
48
|
+
if (relayName.startsWith('yunyi-claude')) return 'yunyi-claude';
|
|
49
|
+
if (relayName.startsWith('yunyi-codex')) return 'yunyi-codex';
|
|
50
|
+
return relayName;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getRelaySuffix(relayName) {
|
|
54
|
+
const key = normalizeRelayKey(relayName);
|
|
55
|
+
if (key === 'yunyi-claude') return '/claude/v1/messages';
|
|
56
|
+
if (key === 'yunyi-codex') return '/codex/response';
|
|
57
|
+
return '';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function buildRelayBaseUrl(baseUrl, relayName) {
|
|
61
|
+
const suffix = getRelaySuffix(relayName);
|
|
62
|
+
if (!suffix) return baseUrl;
|
|
63
|
+
if (!baseUrl) return baseUrl;
|
|
64
|
+
const trimmed = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
|
|
65
|
+
|
|
66
|
+
if (suffix === '/claude/v1/messages') {
|
|
67
|
+
if (/\/claude\/v1\/messages$/.test(trimmed) || /\/claude\/v1\/message$/.test(trimmed)) {
|
|
68
|
+
return trimmed;
|
|
69
|
+
}
|
|
70
|
+
if (/\/claude\/v1$/.test(trimmed)) {
|
|
71
|
+
return `${trimmed}/messages`;
|
|
72
|
+
}
|
|
73
|
+
if (/\/claude$/.test(trimmed)) {
|
|
74
|
+
return `${trimmed}/v1/messages`;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (suffix === '/codex/response') {
|
|
79
|
+
if (/\/codex\/response$/.test(trimmed)) {
|
|
80
|
+
return trimmed;
|
|
81
|
+
}
|
|
82
|
+
if (/\/codex$/.test(trimmed)) {
|
|
83
|
+
return `${trimmed}/response`;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (/\/claude(\/|$)/.test(trimmed) || /\/codex(\/|$)/.test(trimmed)) {
|
|
88
|
+
return trimmed;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return `${trimmed}${suffix}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function getRelayEndpoints(relayName) {
|
|
95
|
+
return RELAY_ENDPOINT_PRESETS[normalizeRelayKey(relayName)] || [];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function getRelayModelPresets(relayName) {
|
|
99
|
+
return MODEL_PRESETS[normalizeRelayKey(relayName)] || [];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function buildModelEntry(model, apiType) {
|
|
103
|
+
const defaults = apiType === 'openai-responses'
|
|
104
|
+
? { contextWindow: 128000, maxTokens: 32768, reasoning: true, input: ['text', 'image'] }
|
|
105
|
+
: { contextWindow: 200000, maxTokens: 8192, reasoning: false, input: ['text'] };
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
id: model.id,
|
|
109
|
+
name: model.name || model.id,
|
|
110
|
+
reasoning: defaults.reasoning,
|
|
111
|
+
input: defaults.input,
|
|
112
|
+
cost: {
|
|
113
|
+
input: 0,
|
|
114
|
+
output: 0,
|
|
115
|
+
cacheRead: 0,
|
|
116
|
+
cacheWrite: 0
|
|
117
|
+
},
|
|
118
|
+
contextWindow: defaults.contextWindow,
|
|
119
|
+
maxTokens: defaults.maxTokens
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
11
123
|
// 获取配置文件路径(跨平台)
|
|
12
124
|
function getConfigPath() {
|
|
13
125
|
const homeDir = os.homedir();
|
|
14
|
-
const
|
|
126
|
+
const openclawStateDir = process.env.OPENCLAW_STATE_DIR || path.join(homeDir, '.openclaw');
|
|
127
|
+
const clawdbotStateDir = process.env.CLAWDBOT_STATE_DIR || path.join(homeDir, '.clawdbot');
|
|
128
|
+
|
|
129
|
+
const envConfig =
|
|
130
|
+
process.env.OPENCLAW_CONFIG_PATH ||
|
|
131
|
+
process.env.CLAWDBOT_CONFIG_PATH ||
|
|
132
|
+
process.env.OPENCLAW_CONFIG;
|
|
133
|
+
|
|
134
|
+
const openclawConfig = envConfig || ([
|
|
135
|
+
path.join(openclawStateDir, 'openclaw.json'),
|
|
136
|
+
path.join(clawdbotStateDir, 'openclaw.json'),
|
|
137
|
+
path.join(clawdbotStateDir, 'clawdbot.json'),
|
|
138
|
+
path.join(clawdbotStateDir, 'moltbot.json')
|
|
139
|
+
].find(p => fs.existsSync(p)) || path.join(openclawStateDir, 'openclaw.json'));
|
|
140
|
+
|
|
141
|
+
const configDir = path.dirname(openclawConfig);
|
|
142
|
+
const authProfiles = resolveAuthProfilesPath({
|
|
143
|
+
configDir,
|
|
144
|
+
openclawStateDir,
|
|
145
|
+
clawdbotStateDir
|
|
146
|
+
});
|
|
147
|
+
|
|
15
148
|
return {
|
|
16
|
-
openclawConfig
|
|
17
|
-
authProfiles
|
|
149
|
+
openclawConfig,
|
|
150
|
+
authProfiles
|
|
18
151
|
};
|
|
19
152
|
}
|
|
20
153
|
|
|
154
|
+
function resolveAuthProfilesPath({ configDir, openclawStateDir, clawdbotStateDir }) {
|
|
155
|
+
const envAgentDir =
|
|
156
|
+
process.env.OPENCLAW_AGENT_DIR ||
|
|
157
|
+
process.env.CLAWDBOT_AGENT_DIR ||
|
|
158
|
+
process.env.PI_CODING_AGENT_DIR;
|
|
159
|
+
const envAgentName =
|
|
160
|
+
process.env.OPENCLAW_AGENT ||
|
|
161
|
+
process.env.CLAWDBOT_AGENT ||
|
|
162
|
+
process.env.PI_CODING_AGENT;
|
|
163
|
+
|
|
164
|
+
const candidates = [];
|
|
165
|
+
|
|
166
|
+
if (envAgentDir) {
|
|
167
|
+
candidates.push(path.join(envAgentDir, 'auth-profiles.json'));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (envAgentName) {
|
|
171
|
+
candidates.push(path.join(openclawStateDir, 'agents', envAgentName, 'agent', 'auth-profiles.json'));
|
|
172
|
+
candidates.push(path.join(clawdbotStateDir, 'agents', envAgentName, 'agent', 'auth-profiles.json'));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// OpenClaw default + legacy paths
|
|
176
|
+
candidates.push(path.join(openclawStateDir, 'agents', 'main', 'agent', 'auth-profiles.json'));
|
|
177
|
+
candidates.push(path.join(openclawStateDir, 'agent', 'auth-profiles.json'));
|
|
178
|
+
|
|
179
|
+
// Clawdbot default + legacy paths
|
|
180
|
+
candidates.push(path.join(clawdbotStateDir, 'agent', 'auth-profiles.json'));
|
|
181
|
+
candidates.push(path.join(clawdbotStateDir, 'agents', 'main', 'agent', 'auth-profiles.json'));
|
|
182
|
+
|
|
183
|
+
// Fallback to config directory if user stores alongside config
|
|
184
|
+
candidates.push(path.join(configDir, 'agent', 'auth-profiles.json'));
|
|
185
|
+
candidates.push(path.join(configDir, 'agents', 'main', 'agent', 'auth-profiles.json'));
|
|
186
|
+
|
|
187
|
+
const inferred = inferSingleAgentAuthPath(openclawStateDir, clawdbotStateDir);
|
|
188
|
+
if (inferred) {
|
|
189
|
+
candidates.unshift(inferred);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return candidates.find(p => fs.existsSync(p)) || candidates[0];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function inferSingleAgentAuthPath(openclawStateDir, clawdbotStateDir) {
|
|
196
|
+
const openclawAgentsDir = path.join(openclawStateDir, 'agents');
|
|
197
|
+
if (fs.existsSync(openclawAgentsDir)) {
|
|
198
|
+
const dirs = fs.readdirSync(openclawAgentsDir, { withFileTypes: true })
|
|
199
|
+
.filter(entry => entry.isDirectory())
|
|
200
|
+
.map(entry => entry.name);
|
|
201
|
+
if (dirs.length === 1) {
|
|
202
|
+
return path.join(openclawAgentsDir, dirs[0], 'agent', 'auth-profiles.json');
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const clawdbotAgentsDir = path.join(clawdbotStateDir, 'agents');
|
|
207
|
+
if (fs.existsSync(clawdbotAgentsDir)) {
|
|
208
|
+
const dirs = fs.readdirSync(clawdbotAgentsDir, { withFileTypes: true })
|
|
209
|
+
.filter(entry => entry.isDirectory())
|
|
210
|
+
.map(entry => entry.name);
|
|
211
|
+
if (dirs.length === 1) {
|
|
212
|
+
return path.join(clawdbotAgentsDir, dirs[0], 'agent', 'auth-profiles.json');
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return '';
|
|
217
|
+
}
|
|
218
|
+
|
|
21
219
|
async function main() {
|
|
22
220
|
console.clear();
|
|
23
221
|
console.log(chalk.cyan.bold('\n🔧 OpenClaw 配置管理工具\n'));
|
|
24
222
|
|
|
223
|
+
// 显示平台信息
|
|
224
|
+
const platformInfo = {
|
|
225
|
+
win32: 'Windows',
|
|
226
|
+
darwin: 'macOS',
|
|
227
|
+
linux: 'Linux'
|
|
228
|
+
};
|
|
229
|
+
console.log(chalk.gray(`平台: ${platformInfo[process.platform] || process.platform}`));
|
|
25
230
|
const configPaths = getConfigPath();
|
|
231
|
+
console.log(chalk.gray(`配置目录: ${path.dirname(configPaths.openclawConfig)}`));
|
|
232
|
+
console.log(chalk.gray(`配置文件: ${configPaths.openclawConfig}\n`));
|
|
233
|
+
console.log(chalk.gray(`认证文件: ${configPaths.authProfiles}\n`));
|
|
26
234
|
const configManager = new ConfigManager(configPaths);
|
|
27
235
|
|
|
28
|
-
//
|
|
236
|
+
// 检查配置文件是否存在,如果不存在则自动初始化
|
|
29
237
|
const exists = await configManager.checkConfigExists();
|
|
30
238
|
if (!exists.openclaw) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
239
|
+
displayInfo('配置文件不存在,正在初始化...');
|
|
240
|
+
try {
|
|
241
|
+
await configManager.initializeConfig();
|
|
242
|
+
displaySuccess('配置文件初始化成功!');
|
|
243
|
+
} catch (error) {
|
|
244
|
+
displayError(`初始化失败: ${error.message}`);
|
|
245
|
+
displayInfo('请检查目录权限或手动创建配置文件');
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
34
248
|
}
|
|
35
249
|
|
|
36
250
|
while (true) {
|
|
@@ -40,9 +254,13 @@ async function main() {
|
|
|
40
254
|
name: 'action',
|
|
41
255
|
message: '请选择操作:',
|
|
42
256
|
choices: [
|
|
257
|
+
{ name: '🚀 快速配置向导 (URL/Key/Token/Workspace)', value: 'quicksetup' },
|
|
258
|
+
new inquirer.Separator(),
|
|
43
259
|
{ name: '📡 管理中转站配置', value: 'relay' },
|
|
260
|
+
{ name: '🧭 快速切换中转端点', value: 'endpoint' },
|
|
44
261
|
{ name: '🤖 管理模型配置', value: 'model' },
|
|
45
|
-
{ name: '
|
|
262
|
+
{ name: '🧩 快速切换模型', value: 'modelSwitch' },
|
|
263
|
+
{ name: '🔑 管理 API Keys / Tokens', value: 'apikey' },
|
|
46
264
|
{ name: '⚡ 测速并切换中转站', value: 'speedtest' },
|
|
47
265
|
{ name: '⚙️ 高级设置', value: 'advanced' },
|
|
48
266
|
{ name: '📄 查看当前配置', value: 'view' },
|
|
@@ -59,12 +277,21 @@ async function main() {
|
|
|
59
277
|
|
|
60
278
|
try {
|
|
61
279
|
switch (action) {
|
|
280
|
+
case 'quicksetup':
|
|
281
|
+
await quickSetup(configManager);
|
|
282
|
+
break;
|
|
62
283
|
case 'relay':
|
|
63
284
|
await manageRelay(configManager);
|
|
64
285
|
break;
|
|
286
|
+
case 'endpoint':
|
|
287
|
+
await quickSwitchEndpoint(configManager);
|
|
288
|
+
break;
|
|
65
289
|
case 'model':
|
|
66
290
|
await manageModel(configManager);
|
|
67
291
|
break;
|
|
292
|
+
case 'modelSwitch':
|
|
293
|
+
await quickSwitchModel(configManager);
|
|
294
|
+
break;
|
|
68
295
|
case 'apikey':
|
|
69
296
|
await manageApiKey(configManager);
|
|
70
297
|
break;
|
|
@@ -86,6 +313,604 @@ async function main() {
|
|
|
86
313
|
}
|
|
87
314
|
}
|
|
88
315
|
|
|
316
|
+
// 快速配置向导 - 一次性配置 URL、Key、Token、Workspace
|
|
317
|
+
async function quickSetup(configManager) {
|
|
318
|
+
console.log(chalk.cyan.bold('\n🚀 快速配置向导\n'));
|
|
319
|
+
console.log(chalk.gray('此向导将帮助您快速配置中转站的 URL、API Key、Token 和工作区\n'));
|
|
320
|
+
|
|
321
|
+
const defaultWorkspace = getDefaultWorkspace();
|
|
322
|
+
|
|
323
|
+
const presetChoices = [
|
|
324
|
+
{ name: 'Claude (云逸 国内) https://yunyi.skem.cn/claude', value: 'yunyi-claude-cn' },
|
|
325
|
+
{ name: 'Claude (云逸 海外) https://yunyi.cfd/claude', value: 'yunyi-claude-global' },
|
|
326
|
+
{ name: 'Codex (云逸 国内) https://yunyi.skem.cn/codex', value: 'yunyi-codex-cn' },
|
|
327
|
+
{ name: 'Codex (云逸 海外) https://yunyi.cfd/codex', value: 'yunyi-codex-global' },
|
|
328
|
+
{ name: '自定义 (其他模型)', value: 'custom' }
|
|
329
|
+
];
|
|
330
|
+
|
|
331
|
+
const presetMap = {
|
|
332
|
+
'yunyi-claude-cn': {
|
|
333
|
+
name: 'yunyi-claude',
|
|
334
|
+
baseUrl: 'https://yunyi.skem.cn/claude/v1/messages',
|
|
335
|
+
modelType: 'claude',
|
|
336
|
+
api: 'anthropic-messages',
|
|
337
|
+
defaultModelId: 'claude-opus-4-5',
|
|
338
|
+
compactionMode: 'safeguard'
|
|
339
|
+
},
|
|
340
|
+
'yunyi-claude-global': {
|
|
341
|
+
name: 'yunyi-claude',
|
|
342
|
+
baseUrl: 'https://yunyi.cfd/claude/v1/messages',
|
|
343
|
+
modelType: 'claude',
|
|
344
|
+
api: 'anthropic-messages',
|
|
345
|
+
defaultModelId: 'claude-opus-4-5',
|
|
346
|
+
compactionMode: 'safeguard'
|
|
347
|
+
},
|
|
348
|
+
'yunyi-codex-cn': {
|
|
349
|
+
name: 'yunyi-codex',
|
|
350
|
+
baseUrl: 'https://yunyi.skem.cn/codex/response',
|
|
351
|
+
modelType: 'codex',
|
|
352
|
+
api: 'openai-responses',
|
|
353
|
+
defaultModelId: 'gpt-5.2',
|
|
354
|
+
defaultModelName: 'GPT 5.2',
|
|
355
|
+
compactionMode: 'safeguard'
|
|
356
|
+
},
|
|
357
|
+
'yunyi-codex-global': {
|
|
358
|
+
name: 'yunyi-codex',
|
|
359
|
+
baseUrl: 'https://yunyi.cfd/codex/response',
|
|
360
|
+
modelType: 'codex',
|
|
361
|
+
api: 'openai-responses',
|
|
362
|
+
defaultModelId: 'gpt-5.2',
|
|
363
|
+
defaultModelName: 'GPT 5.2',
|
|
364
|
+
compactionMode: 'safeguard'
|
|
365
|
+
},
|
|
366
|
+
custom: {}
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
const { preset } = await inquirer.prompt([
|
|
370
|
+
{ type: 'list', name: 'preset', message: '选择模板:', choices: presetChoices }
|
|
371
|
+
]);
|
|
372
|
+
const presetConfig = presetMap[preset] || {};
|
|
373
|
+
|
|
374
|
+
// 步骤 1: 中转站名称和 URL
|
|
375
|
+
const step1 = await inquirer.prompt([
|
|
376
|
+
{
|
|
377
|
+
type: 'input',
|
|
378
|
+
name: 'name',
|
|
379
|
+
message: '中转站名称 (例如: my-relay):',
|
|
380
|
+
default: presetConfig.name || 'default-relay',
|
|
381
|
+
validate: (input) => input.trim() !== '' || '名称不能为空'
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
type: 'input',
|
|
385
|
+
name: 'baseUrl',
|
|
386
|
+
message: '中转站 URL (例如: https://api.example.com/v1):',
|
|
387
|
+
default: presetConfig.baseUrl || '',
|
|
388
|
+
validate: (input) => {
|
|
389
|
+
try {
|
|
390
|
+
const url = new URL(input);
|
|
391
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
392
|
+
return '请输入有效的 HTTP/HTTPS URL';
|
|
393
|
+
}
|
|
394
|
+
return true;
|
|
395
|
+
} catch {
|
|
396
|
+
return '请输入有效的 URL';
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
]);
|
|
401
|
+
|
|
402
|
+
step1.baseUrl = buildRelayBaseUrl(step1.baseUrl, step1.name);
|
|
403
|
+
|
|
404
|
+
let backupConfig = null;
|
|
405
|
+
const { enableBackup } = await inquirer.prompt([
|
|
406
|
+
{
|
|
407
|
+
type: 'confirm',
|
|
408
|
+
name: 'enableBackup',
|
|
409
|
+
message: '是否添加备用中转端点(用于主备切换)?',
|
|
410
|
+
default: false
|
|
411
|
+
}
|
|
412
|
+
]);
|
|
413
|
+
|
|
414
|
+
if (enableBackup) {
|
|
415
|
+
const endpoints = getRelayEndpoints(step1.name);
|
|
416
|
+
const fallbackEndpoint = endpoints.find(item => item.url !== step1.baseUrl);
|
|
417
|
+
const backupAnswers = await inquirer.prompt([
|
|
418
|
+
{
|
|
419
|
+
type: 'input',
|
|
420
|
+
name: 'name',
|
|
421
|
+
message: '备用中转站名称:',
|
|
422
|
+
default: `${step1.name}-backup`,
|
|
423
|
+
validate: (input) => input.trim() !== '' || '名称不能为空'
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
type: 'input',
|
|
427
|
+
name: 'baseUrl',
|
|
428
|
+
message: '备用端点 Base URL:',
|
|
429
|
+
default: fallbackEndpoint ? fallbackEndpoint.url : '',
|
|
430
|
+
validate: (input) => {
|
|
431
|
+
try {
|
|
432
|
+
const url = new URL(input);
|
|
433
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
434
|
+
return '请输入有效的 HTTP/HTTPS URL';
|
|
435
|
+
}
|
|
436
|
+
return true;
|
|
437
|
+
} catch {
|
|
438
|
+
return '请输入有效的 URL';
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
]);
|
|
443
|
+
backupConfig = {
|
|
444
|
+
name: backupAnswers.name.trim(),
|
|
445
|
+
baseUrl: buildRelayBaseUrl(backupAnswers.baseUrl.trim(), step1.name)
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// 步骤 2: 模型类型(仅支持 Claude / Codex)
|
|
450
|
+
let modelType = presetConfig.modelType;
|
|
451
|
+
if (!modelType) {
|
|
452
|
+
const result = await inquirer.prompt([
|
|
453
|
+
{
|
|
454
|
+
type: 'list',
|
|
455
|
+
name: 'modelType',
|
|
456
|
+
message: '选择模型类型:',
|
|
457
|
+
choices: [
|
|
458
|
+
{ name: 'Claude (Anthropic)', value: 'claude' },
|
|
459
|
+
{ name: 'Codex / GPT (OpenAI Responses)', value: 'codex' },
|
|
460
|
+
{ name: '其他/自定义', value: 'other' }
|
|
461
|
+
]
|
|
462
|
+
}
|
|
463
|
+
]);
|
|
464
|
+
modelType = result.modelType;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (modelType === 'other') {
|
|
468
|
+
displayWarning('非 GPT/Claude 模型仅做配置写入,不保证可用,请优先用 OpenClaw 配置页面。');
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// 步骤 3: API 类型
|
|
472
|
+
let apiType = presetConfig.api;
|
|
473
|
+
if (!apiType) {
|
|
474
|
+
if (modelType === 'codex') {
|
|
475
|
+
apiType = 'openai-responses';
|
|
476
|
+
} else if (modelType === 'claude') {
|
|
477
|
+
apiType = 'anthropic-messages';
|
|
478
|
+
} else {
|
|
479
|
+
const result = await inquirer.prompt([
|
|
480
|
+
{
|
|
481
|
+
type: 'list',
|
|
482
|
+
name: 'apiType',
|
|
483
|
+
message: 'API 接口类型:',
|
|
484
|
+
choices: [
|
|
485
|
+
{ name: 'Anthropic Messages (Claude)', value: 'anthropic-messages' },
|
|
486
|
+
{ name: 'OpenAI Responses (Codex / GPT-5+)', value: 'openai-responses' },
|
|
487
|
+
{ name: 'OpenAI Completions', value: 'openai-completions' },
|
|
488
|
+
{ name: 'Google Generative AI (Gemini)', value: 'google-generative-ai' },
|
|
489
|
+
{ name: '自定义', value: 'custom' }
|
|
490
|
+
]
|
|
491
|
+
}
|
|
492
|
+
]);
|
|
493
|
+
apiType = result.apiType;
|
|
494
|
+
if (apiType === 'custom') {
|
|
495
|
+
const custom = await inquirer.prompt([
|
|
496
|
+
{ type: 'input', name: 'customApi', message: '输入 API 类型:', validate: (i) => i.trim() !== '' || '不能为空' }
|
|
497
|
+
]);
|
|
498
|
+
apiType = custom.customApi;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// 步骤 4: 模型选择
|
|
504
|
+
let modelConfig;
|
|
505
|
+
if (modelType === 'claude') {
|
|
506
|
+
const { modelChoice } = await inquirer.prompt([
|
|
507
|
+
{
|
|
508
|
+
type: 'list',
|
|
509
|
+
name: 'modelChoice',
|
|
510
|
+
message: '选择 Claude 模型:',
|
|
511
|
+
choices: [
|
|
512
|
+
{ name: 'Claude Opus 4.5', value: { id: 'claude-opus-4-5', name: 'Claude Opus 4.5' } },
|
|
513
|
+
{ name: 'Claude Sonnet 4.5', value: { id: 'claude-sonnet-4-5', name: 'Claude Sonnet 4.5' } },
|
|
514
|
+
{ name: 'Claude Haiku 4.5', value: { id: 'claude-haiku-4-5', name: 'Claude Haiku 4.5' } }
|
|
515
|
+
],
|
|
516
|
+
default: presetConfig.defaultModelId
|
|
517
|
+
}
|
|
518
|
+
]);
|
|
519
|
+
modelConfig = modelChoice;
|
|
520
|
+
} else if (modelType === 'codex') {
|
|
521
|
+
const { modelChoice } = await inquirer.prompt([
|
|
522
|
+
{
|
|
523
|
+
type: 'list',
|
|
524
|
+
name: 'modelChoice',
|
|
525
|
+
message: '选择 Codex / GPT 模型:',
|
|
526
|
+
choices: [
|
|
527
|
+
{ name: 'GPT 5.2', value: { id: 'gpt-5.2', name: 'GPT 5.2' } },
|
|
528
|
+
{ name: 'GPT 5.2 Codex', value: { id: 'gpt-5.2-codex', name: 'GPT 5.2 Codex' } }
|
|
529
|
+
]
|
|
530
|
+
}
|
|
531
|
+
]);
|
|
532
|
+
modelConfig = modelChoice;
|
|
533
|
+
} else {
|
|
534
|
+
const custom = await inquirer.prompt([
|
|
535
|
+
{ type: 'input', name: 'id', message: '模型 ID:', validate: (i) => i.trim() !== '' || '不能为空' },
|
|
536
|
+
{ type: 'input', name: 'name', message: '模型名称:', validate: (i) => i.trim() !== '' || '不能为空' }
|
|
537
|
+
]);
|
|
538
|
+
modelConfig = { id: custom.id.trim(), name: custom.name.trim() };
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const includeModelsList = apiType !== 'anthropic-messages';
|
|
542
|
+
let modelEntry = null;
|
|
543
|
+
|
|
544
|
+
if (includeModelsList) {
|
|
545
|
+
const defaults = modelType === 'codex'
|
|
546
|
+
? { contextWindow: 128000, maxTokens: 32768 }
|
|
547
|
+
: { contextWindow: 200000, maxTokens: 8192 };
|
|
548
|
+
const { contextWindow, maxTokens } = await inquirer.prompt([
|
|
549
|
+
{
|
|
550
|
+
type: 'number',
|
|
551
|
+
name: 'contextWindow',
|
|
552
|
+
message: '上下文窗口大小:',
|
|
553
|
+
default: defaults.contextWindow
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
type: 'number',
|
|
557
|
+
name: 'maxTokens',
|
|
558
|
+
message: '最大输出 tokens:',
|
|
559
|
+
default: defaults.maxTokens
|
|
560
|
+
}
|
|
561
|
+
]);
|
|
562
|
+
modelEntry = {
|
|
563
|
+
id: modelConfig.id,
|
|
564
|
+
name: modelConfig.name,
|
|
565
|
+
reasoning: apiType === 'openai-responses',
|
|
566
|
+
input: apiType === 'openai-responses' ? ['text', 'image'] : ['text'],
|
|
567
|
+
cost: {
|
|
568
|
+
input: 0,
|
|
569
|
+
output: 0,
|
|
570
|
+
cacheRead: 0,
|
|
571
|
+
cacheWrite: 0
|
|
572
|
+
},
|
|
573
|
+
contextWindow,
|
|
574
|
+
maxTokens
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// 步骤 5: API Key / Token
|
|
579
|
+
const step3 = await inquirer.prompt([
|
|
580
|
+
{
|
|
581
|
+
type: 'password',
|
|
582
|
+
name: 'apiKey',
|
|
583
|
+
message: 'API Key (留空跳过):',
|
|
584
|
+
mask: '*'
|
|
585
|
+
},
|
|
586
|
+
{
|
|
587
|
+
type: 'password',
|
|
588
|
+
name: 'providerToken',
|
|
589
|
+
message: '中转站 Token (留空跳过):',
|
|
590
|
+
mask: '*'
|
|
591
|
+
},
|
|
592
|
+
{
|
|
593
|
+
type: 'password',
|
|
594
|
+
name: 'gatewayToken',
|
|
595
|
+
message: '网关 Token (留空跳过):',
|
|
596
|
+
mask: '*'
|
|
597
|
+
}
|
|
598
|
+
]);
|
|
599
|
+
|
|
600
|
+
// 步骤 6: 工作区
|
|
601
|
+
const step4 = await inquirer.prompt([
|
|
602
|
+
{
|
|
603
|
+
type: 'input',
|
|
604
|
+
name: 'workspace',
|
|
605
|
+
message: `工作区路径 (留空使用默认):`,
|
|
606
|
+
default: defaultWorkspace
|
|
607
|
+
}
|
|
608
|
+
]);
|
|
609
|
+
|
|
610
|
+
// 执行配置
|
|
611
|
+
console.log(chalk.cyan('\n正在保存配置...\n'));
|
|
612
|
+
|
|
613
|
+
try {
|
|
614
|
+
// 添加中转站
|
|
615
|
+
await configManager.addRelay({
|
|
616
|
+
name: step1.name,
|
|
617
|
+
baseUrl: step1.baseUrl,
|
|
618
|
+
auth: 'api-key',
|
|
619
|
+
api: apiType,
|
|
620
|
+
headers: {},
|
|
621
|
+
authHeader: false,
|
|
622
|
+
modelsMode: 'merge',
|
|
623
|
+
model: {
|
|
624
|
+
id: modelConfig.id,
|
|
625
|
+
name: modelConfig.name,
|
|
626
|
+
contextWindow: includeModelsList ? modelEntry.contextWindow : undefined,
|
|
627
|
+
maxTokens: includeModelsList ? modelEntry.maxTokens : undefined,
|
|
628
|
+
reasoning: includeModelsList ? modelEntry.reasoning : undefined,
|
|
629
|
+
input: includeModelsList ? modelEntry.input : undefined,
|
|
630
|
+
cost: includeModelsList ? modelEntry.cost : undefined
|
|
631
|
+
},
|
|
632
|
+
models: includeModelsList ? [modelEntry] : []
|
|
633
|
+
});
|
|
634
|
+
console.log(chalk.green(' ✓ 中转站已添加'));
|
|
635
|
+
|
|
636
|
+
// 设置为主模型
|
|
637
|
+
await configManager.setPrimaryModel(step1.name, modelConfig.id);
|
|
638
|
+
console.log(chalk.green(' ✓ 已设为主模型'));
|
|
639
|
+
|
|
640
|
+
if (backupConfig) {
|
|
641
|
+
try {
|
|
642
|
+
await configManager.addRelay({
|
|
643
|
+
name: backupConfig.name,
|
|
644
|
+
baseUrl: backupConfig.baseUrl,
|
|
645
|
+
auth: 'api-key',
|
|
646
|
+
api: apiType,
|
|
647
|
+
headers: {},
|
|
648
|
+
authHeader: false,
|
|
649
|
+
modelsMode: 'merge',
|
|
650
|
+
model: {
|
|
651
|
+
id: modelConfig.id,
|
|
652
|
+
name: modelConfig.name,
|
|
653
|
+
contextWindow: includeModelsList ? modelEntry.contextWindow : undefined,
|
|
654
|
+
maxTokens: includeModelsList ? modelEntry.maxTokens : undefined,
|
|
655
|
+
reasoning: includeModelsList ? modelEntry.reasoning : undefined,
|
|
656
|
+
input: includeModelsList ? modelEntry.input : undefined,
|
|
657
|
+
cost: includeModelsList ? modelEntry.cost : undefined
|
|
658
|
+
},
|
|
659
|
+
models: includeModelsList ? [modelEntry] : []
|
|
660
|
+
});
|
|
661
|
+
console.log(chalk.green(' ✓ 备用中转站已添加'));
|
|
662
|
+
|
|
663
|
+
const existingFallbacks = await configManager.getFallbackModels();
|
|
664
|
+
const fallbackKey = `${backupConfig.name}/${modelConfig.id}`;
|
|
665
|
+
const mergedFallbacks = Array.from(new Set([...existingFallbacks, fallbackKey]));
|
|
666
|
+
await configManager.setFallbackModels(mergedFallbacks);
|
|
667
|
+
console.log(chalk.green(' ✓ 已设置备用模型'));
|
|
668
|
+
|
|
669
|
+
if (step3.apiKey && step3.apiKey.trim() !== '') {
|
|
670
|
+
const { useSameKey } = await inquirer.prompt([
|
|
671
|
+
{
|
|
672
|
+
type: 'confirm',
|
|
673
|
+
name: 'useSameKey',
|
|
674
|
+
message: '是否给备用中转站使用相同 API Key?',
|
|
675
|
+
default: true
|
|
676
|
+
}
|
|
677
|
+
]);
|
|
678
|
+
if (useSameKey) {
|
|
679
|
+
await configManager.setApiKey(backupConfig.name, step3.apiKey);
|
|
680
|
+
console.log(chalk.green(' ✓ 备用中转站 API Key 已保存'));
|
|
681
|
+
} else {
|
|
682
|
+
const { backupApiKey } = await inquirer.prompt([
|
|
683
|
+
{
|
|
684
|
+
type: 'password',
|
|
685
|
+
name: 'backupApiKey',
|
|
686
|
+
message: '输入备用中转站 API Key:',
|
|
687
|
+
mask: '*',
|
|
688
|
+
validate: (input) => input.trim() !== '' || 'API Key 不能为空'
|
|
689
|
+
}
|
|
690
|
+
]);
|
|
691
|
+
await configManager.setApiKey(backupConfig.name, backupApiKey);
|
|
692
|
+
console.log(chalk.green(' ✓ 备用中转站 API Key 已保存'));
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
} catch (error) {
|
|
696
|
+
displayWarning(`备用中转站添加失败: ${error.message}`);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// 保存 API Key
|
|
701
|
+
if (step3.apiKey && step3.apiKey.trim() !== '') {
|
|
702
|
+
await configManager.setApiKey(step1.name, step3.apiKey);
|
|
703
|
+
console.log(chalk.green(' ✓ API Key 已保存'));
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// 保存中转站 Token
|
|
707
|
+
if (step3.providerToken && step3.providerToken.trim() !== '') {
|
|
708
|
+
await configManager.setToken(step1.name, step3.providerToken);
|
|
709
|
+
console.log(chalk.green(' ✓ 中转站 Token 已保存'));
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// 设置工作区 / 网关 Token / 压缩模式
|
|
713
|
+
await configManager.setAdvancedSettings({
|
|
714
|
+
workspace: step4.workspace || defaultWorkspace,
|
|
715
|
+
compactionMode: presetConfig.compactionMode,
|
|
716
|
+
gatewayToken: step3.gatewayToken
|
|
717
|
+
});
|
|
718
|
+
console.log(chalk.green(' ✓ 高级设置已更新'));
|
|
719
|
+
|
|
720
|
+
displaySuccess('\n🎉 配置完成!您现在可以开始使用了。');
|
|
721
|
+
|
|
722
|
+
// 显示配置摘要
|
|
723
|
+
console.log(chalk.cyan('\n配置摘要:'));
|
|
724
|
+
console.log(` 中转站: ${step1.name}`);
|
|
725
|
+
console.log(` URL: ${step1.baseUrl}`);
|
|
726
|
+
console.log(` 模型: ${modelConfig.name}`);
|
|
727
|
+
console.log(` API: ${apiType}`);
|
|
728
|
+
console.log(` API Key: ${step3.apiKey ? chalk.green('已设置') : chalk.gray('未设置')}`);
|
|
729
|
+
console.log(` 网关 Token: ${step3.gatewayToken ? chalk.green('已设置') : chalk.gray('未设置')}`);
|
|
730
|
+
console.log(` 工作区: ${step4.workspace || defaultWorkspace}`);
|
|
731
|
+
|
|
732
|
+
} catch (error) {
|
|
733
|
+
displayError(`配置失败: ${error.message}`);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// 快速切换中转端点
|
|
738
|
+
async function quickSwitchEndpoint(configManager) {
|
|
739
|
+
const relays = await configManager.listRelays();
|
|
740
|
+
if (relays.length === 0) {
|
|
741
|
+
displayInfo('请先添加中转站');
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const { relayName } = await inquirer.prompt([
|
|
746
|
+
{
|
|
747
|
+
type: 'list',
|
|
748
|
+
name: 'relayName',
|
|
749
|
+
message: '选择要切换端点的中转站:',
|
|
750
|
+
choices: relays.map(r => ({ name: `${r.name} (${r.baseUrl})`, value: r.name }))
|
|
751
|
+
}
|
|
752
|
+
]);
|
|
753
|
+
|
|
754
|
+
const relay = relays.find(r => r.name === relayName);
|
|
755
|
+
const endpoints = getRelayEndpoints(relayName);
|
|
756
|
+
let selectedUrl = '';
|
|
757
|
+
|
|
758
|
+
if (endpoints.length > 0) {
|
|
759
|
+
const { endpoint } = await inquirer.prompt([
|
|
760
|
+
{
|
|
761
|
+
type: 'list',
|
|
762
|
+
name: 'endpoint',
|
|
763
|
+
message: '选择接入端点:',
|
|
764
|
+
choices: [
|
|
765
|
+
...endpoints.map(item => ({ name: item.name, value: item.url })),
|
|
766
|
+
{ name: '自定义', value: '__custom__' }
|
|
767
|
+
]
|
|
768
|
+
}
|
|
769
|
+
]);
|
|
770
|
+
|
|
771
|
+
if (endpoint === '__custom__') {
|
|
772
|
+
const custom = await inquirer.prompt([
|
|
773
|
+
{
|
|
774
|
+
type: 'input',
|
|
775
|
+
name: 'url',
|
|
776
|
+
message: '输入 Base URL:',
|
|
777
|
+
validate: (input) => {
|
|
778
|
+
try {
|
|
779
|
+
const url = new URL(input);
|
|
780
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
781
|
+
return '请输入有效的 HTTP/HTTPS URL';
|
|
782
|
+
}
|
|
783
|
+
return true;
|
|
784
|
+
} catch {
|
|
785
|
+
return '请输入有效的 URL';
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
]);
|
|
790
|
+
selectedUrl = custom.url;
|
|
791
|
+
} else {
|
|
792
|
+
selectedUrl = endpoint;
|
|
793
|
+
}
|
|
794
|
+
} else {
|
|
795
|
+
const custom = await inquirer.prompt([
|
|
796
|
+
{
|
|
797
|
+
type: 'input',
|
|
798
|
+
name: 'url',
|
|
799
|
+
message: '输入 Base URL:',
|
|
800
|
+
default: relay.baseUrl,
|
|
801
|
+
validate: (input) => {
|
|
802
|
+
try {
|
|
803
|
+
const url = new URL(input);
|
|
804
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
805
|
+
return '请输入有效的 HTTP/HTTPS URL';
|
|
806
|
+
}
|
|
807
|
+
return true;
|
|
808
|
+
} catch {
|
|
809
|
+
return '请输入有效的 URL';
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
]);
|
|
814
|
+
selectedUrl = custom.url;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
const finalUrl = buildRelayBaseUrl(selectedUrl, relayName);
|
|
818
|
+
if (finalUrl && finalUrl !== relay.baseUrl) {
|
|
819
|
+
await configManager.updateRelay(relayName, { baseUrl: finalUrl });
|
|
820
|
+
displaySuccess(`✅ 已切换 "${relayName}" 的端点`);
|
|
821
|
+
} else {
|
|
822
|
+
displayInfo('端点未变化');
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// 快速切换模型
|
|
827
|
+
async function quickSwitchModel(configManager) {
|
|
828
|
+
const relays = await configManager.listRelays();
|
|
829
|
+
if (relays.length === 0) {
|
|
830
|
+
displayInfo('请先添加中转站');
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
const { relayName } = await inquirer.prompt([
|
|
835
|
+
{
|
|
836
|
+
type: 'list',
|
|
837
|
+
name: 'relayName',
|
|
838
|
+
message: '选择要切换模型的中转站:',
|
|
839
|
+
choices: relays.map(r => ({ name: `${r.name} (${r.modelName})`, value: r.name }))
|
|
840
|
+
}
|
|
841
|
+
]);
|
|
842
|
+
|
|
843
|
+
const config = await configManager.readOpenclawConfig();
|
|
844
|
+
const provider = config.models?.providers?.[relayName] || {};
|
|
845
|
+
const apiType = provider.api || (normalizeRelayKey(relayName) === 'yunyi-codex'
|
|
846
|
+
? 'openai-responses'
|
|
847
|
+
: 'anthropic-messages');
|
|
848
|
+
const providerModels = Array.isArray(provider.models) ? provider.models : [];
|
|
849
|
+
const presetModels = getRelayModelPresets(relayName);
|
|
850
|
+
|
|
851
|
+
const choiceMap = new Map();
|
|
852
|
+
const choices = [];
|
|
853
|
+
|
|
854
|
+
const allowedIds = new Set(presetModels.map(m => m.id));
|
|
855
|
+
|
|
856
|
+
providerModels.forEach(m => {
|
|
857
|
+
const key = m.id;
|
|
858
|
+
if (allowedIds.has(key) && !choiceMap.has(key)) {
|
|
859
|
+
choiceMap.set(key, m);
|
|
860
|
+
choices.push({ name: `${m.name || m.id} (${m.id})`, value: m.id });
|
|
861
|
+
}
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
presetModels.forEach(m => {
|
|
865
|
+
if (!choiceMap.has(m.id)) {
|
|
866
|
+
choiceMap.set(m.id, m);
|
|
867
|
+
choices.push({ name: `${m.name} (${m.id})`, value: m.id });
|
|
868
|
+
}
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
if (choices.length === 0) {
|
|
872
|
+
displayInfo('没有可用的模型选项');
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
const { selected } = await inquirer.prompt([
|
|
877
|
+
{
|
|
878
|
+
type: 'list',
|
|
879
|
+
name: 'selected',
|
|
880
|
+
message: '选择模型:',
|
|
881
|
+
choices
|
|
882
|
+
}
|
|
883
|
+
]);
|
|
884
|
+
|
|
885
|
+
const selectedModel = choiceMap.get(selected);
|
|
886
|
+
const model = {
|
|
887
|
+
id: selected,
|
|
888
|
+
name: selectedModel?.name || selected
|
|
889
|
+
};
|
|
890
|
+
|
|
891
|
+
await configManager.setPrimaryModel(relayName, model.id);
|
|
892
|
+
displaySuccess(`✅ 已切换主模型为 "${relayName}/${model.id}"`);
|
|
893
|
+
|
|
894
|
+
if (apiType !== 'anthropic-messages' && Array.isArray(provider.models)) {
|
|
895
|
+
const exists = providerModels.some(m => m.id === model.id);
|
|
896
|
+
if (!exists) {
|
|
897
|
+
const { shouldAdd } = await inquirer.prompt([
|
|
898
|
+
{
|
|
899
|
+
type: 'confirm',
|
|
900
|
+
name: 'shouldAdd',
|
|
901
|
+
message: '是否将模型写入 providers.models 列表?',
|
|
902
|
+
default: false
|
|
903
|
+
}
|
|
904
|
+
]);
|
|
905
|
+
if (shouldAdd) {
|
|
906
|
+
provider.models.push(buildModelEntry(model, apiType));
|
|
907
|
+
await configManager.writeOpenclawConfig(config);
|
|
908
|
+
displaySuccess('✅ 已更新模型列表');
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
89
914
|
// 管理中转站配置
|
|
90
915
|
async function manageRelay(configManager) {
|
|
91
916
|
const { relayAction } = await inquirer.prompt([
|
|
@@ -151,88 +976,133 @@ async function addRelay(configManager) {
|
|
|
151
976
|
message: '模型类型:',
|
|
152
977
|
choices: [
|
|
153
978
|
{ name: 'Claude (Anthropic)', value: 'claude' },
|
|
154
|
-
{ name: 'GPT (OpenAI)', value: '
|
|
155
|
-
{ name: '
|
|
156
|
-
{ name: '其他', value: 'other' }
|
|
979
|
+
{ name: 'Codex / GPT (OpenAI Responses)', value: 'codex' },
|
|
980
|
+
{ name: '其他/自定义', value: 'other' }
|
|
157
981
|
]
|
|
158
982
|
}
|
|
159
983
|
]);
|
|
160
984
|
|
|
985
|
+
answers.baseUrl = buildRelayBaseUrl(answers.baseUrl, answers.name);
|
|
986
|
+
|
|
987
|
+
let apiType = answers.modelType === 'codex'
|
|
988
|
+
? 'openai-responses'
|
|
989
|
+
: 'anthropic-messages';
|
|
990
|
+
if (answers.modelType === 'other') {
|
|
991
|
+
displayWarning('非 GPT/Claude 模型仅做配置写入,不保证可用,请优先用 OpenClaw 配置页面。');
|
|
992
|
+
const result = await inquirer.prompt([
|
|
993
|
+
{
|
|
994
|
+
type: 'list',
|
|
995
|
+
name: 'apiType',
|
|
996
|
+
message: 'API 接口类型:',
|
|
997
|
+
choices: [
|
|
998
|
+
{ name: 'Anthropic Messages (Claude)', value: 'anthropic-messages' },
|
|
999
|
+
{ name: 'OpenAI Responses (Codex / GPT-5+)', value: 'openai-responses' },
|
|
1000
|
+
{ name: 'OpenAI Completions', value: 'openai-completions' },
|
|
1001
|
+
{ name: 'Google Generative AI (Gemini)', value: 'google-generative-ai' },
|
|
1002
|
+
{ name: '自定义', value: 'custom' }
|
|
1003
|
+
]
|
|
1004
|
+
}
|
|
1005
|
+
]);
|
|
1006
|
+
apiType = result.apiType;
|
|
1007
|
+
if (apiType === 'custom') {
|
|
1008
|
+
const custom = await inquirer.prompt([
|
|
1009
|
+
{ type: 'input', name: 'customApi', message: '输入 API 类型:', validate: (i) => i.trim() !== '' || '不能为空' }
|
|
1010
|
+
]);
|
|
1011
|
+
apiType = custom.customApi;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
161
1015
|
let modelConfig;
|
|
162
1016
|
if (answers.modelType === 'claude') {
|
|
163
|
-
const {
|
|
1017
|
+
const { modelChoice } = await inquirer.prompt([
|
|
164
1018
|
{
|
|
165
1019
|
type: 'list',
|
|
166
|
-
name: '
|
|
1020
|
+
name: 'modelChoice',
|
|
167
1021
|
message: '选择 Claude 模型:',
|
|
168
1022
|
choices: [
|
|
169
|
-
{ name: 'Claude
|
|
170
|
-
{ name: 'Claude Sonnet
|
|
171
|
-
{ name: 'Claude
|
|
172
|
-
{ name: '自定义', value: 'custom' }
|
|
1023
|
+
{ name: 'Claude Opus 4.5', value: { id: 'claude-opus-4-5', name: 'Claude Opus 4.5' } },
|
|
1024
|
+
{ name: 'Claude Sonnet 4.5', value: { id: 'claude-sonnet-4-5', name: 'Claude Sonnet 4.5' } },
|
|
1025
|
+
{ name: 'Claude Haiku 4.5', value: { id: 'claude-haiku-4-5', name: 'Claude Haiku 4.5' } }
|
|
173
1026
|
]
|
|
174
1027
|
}
|
|
175
1028
|
]);
|
|
176
1029
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
modelConfig = { id: 'gpt-4o', name: 'GPT-4o' };
|
|
192
|
-
} else if (answers.modelType === 'gemini') {
|
|
193
|
-
modelConfig = { id: 'gemini-3-pro', name: 'Gemini 3 Pro' };
|
|
1030
|
+
modelConfig = modelChoice;
|
|
1031
|
+
} else if (answers.modelType === 'codex') {
|
|
1032
|
+
const { modelChoice } = await inquirer.prompt([
|
|
1033
|
+
{
|
|
1034
|
+
type: 'list',
|
|
1035
|
+
name: 'modelChoice',
|
|
1036
|
+
message: '选择 Codex / GPT 模型:',
|
|
1037
|
+
choices: [
|
|
1038
|
+
{ name: 'GPT 5.2', value: { id: 'gpt-5.2', name: 'GPT 5.2' } },
|
|
1039
|
+
{ name: 'GPT 5.2 Codex', value: { id: 'gpt-5.2-codex', name: 'GPT 5.2 Codex' } }
|
|
1040
|
+
]
|
|
1041
|
+
}
|
|
1042
|
+
]);
|
|
1043
|
+
modelConfig = modelChoice;
|
|
194
1044
|
} else {
|
|
195
|
-
const
|
|
1045
|
+
const custom = await inquirer.prompt([
|
|
1046
|
+
{ type: 'input', name: 'id', message: '模型 ID:', validate: (i) => i.trim() !== '' || '不能为空' },
|
|
1047
|
+
{ type: 'input', name: 'name', message: '模型名称:', validate: (i) => i.trim() !== '' || '不能为空' }
|
|
1048
|
+
]);
|
|
1049
|
+
modelConfig = { id: custom.id.trim(), name: custom.name.trim() };
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
const includeModelsList = apiType !== 'anthropic-messages';
|
|
1053
|
+
let modelEntry = null;
|
|
1054
|
+
if (includeModelsList) {
|
|
1055
|
+
const defaults = answers.modelType === 'codex'
|
|
1056
|
+
? { contextWindow: 128000, maxTokens: 32768 }
|
|
1057
|
+
: { contextWindow: 200000, maxTokens: 8192 };
|
|
1058
|
+
const { contextWindow, maxTokens } = await inquirer.prompt([
|
|
196
1059
|
{
|
|
197
|
-
type: '
|
|
198
|
-
name: '
|
|
199
|
-
message: '
|
|
200
|
-
|
|
1060
|
+
type: 'number',
|
|
1061
|
+
name: 'contextWindow',
|
|
1062
|
+
message: '上下文窗口大小:',
|
|
1063
|
+
default: defaults.contextWindow
|
|
201
1064
|
},
|
|
202
1065
|
{
|
|
203
|
-
type: '
|
|
204
|
-
name: '
|
|
205
|
-
message: '
|
|
206
|
-
|
|
1066
|
+
type: 'number',
|
|
1067
|
+
name: 'maxTokens',
|
|
1068
|
+
message: '最大输出 tokens:',
|
|
1069
|
+
default: defaults.maxTokens
|
|
207
1070
|
}
|
|
208
1071
|
]);
|
|
209
|
-
|
|
1072
|
+
modelEntry = {
|
|
1073
|
+
id: modelConfig.id,
|
|
1074
|
+
name: modelConfig.name,
|
|
1075
|
+
reasoning: apiType === 'openai-responses',
|
|
1076
|
+
input: apiType === 'openai-responses' ? ['text', 'image'] : ['text'],
|
|
1077
|
+
cost: {
|
|
1078
|
+
input: 0,
|
|
1079
|
+
output: 0,
|
|
1080
|
+
cacheRead: 0,
|
|
1081
|
+
cacheWrite: 0
|
|
1082
|
+
},
|
|
1083
|
+
contextWindow,
|
|
1084
|
+
maxTokens
|
|
1085
|
+
};
|
|
210
1086
|
}
|
|
211
1087
|
|
|
212
|
-
const { contextWindow, maxTokens } = await inquirer.prompt([
|
|
213
|
-
{
|
|
214
|
-
type: 'number',
|
|
215
|
-
name: 'contextWindow',
|
|
216
|
-
message: '上下文窗口大小:',
|
|
217
|
-
default: 200000
|
|
218
|
-
},
|
|
219
|
-
{
|
|
220
|
-
type: 'number',
|
|
221
|
-
name: 'maxTokens',
|
|
222
|
-
message: '最大输出 tokens:',
|
|
223
|
-
default: 8192
|
|
224
|
-
}
|
|
225
|
-
]);
|
|
226
|
-
|
|
227
1088
|
await configManager.addRelay({
|
|
228
1089
|
name: answers.name,
|
|
229
1090
|
baseUrl: answers.baseUrl,
|
|
1091
|
+
auth: 'api-key',
|
|
1092
|
+
api: apiType,
|
|
1093
|
+
headers: {},
|
|
1094
|
+
authHeader: false,
|
|
1095
|
+
modelsMode: 'merge',
|
|
230
1096
|
model: {
|
|
231
1097
|
id: modelConfig.id,
|
|
232
1098
|
name: modelConfig.name,
|
|
233
|
-
contextWindow,
|
|
234
|
-
maxTokens
|
|
235
|
-
|
|
1099
|
+
contextWindow: includeModelsList ? modelEntry.contextWindow : undefined,
|
|
1100
|
+
maxTokens: includeModelsList ? modelEntry.maxTokens : undefined,
|
|
1101
|
+
reasoning: includeModelsList ? modelEntry.reasoning : undefined,
|
|
1102
|
+
input: includeModelsList ? modelEntry.input : undefined,
|
|
1103
|
+
cost: includeModelsList ? modelEntry.cost : undefined
|
|
1104
|
+
},
|
|
1105
|
+
models: includeModelsList ? [modelEntry] : []
|
|
236
1106
|
});
|
|
237
1107
|
|
|
238
1108
|
displaySuccess(`✅ 中转站 "${answers.name}" 添加成功!`);
|
|
@@ -256,8 +1126,29 @@ async function editRelay(configManager) {
|
|
|
256
1126
|
]);
|
|
257
1127
|
|
|
258
1128
|
const relay = relays.find(r => r.name === relayName);
|
|
1129
|
+
const config = await configManager.readOpenclawConfig();
|
|
1130
|
+
const provider = config.models?.providers?.[relayName] || {};
|
|
1131
|
+
const apiTypeDefault = provider.api || (normalizeRelayKey(relayName) === 'yunyi-codex'
|
|
1132
|
+
? 'openai-responses'
|
|
1133
|
+
: 'anthropic-messages');
|
|
1134
|
+
const hasModelList = Array.isArray(provider.models) && provider.models.length > 0;
|
|
1135
|
+
if (!['yunyi-claude', 'yunyi-codex'].includes(normalizeRelayKey(relayName))) {
|
|
1136
|
+
displayWarning('该中转站不在本工具保障范围,仅做配置写入。');
|
|
1137
|
+
}
|
|
259
1138
|
|
|
260
|
-
const
|
|
1139
|
+
const apiChoices = [
|
|
1140
|
+
{ name: 'Anthropic Messages (Claude)', value: 'anthropic-messages' },
|
|
1141
|
+
{ name: 'OpenAI Responses (Codex / GPT-5+)', value: 'openai-responses' },
|
|
1142
|
+
{ name: 'OpenAI Completions', value: 'openai-completions' },
|
|
1143
|
+
{ name: 'Google Generative AI (Gemini)', value: 'google-generative-ai' },
|
|
1144
|
+
{ name: '自定义', value: 'custom' }
|
|
1145
|
+
];
|
|
1146
|
+
|
|
1147
|
+
if (apiTypeDefault && !apiChoices.some(choice => choice.value === apiTypeDefault)) {
|
|
1148
|
+
apiChoices.unshift({ name: `当前 (${apiTypeDefault})`, value: apiTypeDefault });
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
const questions = [
|
|
261
1152
|
{
|
|
262
1153
|
type: 'input',
|
|
263
1154
|
name: 'baseUrl',
|
|
@@ -265,18 +1156,76 @@ async function editRelay(configManager) {
|
|
|
265
1156
|
default: relay.baseUrl
|
|
266
1157
|
},
|
|
267
1158
|
{
|
|
268
|
-
type: '
|
|
269
|
-
name: '
|
|
270
|
-
message: '
|
|
271
|
-
default:
|
|
1159
|
+
type: 'list',
|
|
1160
|
+
name: 'api',
|
|
1161
|
+
message: 'API 接口类型:',
|
|
1162
|
+
default: apiTypeDefault,
|
|
1163
|
+
choices: apiChoices
|
|
272
1164
|
},
|
|
273
1165
|
{
|
|
274
|
-
type: '
|
|
275
|
-
name: '
|
|
276
|
-
message: '
|
|
277
|
-
default:
|
|
1166
|
+
type: 'confirm',
|
|
1167
|
+
name: 'authHeader',
|
|
1168
|
+
message: '是否启用 authHeader?',
|
|
1169
|
+
default: !!provider.authHeader
|
|
1170
|
+
},
|
|
1171
|
+
{
|
|
1172
|
+
type: 'input',
|
|
1173
|
+
name: 'headers',
|
|
1174
|
+
message: 'Headers (JSON/JSON5,留空保持不变):',
|
|
1175
|
+
default: provider.headers ? JSON.stringify(provider.headers) : ''
|
|
1176
|
+
},
|
|
1177
|
+
{
|
|
1178
|
+
type: 'password',
|
|
1179
|
+
name: 'apiKey',
|
|
1180
|
+
message: 'API Key (留空保持不变):',
|
|
1181
|
+
mask: '*'
|
|
278
1182
|
}
|
|
279
|
-
]
|
|
1183
|
+
];
|
|
1184
|
+
|
|
1185
|
+
if (hasModelList) {
|
|
1186
|
+
questions.push(
|
|
1187
|
+
{
|
|
1188
|
+
type: 'number',
|
|
1189
|
+
name: 'contextWindow',
|
|
1190
|
+
message: '上下文窗口:',
|
|
1191
|
+
default: relay.contextWindow
|
|
1192
|
+
},
|
|
1193
|
+
{
|
|
1194
|
+
type: 'number',
|
|
1195
|
+
name: 'maxTokens',
|
|
1196
|
+
message: '最大输出 tokens:',
|
|
1197
|
+
default: relay.maxTokens
|
|
1198
|
+
}
|
|
1199
|
+
);
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
const answers = await inquirer.prompt(questions);
|
|
1203
|
+
|
|
1204
|
+
if (answers.api === 'custom') {
|
|
1205
|
+
const custom = await inquirer.prompt([
|
|
1206
|
+
{ type: 'input', name: 'customApi', message: '输入 API 类型:', validate: (i) => i.trim() !== '' || '不能为空' }
|
|
1207
|
+
]);
|
|
1208
|
+
answers.api = custom.customApi;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
if (answers.headers && answers.headers.trim() !== '') {
|
|
1212
|
+
try {
|
|
1213
|
+
answers.headers = JSON5.parse(answers.headers);
|
|
1214
|
+
} catch (error) {
|
|
1215
|
+
displayError(`Headers 解析失败: ${error.message}`);
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
} else {
|
|
1219
|
+
delete answers.headers;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
if (answers.baseUrl) {
|
|
1223
|
+
answers.baseUrl = buildRelayBaseUrl(answers.baseUrl, relayName);
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
if (!answers.apiKey || answers.apiKey.trim() === '') {
|
|
1227
|
+
delete answers.apiKey;
|
|
1228
|
+
}
|
|
280
1229
|
|
|
281
1230
|
await configManager.updateRelay(relayName, answers);
|
|
282
1231
|
displaySuccess(`✅ 中转站 "${relayName}" 更新成功!`);
|
|
@@ -336,7 +1285,21 @@ async function switchRelay(configManager) {
|
|
|
336
1285
|
}
|
|
337
1286
|
]);
|
|
338
1287
|
|
|
339
|
-
|
|
1288
|
+
const selected = relays.find(r => r.name === relayName);
|
|
1289
|
+
let modelId = selected?.modelId;
|
|
1290
|
+
if (!modelId) {
|
|
1291
|
+
const result = await inquirer.prompt([
|
|
1292
|
+
{
|
|
1293
|
+
type: 'input',
|
|
1294
|
+
name: 'modelId',
|
|
1295
|
+
message: '该中转站未配置模型 ID,请输入模型 ID:',
|
|
1296
|
+
validate: (input) => input.trim() !== '' || '模型 ID 不能为空'
|
|
1297
|
+
}
|
|
1298
|
+
]);
|
|
1299
|
+
modelId = result.modelId;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
await configManager.setPrimaryModel(relayName, modelId);
|
|
340
1303
|
displaySuccess(`✅ 已切换到 "${relayName}"`);
|
|
341
1304
|
}
|
|
342
1305
|
|
|
@@ -371,13 +1334,18 @@ async function manageModel(configManager) {
|
|
|
371
1334
|
async function manageFallback(configManager) {
|
|
372
1335
|
const relays = await configManager.listRelays();
|
|
373
1336
|
const currentFallbacks = await configManager.getFallbackModels();
|
|
1337
|
+
const availableRelays = relays.filter(r => r.modelId);
|
|
1338
|
+
|
|
1339
|
+
if (availableRelays.length !== relays.length) {
|
|
1340
|
+
displayWarning('部分中转站未配置模型 ID,已跳过');
|
|
1341
|
+
}
|
|
374
1342
|
|
|
375
1343
|
const { selected } = await inquirer.prompt([
|
|
376
1344
|
{
|
|
377
1345
|
type: 'checkbox',
|
|
378
1346
|
name: 'selected',
|
|
379
1347
|
message: '选择备用模型(按优先级排序):',
|
|
380
|
-
choices:
|
|
1348
|
+
choices: availableRelays.map(r => ({
|
|
381
1349
|
name: `${r.name} - ${r.modelName}`,
|
|
382
1350
|
value: `${r.name}/${r.modelId}`,
|
|
383
1351
|
checked: currentFallbacks.includes(`${r.name}/${r.modelId}`)
|
|
@@ -389,17 +1357,19 @@ async function manageFallback(configManager) {
|
|
|
389
1357
|
displaySuccess('✅ 备用模型配置已更新');
|
|
390
1358
|
}
|
|
391
1359
|
|
|
392
|
-
// 管理 API Keys
|
|
1360
|
+
// 管理 API Keys 和 Tokens
|
|
393
1361
|
async function manageApiKey(configManager) {
|
|
394
1362
|
const { keyAction } = await inquirer.prompt([
|
|
395
1363
|
{
|
|
396
1364
|
type: 'list',
|
|
397
1365
|
name: 'keyAction',
|
|
398
|
-
message: 'API Key 管理:',
|
|
1366
|
+
message: 'API Key / Token 管理:',
|
|
399
1367
|
choices: [
|
|
400
1368
|
{ name: '➕ 添加/更新 API Key', value: 'add' },
|
|
401
|
-
{ name: '
|
|
1369
|
+
{ name: '🔐 添加/更新 Token', value: 'addToken' },
|
|
1370
|
+
{ name: '👁️ 查看已配置的 Keys/Tokens', value: 'view' },
|
|
402
1371
|
{ name: '🗑️ 删除 API Key', value: 'delete' },
|
|
1372
|
+
{ name: '🗑️ 删除 Token', value: 'deleteToken' },
|
|
403
1373
|
{ name: '⬅️ 返回', value: 'back' }
|
|
404
1374
|
]
|
|
405
1375
|
}
|
|
@@ -411,12 +1381,18 @@ async function manageApiKey(configManager) {
|
|
|
411
1381
|
case 'add':
|
|
412
1382
|
await addApiKey(configManager);
|
|
413
1383
|
break;
|
|
1384
|
+
case 'addToken':
|
|
1385
|
+
await addToken(configManager);
|
|
1386
|
+
break;
|
|
414
1387
|
case 'view':
|
|
415
1388
|
await viewApiKeys(configManager);
|
|
416
1389
|
break;
|
|
417
1390
|
case 'delete':
|
|
418
1391
|
await deleteApiKey(configManager);
|
|
419
1392
|
break;
|
|
1393
|
+
case 'deleteToken':
|
|
1394
|
+
await deleteToken(configManager);
|
|
1395
|
+
break;
|
|
420
1396
|
}
|
|
421
1397
|
}
|
|
422
1398
|
|
|
@@ -424,6 +1400,11 @@ async function manageApiKey(configManager) {
|
|
|
424
1400
|
async function addApiKey(configManager) {
|
|
425
1401
|
const relays = await configManager.listRelays();
|
|
426
1402
|
|
|
1403
|
+
if (relays.length === 0) {
|
|
1404
|
+
displayInfo('请先添加中转站');
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
427
1408
|
const { relayName } = await inquirer.prompt([
|
|
428
1409
|
{
|
|
429
1410
|
type: 'list',
|
|
@@ -447,27 +1428,97 @@ async function addApiKey(configManager) {
|
|
|
447
1428
|
displaySuccess(`✅ API Key 已保存到 "${relayName}"`);
|
|
448
1429
|
}
|
|
449
1430
|
|
|
450
|
-
//
|
|
1431
|
+
// 添加 Token
|
|
1432
|
+
async function addToken(configManager) {
|
|
1433
|
+
const relays = await configManager.listRelays();
|
|
1434
|
+
|
|
1435
|
+
if (relays.length === 0) {
|
|
1436
|
+
displayInfo('请先添加中转站');
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
const { relayName } = await inquirer.prompt([
|
|
1441
|
+
{
|
|
1442
|
+
type: 'list',
|
|
1443
|
+
name: 'relayName',
|
|
1444
|
+
message: '选择中转站:',
|
|
1445
|
+
choices: relays.map(r => ({ name: r.name, value: r.name }))
|
|
1446
|
+
}
|
|
1447
|
+
]);
|
|
1448
|
+
|
|
1449
|
+
const { token } = await inquirer.prompt([
|
|
1450
|
+
{
|
|
1451
|
+
type: 'password',
|
|
1452
|
+
name: 'token',
|
|
1453
|
+
message: '输入 Token:',
|
|
1454
|
+
mask: '*',
|
|
1455
|
+
validate: (input) => input.trim() !== '' || 'Token 不能为空'
|
|
1456
|
+
}
|
|
1457
|
+
]);
|
|
1458
|
+
|
|
1459
|
+
await configManager.setToken(relayName, token);
|
|
1460
|
+
displaySuccess(`✅ Token 已保存到 "${relayName}"`);
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
// 查看 API Keys 和 Tokens
|
|
451
1464
|
async function viewApiKeys(configManager) {
|
|
452
1465
|
const keys = await configManager.listApiKeys();
|
|
453
1466
|
|
|
454
1467
|
if (keys.length === 0) {
|
|
455
|
-
displayInfo('没有配置任何 API Key');
|
|
1468
|
+
displayInfo('没有配置任何 API Key 或 Token');
|
|
456
1469
|
return;
|
|
457
1470
|
}
|
|
458
1471
|
|
|
459
|
-
console.log(chalk.cyan('\n已配置的 API Keys:\n'));
|
|
1472
|
+
console.log(chalk.cyan('\n已配置的 API Keys / Tokens:\n'));
|
|
460
1473
|
keys.forEach(k => {
|
|
461
|
-
const
|
|
462
|
-
|
|
1474
|
+
const maskedKey = k.key ? `${k.key.substring(0, 8)}...${k.key.substring(k.key.length - 4)}` : '未设置';
|
|
1475
|
+
const maskedToken = k.token ? `${k.token.substring(0, 8)}...${k.token.substring(k.token.length - 4)}` : '未设置';
|
|
1476
|
+
console.log(` ${chalk.green('●')} ${k.provider}`);
|
|
1477
|
+
console.log(` API Key: ${k.key ? chalk.green(maskedKey) : chalk.gray(maskedKey)}`);
|
|
1478
|
+
console.log(` Token: ${k.token ? chalk.green(maskedToken) : chalk.gray(maskedToken)}`);
|
|
463
1479
|
});
|
|
464
1480
|
}
|
|
465
1481
|
|
|
1482
|
+
// 删除 Token
|
|
1483
|
+
async function deleteToken(configManager) {
|
|
1484
|
+
const keys = await configManager.listApiKeys();
|
|
1485
|
+
const keysWithToken = keys.filter(k => k.token);
|
|
1486
|
+
|
|
1487
|
+
if (keysWithToken.length === 0) {
|
|
1488
|
+
displayInfo('没有可删除的 Token');
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
const { provider } = await inquirer.prompt([
|
|
1493
|
+
{
|
|
1494
|
+
type: 'list',
|
|
1495
|
+
name: 'provider',
|
|
1496
|
+
message: '选择要删除的 Token:',
|
|
1497
|
+
choices: keysWithToken.map(k => ({ name: k.provider, value: k.provider }))
|
|
1498
|
+
}
|
|
1499
|
+
]);
|
|
1500
|
+
|
|
1501
|
+
const { confirm } = await inquirer.prompt([
|
|
1502
|
+
{
|
|
1503
|
+
type: 'confirm',
|
|
1504
|
+
name: 'confirm',
|
|
1505
|
+
message: `确定要删除 "${provider}" 的 Token 吗?`,
|
|
1506
|
+
default: false
|
|
1507
|
+
}
|
|
1508
|
+
]);
|
|
1509
|
+
|
|
1510
|
+
if (confirm) {
|
|
1511
|
+
await configManager.deleteToken(provider);
|
|
1512
|
+
displaySuccess(`✅ Token 已删除`);
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
|
|
466
1516
|
// 删除 API Key
|
|
467
1517
|
async function deleteApiKey(configManager) {
|
|
468
1518
|
const keys = await configManager.listApiKeys();
|
|
1519
|
+
const keysWithApiKey = keys.filter(k => k.key);
|
|
469
1520
|
|
|
470
|
-
if (
|
|
1521
|
+
if (keysWithApiKey.length === 0) {
|
|
471
1522
|
displayInfo('没有可删除的 API Key');
|
|
472
1523
|
return;
|
|
473
1524
|
}
|
|
@@ -477,7 +1528,7 @@ async function deleteApiKey(configManager) {
|
|
|
477
1528
|
type: 'list',
|
|
478
1529
|
name: 'provider',
|
|
479
1530
|
message: '选择要删除的 API Key:',
|
|
480
|
-
choices:
|
|
1531
|
+
choices: keysWithApiKey.map(k => ({ name: k.provider, value: k.provider }))
|
|
481
1532
|
}
|
|
482
1533
|
]);
|
|
483
1534
|
|
|
@@ -553,13 +1604,26 @@ async function speedTestAndSwitch(configManager) {
|
|
|
553
1604
|
|
|
554
1605
|
if (action === 'fastest') {
|
|
555
1606
|
// 切换到最快的
|
|
556
|
-
|
|
1607
|
+
const fastestRelay = relays.find(rel => rel.name === fastest.name);
|
|
1608
|
+
let fastestModelId = fastestRelay?.modelId;
|
|
1609
|
+
if (!fastestModelId) {
|
|
1610
|
+
const result = await inquirer.prompt([
|
|
1611
|
+
{
|
|
1612
|
+
type: 'input',
|
|
1613
|
+
name: 'modelId',
|
|
1614
|
+
message: `未找到 "${fastest.name}" 的模型 ID,请输入:`,
|
|
1615
|
+
validate: (input) => input.trim() !== '' || '模型 ID 不能为空'
|
|
1616
|
+
}
|
|
1617
|
+
]);
|
|
1618
|
+
fastestModelId = result.modelId;
|
|
1619
|
+
}
|
|
1620
|
+
await configManager.setPrimaryModel(fastest.name, fastestModelId);
|
|
557
1621
|
|
|
558
1622
|
// 设置其他可用的为备用
|
|
559
|
-
const fallbacks = sorted.slice(1)
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
1623
|
+
const fallbacks = sorted.slice(1)
|
|
1624
|
+
.map(r => relays.find(rel => rel.name === r.name))
|
|
1625
|
+
.filter(relay => relay && relay.modelId)
|
|
1626
|
+
.map(relay => `${relay.name}/${relay.modelId}`);
|
|
563
1627
|
await configManager.setFallbackModels(fallbacks);
|
|
564
1628
|
|
|
565
1629
|
displaySuccess(`✅ 已切换到 "${fastest.name}" (${fastest.latency}ms)`);
|
|
@@ -580,15 +1644,27 @@ async function speedTestAndSwitch(configManager) {
|
|
|
580
1644
|
}
|
|
581
1645
|
]);
|
|
582
1646
|
|
|
583
|
-
|
|
1647
|
+
const selectedRelay = relays.find(rel => rel.name === selected);
|
|
1648
|
+
let selectedModelId = selectedRelay?.modelId;
|
|
1649
|
+
if (!selectedModelId) {
|
|
1650
|
+
const result = await inquirer.prompt([
|
|
1651
|
+
{
|
|
1652
|
+
type: 'input',
|
|
1653
|
+
name: 'modelId',
|
|
1654
|
+
message: `未找到 "${selected}" 的模型 ID,请输入:`,
|
|
1655
|
+
validate: (input) => input.trim() !== '' || '模型 ID 不能为空'
|
|
1656
|
+
}
|
|
1657
|
+
]);
|
|
1658
|
+
selectedModelId = result.modelId;
|
|
1659
|
+
}
|
|
1660
|
+
await configManager.setPrimaryModel(selected, selectedModelId);
|
|
584
1661
|
|
|
585
1662
|
// 设置其他为备用
|
|
586
1663
|
const fallbacks = sorted
|
|
587
1664
|
.filter(r => r.name !== selected)
|
|
588
|
-
.map(r =>
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
});
|
|
1665
|
+
.map(r => relays.find(rel => rel.name === r.name))
|
|
1666
|
+
.filter(relay => relay && relay.modelId)
|
|
1667
|
+
.map(relay => `${relay.name}/${relay.modelId}`);
|
|
592
1668
|
await configManager.setFallbackModels(fallbacks);
|
|
593
1669
|
|
|
594
1670
|
const selectedResult = results.find(r => r.name === selected);
|
|
@@ -602,29 +1678,91 @@ async function speedTestAndSwitch(configManager) {
|
|
|
602
1678
|
// 高级设置
|
|
603
1679
|
async function manageAdvanced(configManager) {
|
|
604
1680
|
const currentConfig = await configManager.getAdvancedSettings();
|
|
1681
|
+
const defaultWorkspace = getDefaultWorkspace();
|
|
1682
|
+
|
|
1683
|
+
console.log(chalk.cyan('\n当前高级设置:'));
|
|
1684
|
+
console.log(` 最大并发: ${currentConfig.maxConcurrent}`);
|
|
1685
|
+
console.log(` 子代理并发: ${currentConfig.subagentMaxConcurrent}`);
|
|
1686
|
+
console.log(` 工作区: ${currentConfig.workspace || chalk.gray('(使用默认)')}`);
|
|
1687
|
+
console.log(` 压缩模式: ${currentConfig.compactionMode || chalk.gray('(未设置)')}`);
|
|
1688
|
+
if (currentConfig.gatewayToken) {
|
|
1689
|
+
const token = currentConfig.gatewayToken;
|
|
1690
|
+
const masked = `${token.substring(0, 6)}...${token.substring(token.length - 4)}`;
|
|
1691
|
+
console.log(` 网关 Token: ${chalk.gray(masked)}`);
|
|
1692
|
+
} else {
|
|
1693
|
+
console.log(` 网关 Token: ${chalk.gray('(未设置)')}`);
|
|
1694
|
+
}
|
|
1695
|
+
console.log(` 默认工作区: ${chalk.gray(defaultWorkspace)}\n`);
|
|
605
1696
|
|
|
606
1697
|
const answers = await inquirer.prompt([
|
|
607
1698
|
{
|
|
608
1699
|
type: 'number',
|
|
609
1700
|
name: 'maxConcurrent',
|
|
610
|
-
message: '
|
|
611
|
-
default: currentConfig.maxConcurrent || 4
|
|
1701
|
+
message: '最大并发任务数 (1-100):',
|
|
1702
|
+
default: currentConfig.maxConcurrent || 4,
|
|
1703
|
+
validate: (input) => {
|
|
1704
|
+
const num = Number(input);
|
|
1705
|
+
if (isNaN(num) || num < 1 || num > 100) {
|
|
1706
|
+
return '请输入 1 到 100 之间的数字';
|
|
1707
|
+
}
|
|
1708
|
+
return true;
|
|
1709
|
+
}
|
|
612
1710
|
},
|
|
613
1711
|
{
|
|
614
1712
|
type: 'number',
|
|
615
1713
|
name: 'subagentMaxConcurrent',
|
|
616
|
-
message: '
|
|
617
|
-
default: currentConfig.subagentMaxConcurrent || 8
|
|
1714
|
+
message: '子代理最大并发数 (1-100):',
|
|
1715
|
+
default: currentConfig.subagentMaxConcurrent || 8,
|
|
1716
|
+
validate: (input) => {
|
|
1717
|
+
const num = Number(input);
|
|
1718
|
+
if (isNaN(num) || num < 1 || num > 100) {
|
|
1719
|
+
return '请输入 1 到 100 之间的数字';
|
|
1720
|
+
}
|
|
1721
|
+
return true;
|
|
1722
|
+
}
|
|
618
1723
|
},
|
|
619
1724
|
{
|
|
620
1725
|
type: 'input',
|
|
621
1726
|
name: 'workspace',
|
|
622
|
-
message:
|
|
623
|
-
default: currentConfig.workspace ||
|
|
1727
|
+
message: `工作区路径 (留空使用默认: ${defaultWorkspace}):`,
|
|
1728
|
+
default: currentConfig.workspace || ''
|
|
1729
|
+
},
|
|
1730
|
+
{
|
|
1731
|
+
type: 'list',
|
|
1732
|
+
name: 'compactionMode',
|
|
1733
|
+
message: '压缩模式:',
|
|
1734
|
+
choices: [
|
|
1735
|
+
{ name: '保持不变', value: '__keep__' },
|
|
1736
|
+
{ name: 'safeguard', value: 'safeguard' },
|
|
1737
|
+
{ name: 'auto', value: 'auto' },
|
|
1738
|
+
{ name: 'off', value: 'off' }
|
|
1739
|
+
],
|
|
1740
|
+
default: currentConfig.compactionMode ? currentConfig.compactionMode : '__keep__'
|
|
1741
|
+
},
|
|
1742
|
+
{
|
|
1743
|
+
type: 'password',
|
|
1744
|
+
name: 'gatewayToken',
|
|
1745
|
+
message: '网关 Token (留空保持不变):',
|
|
1746
|
+
mask: '*'
|
|
624
1747
|
}
|
|
625
1748
|
]);
|
|
626
1749
|
|
|
627
|
-
|
|
1750
|
+
// 如果用户没有输入工作区路径,使用默认值
|
|
1751
|
+
if (!answers.workspace || answers.workspace.trim() === '') {
|
|
1752
|
+
answers.workspace = defaultWorkspace;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
const advancedUpdate = {
|
|
1756
|
+
maxConcurrent: answers.maxConcurrent,
|
|
1757
|
+
subagentMaxConcurrent: answers.subagentMaxConcurrent,
|
|
1758
|
+
workspace: answers.workspace,
|
|
1759
|
+
gatewayToken: answers.gatewayToken
|
|
1760
|
+
};
|
|
1761
|
+
if (answers.compactionMode !== '__keep__') {
|
|
1762
|
+
advancedUpdate.compactionMode = answers.compactionMode;
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
await configManager.setAdvancedSettings(advancedUpdate);
|
|
628
1766
|
displaySuccess('✅ 高级设置已更新');
|
|
629
1767
|
}
|
|
630
1768
|
|
|
@@ -646,7 +1784,8 @@ async function viewConfig(configManager) {
|
|
|
646
1784
|
console.log(` ${chalk.green('●')} ${r.name}`);
|
|
647
1785
|
console.log(` URL: ${r.baseUrl}`);
|
|
648
1786
|
console.log(` 模型: ${r.modelName} (${r.modelId})`);
|
|
649
|
-
|
|
1787
|
+
const contextInfo = r.contextWindow ? `${r.contextWindow} tokens` : '-';
|
|
1788
|
+
console.log(` 上下文: ${contextInfo}`);
|
|
650
1789
|
});
|
|
651
1790
|
|
|
652
1791
|
console.log(chalk.yellow('\n高级设置:'));
|