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