openclawapi 1.1.1 → 1.2.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.
Files changed (2) hide show
  1. package/cli.js +310 -1663
  2. package/package.json +1 -1
package/cli.js CHANGED
@@ -6,1796 +6,443 @@ const fs = require('fs');
6
6
  const JSON5 = require('json5');
7
7
  const path = require('path');
8
8
  const os = require('os');
9
- const { ConfigManager, getDefaultWorkspace } = require('./lib/config-manager');
10
- const { displayMenu, displaySuccess, displayError, displayInfo, displayWarning } = require('./lib/ui');
11
- const { testMultipleRelays, sortBySpeed, formatLatency } = require('./lib/speed-test');
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
- }
9
+ const https = require('https');
10
+ const http = require('http');
11
+
12
+ // ============ 节点预设 (同步自 yunyi-activator) ============
13
+ const ENDPOINTS = [
14
+ { name: '国内节点1', url: 'https://yunyi.rdzhvip.com' },
15
+ { name: '国内节点2', 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
+ ];
86
22
 
87
- if (/\/claude(\/|$)/.test(trimmed) || /\/codex(\/|$)/.test(trimmed)) {
88
- return trimmed;
89
- }
23
+ // 模型预设
24
+ const MODELS = [
25
+ { id: 'claude-opus-4-5', name: 'Claude Opus 4.5' },
26
+ { id: 'claude-sonnet-4-5', name: 'Claude Sonnet 4.5' },
27
+ { id: 'claude-haiku-4-5', name: 'Claude Haiku 4.5' }
28
+ ];
90
29
 
91
- return `${trimmed}${suffix}`;
92
- }
30
+ // 备份文件名
31
+ const BACKUP_FILENAME = 'openclaw-default.json.bak';
32
+
33
+ // ============ 测速功能 ============
34
+ async function testSpeed(url) {
35
+ return new Promise((resolve) => {
36
+ const startTime = Date.now();
37
+ const urlObj = new URL(url);
38
+ const protocol = urlObj.protocol === 'https:' ? https : http;
39
+ const timeout = 5000;
40
+
41
+ const req = protocol.get(url, { timeout, rejectUnauthorized: false }, (res) => {
42
+ const endTime = Date.now();
43
+ res.on('data', () => {});
44
+ res.on('end', () => {
45
+ resolve({ success: true, latency: endTime - startTime });
46
+ });
47
+ });
93
48
 
94
- function getRelayEndpoints(relayName) {
95
- return RELAY_ENDPOINT_PRESETS[normalizeRelayKey(relayName)] || [];
96
- }
49
+ req.on('timeout', () => {
50
+ req.destroy();
51
+ resolve({ success: false, latency: null, error: '超时' });
52
+ });
97
53
 
98
- function getRelayModelPresets(relayName) {
99
- return MODEL_PRESETS[normalizeRelayKey(relayName)] || [];
54
+ req.on('error', () => {
55
+ resolve({ success: false, latency: null, error: '连接失败' });
56
+ });
57
+ });
100
58
  }
101
59
 
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
- };
60
+ async function testAllEndpoints() {
61
+ const results = [];
62
+ for (const endpoint of ENDPOINTS) {
63
+ process.stdout.write(chalk.gray(` 测试 ${endpoint.name}...`));
64
+ const result = await testSpeed(endpoint.url);
65
+ results.push({ ...endpoint, ...result });
66
+ if (result.success) {
67
+ console.log(chalk.green(` ${result.latency}ms`));
68
+ } else {
69
+ console.log(chalk.red(` ${result.error}`));
70
+ }
71
+ }
72
+ return results;
121
73
  }
122
74
 
123
- // 获取配置文件路径(跨平台)
75
+ // ============ 配置路径 ============
124
76
  function getConfigPath() {
125
77
  const homeDir = os.homedir();
126
78
  const openclawStateDir = process.env.OPENCLAW_STATE_DIR || path.join(homeDir, '.openclaw');
127
79
  const clawdbotStateDir = process.env.CLAWDBOT_STATE_DIR || path.join(homeDir, '.clawdbot');
128
80
 
129
- const envConfig =
130
- process.env.OPENCLAW_CONFIG_PATH ||
131
- process.env.CLAWDBOT_CONFIG_PATH ||
132
- process.env.OPENCLAW_CONFIG;
81
+ const envConfig = process.env.OPENCLAW_CONFIG_PATH || process.env.CLAWDBOT_CONFIG_PATH;
133
82
 
134
83
  const openclawConfig = envConfig || ([
135
84
  path.join(openclawStateDir, 'openclaw.json'),
136
85
  path.join(clawdbotStateDir, 'openclaw.json'),
137
- path.join(clawdbotStateDir, 'clawdbot.json'),
138
- path.join(clawdbotStateDir, 'moltbot.json')
86
+ path.join(clawdbotStateDir, 'clawdbot.json')
139
87
  ].find(p => fs.existsSync(p)) || path.join(openclawStateDir, 'openclaw.json'));
140
88
 
141
89
  const configDir = path.dirname(openclawConfig);
142
- const authProfiles = resolveAuthProfilesPath({
143
- configDir,
144
- openclawStateDir,
145
- clawdbotStateDir
146
- });
147
90
 
148
- return {
149
- openclawConfig,
150
- authProfiles
151
- };
152
- }
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 = [];
91
+ // 查找 auth-profiles 路径
92
+ const authCandidates = [
93
+ path.join(openclawStateDir, 'agents', 'main', 'agent', 'auth-profiles.json'),
94
+ path.join(clawdbotStateDir, 'agents', 'main', 'agent', 'auth-profiles.json'),
95
+ path.join(clawdbotStateDir, 'agent', 'auth-profiles.json')
96
+ ];
97
+ const authProfiles = authCandidates.find(p => fs.existsSync(p)) || authCandidates[0];
165
98
 
166
- if (envAgentDir) {
167
- candidates.push(path.join(envAgentDir, 'auth-profiles.json'));
168
- }
99
+ return { openclawConfig, authProfiles, configDir };
100
+ }
169
101
 
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'));
102
+ // ============ 配置读写 ============
103
+ function readConfig(configPath) {
104
+ if (fs.existsSync(configPath)) {
105
+ const raw = fs.readFileSync(configPath, 'utf8');
106
+ return JSON5.parse(raw);
173
107
  }
108
+ return null;
109
+ }
174
110
 
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);
111
+ function writeConfig(configPath, config) {
112
+ const dir = path.dirname(configPath);
113
+ if (!fs.existsSync(dir)) {
114
+ fs.mkdirSync(dir, { recursive: true });
190
115
  }
191
-
192
- return candidates.find(p => fs.existsSync(p)) || candidates[0];
116
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
193
117
  }
194
118
 
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
- }
119
+ // ============ 备份/恢复 ============
120
+ function backupOriginalConfig(configPath, configDir) {
121
+ const backupPath = path.join(configDir, BACKUP_FILENAME);
122
+ if (!fs.existsSync(backupPath) && fs.existsSync(configPath)) {
123
+ fs.copyFileSync(configPath, backupPath);
124
+ return true;
204
125
  }
126
+ return false;
127
+ }
205
128
 
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
- }
129
+ function restoreDefaultConfig(configPath, configDir) {
130
+ const backupPath = path.join(configDir, BACKUP_FILENAME);
131
+ if (fs.existsSync(backupPath)) {
132
+ fs.copyFileSync(backupPath, configPath);
133
+ return true;
214
134
  }
135
+ return false;
136
+ }
215
137
 
216
- return '';
138
+ // ============ URL 构建 ============
139
+ function buildFullUrl(baseUrl) {
140
+ const trimmed = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
141
+ if (trimmed.includes('/claude/v1/messages')) return trimmed;
142
+ if (trimmed.includes('/claude')) return trimmed + '/v1/messages';
143
+ return trimmed + '/claude/v1/messages';
217
144
  }
218
145
 
146
+ // ============ 主程序 ============
219
147
  async function main() {
220
148
  console.clear();
221
- console.log(chalk.cyan.bold('\n🔧 OpenClaw 配置管理工具\n'));
149
+ console.log(chalk.cyan.bold('\n🔧 OpenClaw 配置工具 (简化版)\n'));
222
150
 
223
- // 显示平台信息
224
- const platformInfo = {
225
- win32: 'Windows',
226
- darwin: 'macOS',
227
- linux: 'Linux'
228
- };
229
- console.log(chalk.gray(`平台: ${platformInfo[process.platform] || process.platform}`));
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`));
234
- const configManager = new ConfigManager(configPaths);
235
-
236
- // 检查配置文件是否存在,如果不存在则自动初始化
237
- const exists = await configManager.checkConfigExists();
238
- if (!exists.openclaw) {
239
- displayInfo('配置文件不存在,正在初始化...');
240
- try {
241
- await configManager.initializeConfig();
242
- displaySuccess('配置文件初始化成功!');
243
- } catch (error) {
244
- displayError(`初始化失败: ${error.message}`);
245
- displayInfo('请检查目录权限或手动创建配置文件');
246
- process.exit(1);
247
- }
151
+ const paths = getConfigPath();
152
+ console.log(chalk.gray(`配置文件: ${paths.openclawConfig}\n`));
153
+
154
+ // 首次运行备份
155
+ if (backupOriginalConfig(paths.openclawConfig, paths.configDir)) {
156
+ console.log(chalk.green('✓ 已备份原始配置\n'));
248
157
  }
249
158
 
250
159
  while (true) {
251
- const { action } = await inquirer.prompt([
252
- {
253
- type: 'list',
254
- name: 'action',
255
- message: '请选择操作:',
256
- choices: [
257
- { name: '🚀 快速配置向导 (URL/Key/Token/Workspace)', value: 'quicksetup' },
258
- new inquirer.Separator(),
259
- { name: '📡 管理中转站配置', value: 'relay' },
260
- { name: '🧭 快速切换中转端点', value: 'endpoint' },
261
- { name: '🤖 管理模型配置', value: 'model' },
262
- { name: '🧩 快速切换模型', value: 'modelSwitch' },
263
- { name: '🔑 管理 API Keys / Tokens', value: 'apikey' },
264
- { name: '⚡ 测速并切换中转站', value: 'speedtest' },
265
- { name: '⚙️ 高级设置', value: 'advanced' },
266
- { name: '📄 查看当前配置', value: 'view' },
267
- new inquirer.Separator(),
268
- { name: '❌ 退出', value: 'exit' }
269
- ]
270
- }
271
- ]);
160
+ const { action } = await inquirer.prompt([{
161
+ type: 'list',
162
+ name: 'action',
163
+ message: '请选择操作:',
164
+ choices: [
165
+ { name: '🌐 选择节点', value: 'select_node' },
166
+ { name: '🤖 选择默认模型', value: 'select_model' },
167
+ { name: '🔑 设置 API Key', value: 'set_apikey' },
168
+ { name: '📋 查看当前配置', value: 'view_config' },
169
+ { name: '🔄 恢复默认配置', value: 'restore' },
170
+ new inquirer.Separator(),
171
+ { name: ' 退出', value: 'exit' }
172
+ ]
173
+ }]);
272
174
 
273
- if (action === 'exit') {
274
- console.log(chalk.green('\n👋 再见!\n'));
275
- process.exit(0);
276
- }
175
+ console.log('');
277
176
 
278
177
  try {
279
178
  switch (action) {
280
- case 'quicksetup':
281
- await quickSetup(configManager);
282
- break;
283
- case 'relay':
284
- await manageRelay(configManager);
179
+ case 'select_node':
180
+ await selectNode(paths);
285
181
  break;
286
- case 'endpoint':
287
- await quickSwitchEndpoint(configManager);
182
+ case 'select_model':
183
+ await selectModel(paths);
288
184
  break;
289
- case 'model':
290
- await manageModel(configManager);
185
+ case 'set_apikey':
186
+ await setApiKey(paths);
291
187
  break;
292
- case 'modelSwitch':
293
- await quickSwitchModel(configManager);
188
+ case 'view_config':
189
+ await viewConfig(paths);
294
190
  break;
295
- case 'apikey':
296
- await manageApiKey(configManager);
297
- break;
298
- case 'speedtest':
299
- await speedTestAndSwitch(configManager);
300
- break;
301
- case 'advanced':
302
- await manageAdvanced(configManager);
303
- break;
304
- case 'view':
305
- await viewConfig(configManager);
191
+ case 'restore':
192
+ await restore(paths);
306
193
  break;
194
+ case 'exit':
195
+ console.log(chalk.cyan('👋 再见!\n'));
196
+ process.exit(0);
307
197
  }
308
198
  } catch (error) {
309
- displayError(`操作失败: ${error.message}`);
199
+ console.log(chalk.red(`错误: ${error.message}\n`));
310
200
  }
311
201
 
312
- console.log('\n');
202
+ console.log('');
313
203
  }
314
204
  }
315
205
 
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'));
206
+ // ============ 选择节点 ============
207
+ async function selectNode(paths) {
208
+ console.log(chalk.cyan('📡 节点测速中...\n'));
320
209
 
321
- const defaultWorkspace = getDefaultWorkspace();
210
+ const results = await testAllEndpoints();
322
211
 
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
- ];
212
+ // 按延迟排序
213
+ const sorted = results
214
+ .filter(r => r.success)
215
+ .sort((a, b) => a.latency - b.latency);
330
216
 
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
- ]);
217
+ if (sorted.length === 0) {
218
+ console.log(chalk.red('\n所有节点都无法访问!'));
219
+ return;
220
+ }
401
221
 
402
- step1.baseUrl = buildRelayBaseUrl(step1.baseUrl, step1.name);
222
+ console.log(chalk.green(`\n🏆 最快节点: ${sorted[0].name} (${sorted[0].latency}ms)\n`));
403
223
 
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
- }
224
+ // 选择节点
225
+ const { selectedIndex } = await inquirer.prompt([{
226
+ type: 'list',
227
+ name: 'selectedIndex',
228
+ message: '选择节点:',
229
+ choices: [
230
+ { name: `🚀 使用最快节点 (${sorted[0].name})`, value: -1 },
231
+ new inquirer.Separator('--- 或手动选择 ---'),
232
+ ...sorted.map((e, i) => ({
233
+ name: `${e.name} - ${e.latency}ms`,
234
+ value: i
235
+ }))
236
+ ]
237
+ }]);
448
238
 
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
- }
239
+ const primaryIndex = selectedIndex === -1 ? 0 : selectedIndex;
240
+ const selectedEndpoint = sorted[primaryIndex];
466
241
 
467
- if (modelType === 'other') {
468
- displayWarning('非 GPT/Claude 模型仅做配置写入,不保证可用,请优先用 OpenClaw 配置页面。');
469
- }
242
+ // 读取或创建配置
243
+ let config = readConfig(paths.openclawConfig) || {};
470
244
 
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
- }
245
+ // 获取当前模型(保持不变)
246
+ const currentModelId = config.agents?.defaults?.model?.primary?.split('/')[1] || MODELS[0].id;
247
+ const modelConfig = MODELS.find(m => m.id === currentModelId) || MODELS[0];
502
248
 
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() };
249
+ // 清除旧的 yunyi 配置
250
+ if (config.models?.providers) {
251
+ Object.keys(config.models.providers).forEach(key => {
252
+ if (key.startsWith('yunyi-')) delete config.models.providers[key];
253
+ });
539
254
  }
540
255
 
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 = {
256
+ // 初始化结构
257
+ if (!config.models) config.models = {};
258
+ if (!config.models.providers) config.models.providers = {};
259
+ if (!config.agents) config.agents = {};
260
+ if (!config.agents.defaults) config.agents.defaults = {};
261
+ if (!config.agents.defaults.model) config.agents.defaults.model = {};
262
+ if (!config.agents.defaults.models) config.agents.defaults.models = {};
263
+
264
+ // 添加选中的节点
265
+ const providerName = 'yunyi-001';
266
+ config.models.providers[providerName] = {
267
+ baseUrl: buildFullUrl(selectedEndpoint.url),
268
+ api: 'anthropic-messages',
269
+ models: [{
563
270
  id: modelConfig.id,
564
271
  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
- }
272
+ contextWindow: 200000,
273
+ maxTokens: 8192
274
+ }]
275
+ };
699
276
 
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
- }
277
+ // 设置主模型(无备用)
278
+ config.agents.defaults.model.primary = `${providerName}/${modelConfig.id}`;
279
+ config.agents.defaults.model.fallbacks = [];
280
+ config.agents.defaults.models = {
281
+ [`${providerName}/${modelConfig.id}`]: { alias: providerName }
282
+ };
705
283
 
706
- // 保存中转站 Token
707
- if (step3.providerToken && step3.providerToken.trim() !== '') {
708
- await configManager.setToken(step1.name, step3.providerToken);
709
- console.log(chalk.green(' ✓ 中转站 Token 已保存'));
710
- }
284
+ writeConfig(paths.openclawConfig, config);
711
285
 
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
- }
286
+ console.log(chalk.green(`\n✅ 配置完成!`));
287
+ console.log(chalk.cyan(` 节点: ${selectedEndpoint.name} (${selectedEndpoint.url})`));
288
+ console.log(chalk.gray(` 模型: ${modelConfig.name}`));
735
289
  }
736
290
 
737
- // 快速切换中转端点
738
- async function quickSwitchEndpoint(configManager) {
739
- const relays = await configManager.listRelays();
740
- if (relays.length === 0) {
741
- displayInfo('请先添加中转站');
742
- return;
743
- }
291
+ // ============ 选择模型 ============
292
+ async function selectModel(paths) {
293
+ console.log(chalk.cyan('🤖 选择默认模型\n'));
744
294
 
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
- }
295
+ const { selectedModel } = await inquirer.prompt([{
296
+ type: 'list',
297
+ name: 'selectedModel',
298
+ message: '选择模型:',
299
+ choices: MODELS.map(m => ({ name: m.name, value: m.id }))
300
+ }]);
816
301
 
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
- }
302
+ const modelConfig = MODELS.find(m => m.id === selectedModel);
303
+ let config = readConfig(paths.openclawConfig);
825
304
 
826
- // 快速切换模型
827
- async function quickSwitchModel(configManager) {
828
- const relays = await configManager.listRelays();
829
- if (relays.length === 0) {
830
- displayInfo('请先添加中转站');
305
+ if (!config?.models?.providers) {
306
+ console.log(chalk.yellow('⚠️ 请先选择节点'));
831
307
  return;
832
308
  }
833
309
 
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
-
914
- // 管理中转站配置
915
- async function manageRelay(configManager) {
916
- const { relayAction } = await inquirer.prompt([
917
- {
918
- type: 'list',
919
- name: 'relayAction',
920
- message: '中转站管理:',
921
- choices: [
922
- { name: '➕ 添加新中转站', value: 'add' },
923
- { name: '📝 编辑现有中转站', value: 'edit' },
924
- { name: '🗑️ 删除中转站', value: 'delete' },
925
- { name: '🔄 切换主中转站', value: 'switch' },
926
- { name: '⬅️ 返回', value: 'back' }
927
- ]
928
- }
929
- ]);
930
-
931
- if (relayAction === 'back') return;
932
-
933
- switch (relayAction) {
934
- case 'add':
935
- await addRelay(configManager);
936
- break;
937
- case 'edit':
938
- await editRelay(configManager);
939
- break;
940
- case 'delete':
941
- await deleteRelay(configManager);
942
- break;
943
- case 'switch':
944
- await switchRelay(configManager);
945
- break;
946
- }
947
- }
948
-
949
- // 添加新中转站
950
- async function addRelay(configManager) {
951
- console.log(chalk.yellow('\n添加新中转站\n'));
952
-
953
- const answers = await inquirer.prompt([
954
- {
955
- type: 'input',
956
- name: 'name',
957
- message: '中转站名称 (例如: claude-relay-1):',
958
- validate: (input) => input.trim() !== '' || '名称不能为空'
959
- },
960
- {
961
- type: 'input',
962
- name: 'baseUrl',
963
- message: 'Base URL (例如: https://yunyi.cfd/claude/v1):',
964
- validate: (input) => {
965
- try {
966
- new URL(input);
967
- return true;
968
- } catch {
969
- return '请输入有效的 URL';
970
- }
971
- }
972
- },
973
- {
974
- type: 'list',
975
- name: 'modelType',
976
- message: '模型类型:',
977
- choices: [
978
- { name: 'Claude (Anthropic)', value: 'claude' },
979
- { name: 'Codex / GPT (OpenAI Responses)', value: 'codex' },
980
- { name: '其他/自定义', value: 'other' }
981
- ]
982
- }
983
- ]);
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
-
1015
- let modelConfig;
1016
- if (answers.modelType === 'claude') {
1017
- const { modelChoice } = await inquirer.prompt([
1018
- {
1019
- type: 'list',
1020
- name: 'modelChoice',
1021
- message: '选择 Claude 模型:',
1022
- choices: [
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' } }
1026
- ]
1027
- }
1028
- ]);
1029
-
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;
1044
- } else {
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([
1059
- {
1060
- type: 'number',
1061
- name: 'contextWindow',
1062
- message: '上下文窗口大小:',
1063
- default: defaults.contextWindow
1064
- },
1065
- {
1066
- type: 'number',
1067
- name: 'maxTokens',
1068
- message: '最大输出 tokens:',
1069
- default: defaults.maxTokens
1070
- }
1071
- ]);
1072
- modelEntry = {
310
+ // 更新 yunyi 节点的模型
311
+ const providerName = Object.keys(config.models.providers).find(k => k.startsWith('yunyi-'));
312
+ if (providerName) {
313
+ config.models.providers[providerName].models = [{
1073
314
  id: modelConfig.id,
1074
315
  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
316
+ contextWindow: 200000,
317
+ maxTokens: 8192
318
+ }];
319
+
320
+ // 更新主模型(无备用)
321
+ config.agents.defaults.model.primary = `${providerName}/${modelConfig.id}`;
322
+ config.agents.defaults.model.fallbacks = [];
323
+ config.agents.defaults.models = {
324
+ [`${providerName}/${modelConfig.id}`]: { alias: providerName }
1085
325
  };
1086
326
  }
1087
327
 
1088
- await configManager.addRelay({
1089
- name: answers.name,
1090
- baseUrl: answers.baseUrl,
1091
- auth: 'api-key',
1092
- api: apiType,
1093
- headers: {},
1094
- authHeader: false,
1095
- modelsMode: 'merge',
1096
- model: {
1097
- id: modelConfig.id,
1098
- name: modelConfig.name,
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] : []
1106
- });
1107
-
1108
- displaySuccess(`✅ 中转站 "${answers.name}" 添加成功!`);
328
+ writeConfig(paths.openclawConfig, config);
329
+ console.log(chalk.green(`\n✅ 模型已切换为: ${modelConfig.name}`));
1109
330
  }
1110
331
 
1111
- // 编辑中转站
1112
- async function editRelay(configManager) {
1113
- const relays = await configManager.listRelays();
1114
- if (relays.length === 0) {
1115
- displayInfo('没有可编辑的中转站');
1116
- return;
1117
- }
1118
-
1119
- const { relayName } = await inquirer.prompt([
1120
- {
1121
- type: 'list',
1122
- name: 'relayName',
1123
- message: '选择要编辑的中转站:',
1124
- choices: relays.map(r => ({ name: `${r.name} (${r.baseUrl})`, value: r.name }))
1125
- }
1126
- ]);
1127
-
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
- }
1138
-
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 = [
1152
- {
1153
- type: 'input',
1154
- name: 'baseUrl',
1155
- message: 'Base URL:',
1156
- default: relay.baseUrl
1157
- },
1158
- {
1159
- type: 'list',
1160
- name: 'api',
1161
- message: 'API 接口类型:',
1162
- default: apiTypeDefault,
1163
- choices: apiChoices
1164
- },
1165
- {
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: '*'
1182
- }
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
- }
332
+ // ============ 设置 API Key ============
333
+ async function setApiKey(paths) {
334
+ console.log(chalk.cyan('🔑 设置 API Key\n'));
1201
335
 
1202
- const answers = await inquirer.prompt(questions);
336
+ const { apiKey } = await inquirer.prompt([{
337
+ type: 'password',
338
+ name: 'apiKey',
339
+ message: '请输入 API Key:',
340
+ mask: '*',
341
+ validate: input => input.trim() !== '' || 'API Key 不能为空'
342
+ }]);
1203
343
 
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
- }
344
+ let config = readConfig(paths.openclawConfig);
1210
345
 
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
- }
1229
-
1230
- await configManager.updateRelay(relayName, answers);
1231
- displaySuccess(`✅ 中转站 "${relayName}" 更新成功!`);
1232
- }
1233
-
1234
- // 删除中转站
1235
- async function deleteRelay(configManager) {
1236
- const relays = await configManager.listRelays();
1237
- if (relays.length === 0) {
1238
- displayInfo('没有可删除的中转站');
346
+ if (!config?.models?.providers) {
347
+ console.log(chalk.yellow('⚠️ 请先选择节点'));
1239
348
  return;
1240
349
  }
1241
350
 
1242
- const { relayName } = await inquirer.prompt([
1243
- {
1244
- type: 'list',
1245
- name: 'relayName',
1246
- message: '选择要删除的中转站:',
1247
- choices: relays.map(r => ({ name: `${r.name} (${r.baseUrl})`, value: r.name }))
1248
- }
1249
- ]);
1250
-
1251
- const { confirm } = await inquirer.prompt([
1252
- {
1253
- type: 'confirm',
1254
- name: 'confirm',
1255
- message: `确定要删除 "${relayName}" 吗?`,
1256
- default: false
1257
- }
1258
- ]);
1259
-
1260
- if (confirm) {
1261
- await configManager.deleteRelay(relayName);
1262
- displaySuccess(`✅ 中转站 "${relayName}" 已删除`);
1263
- }
1264
- }
1265
-
1266
- // 切换主中转站
1267
- async function switchRelay(configManager) {
1268
- const relays = await configManager.listRelays();
1269
- if (relays.length === 0) {
1270
- displayInfo('没有可用的中转站');
1271
- return;
351
+ // yunyi 节点设置 API Key
352
+ const providerName = Object.keys(config.models.providers).find(k => k.startsWith('yunyi-'));
353
+ if (providerName) {
354
+ config.models.providers[providerName].apiKey = apiKey.trim();
1272
355
  }
1273
356
 
1274
- const currentPrimary = await configManager.getPrimaryModel();
357
+ writeConfig(paths.openclawConfig, config);
1275
358
 
1276
- const { relayName } = await inquirer.prompt([
1277
- {
1278
- type: 'list',
1279
- name: 'relayName',
1280
- message: '选择主中转站:',
1281
- choices: relays.map(r => ({
1282
- name: `${r.name} - ${r.modelName} ${r.name === currentPrimary.provider ? chalk.green('(当前)') : ''}`,
1283
- value: r.name
1284
- }))
1285
- }
1286
- ]);
1287
-
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;
359
+ // 同时写入 auth-profiles
360
+ const authDir = path.dirname(paths.authProfiles);
361
+ if (!fs.existsSync(authDir)) {
362
+ fs.mkdirSync(authDir, { recursive: true });
1300
363
  }
1301
364
 
1302
- await configManager.setPrimaryModel(relayName, modelId);
1303
- displaySuccess(`✅ 已切换到 "${relayName}"`);
1304
- }
1305
-
1306
- // 管理模型配置
1307
- async function manageModel(configManager) {
1308
- const { modelAction } = await inquirer.prompt([
1309
- {
1310
- type: 'list',
1311
- name: 'modelAction',
1312
- message: '模型管理:',
1313
- choices: [
1314
- { name: '🔄 切换主模型', value: 'switch' },
1315
- { name: '📋 管理备用模型', value: 'fallback' },
1316
- { name: '⬅️ 返回', value: 'back' }
1317
- ]
1318
- }
1319
- ]);
1320
-
1321
- if (modelAction === 'back') return;
1322
-
1323
- switch (modelAction) {
1324
- case 'switch':
1325
- await switchRelay(configManager);
1326
- break;
1327
- case 'fallback':
1328
- await manageFallback(configManager);
1329
- break;
365
+ let authProfiles = {};
366
+ if (fs.existsSync(paths.authProfiles)) {
367
+ try { authProfiles = JSON.parse(fs.readFileSync(paths.authProfiles, 'utf8')); } catch {}
1330
368
  }
1331
- }
1332
369
 
1333
- // 管理备用模型
1334
- async function manageFallback(configManager) {
1335
- const relays = await configManager.listRelays();
1336
- const currentFallbacks = await configManager.getFallbackModels();
1337
- const availableRelays = relays.filter(r => r.modelId);
1338
-
1339
- if (availableRelays.length !== relays.length) {
1340
- displayWarning('部分中转站未配置模型 ID,已跳过');
370
+ if (providerName) {
371
+ authProfiles[`${providerName}:default`] = { apiKey: apiKey.trim() };
1341
372
  }
1342
373
 
1343
- const { selected } = await inquirer.prompt([
1344
- {
1345
- type: 'checkbox',
1346
- name: 'selected',
1347
- message: '选择备用模型(按优先级排序):',
1348
- choices: availableRelays.map(r => ({
1349
- name: `${r.name} - ${r.modelName}`,
1350
- value: `${r.name}/${r.modelId}`,
1351
- checked: currentFallbacks.includes(`${r.name}/${r.modelId}`)
1352
- }))
1353
- }
1354
- ]);
374
+ fs.writeFileSync(paths.authProfiles, JSON.stringify(authProfiles, null, 2), 'utf8');
1355
375
 
1356
- await configManager.setFallbackModels(selected);
1357
- displaySuccess('✅ 备用模型配置已更新');
376
+ console.log(chalk.green('\n✅ API Key 已保存'));
1358
377
  }
1359
378
 
1360
- // 管理 API Keys 和 Tokens
1361
- async function manageApiKey(configManager) {
1362
- const { keyAction } = await inquirer.prompt([
1363
- {
1364
- type: 'list',
1365
- name: 'keyAction',
1366
- message: 'API Key / Token 管理:',
1367
- choices: [
1368
- { name: '➕ 添加/更新 API Key', value: 'add' },
1369
- { name: '🔐 添加/更新 Token', value: 'addToken' },
1370
- { name: '👁️ 查看已配置的 Keys/Tokens', value: 'view' },
1371
- { name: '🗑️ 删除 API Key', value: 'delete' },
1372
- { name: '🗑️ 删除 Token', value: 'deleteToken' },
1373
- { name: '⬅️ 返回', value: 'back' }
1374
- ]
1375
- }
1376
- ]);
1377
-
1378
- if (keyAction === 'back') return;
1379
-
1380
- switch (keyAction) {
1381
- case 'add':
1382
- await addApiKey(configManager);
1383
- break;
1384
- case 'addToken':
1385
- await addToken(configManager);
1386
- break;
1387
- case 'view':
1388
- await viewApiKeys(configManager);
1389
- break;
1390
- case 'delete':
1391
- await deleteApiKey(configManager);
1392
- break;
1393
- case 'deleteToken':
1394
- await deleteToken(configManager);
1395
- break;
1396
- }
1397
- }
379
+ // ============ 查看配置 ============
380
+ async function viewConfig(paths) {
381
+ console.log(chalk.cyan('📋 当前配置\n'));
1398
382
 
1399
- // 添加 API Key
1400
- async function addApiKey(configManager) {
1401
- const relays = await configManager.listRelays();
383
+ const config = readConfig(paths.openclawConfig);
1402
384
 
1403
- if (relays.length === 0) {
1404
- displayInfo('请先添加中转站');
385
+ if (!config) {
386
+ console.log(chalk.yellow('配置文件不存在,请先选择节点'));
1405
387
  return;
1406
388
  }
1407
389
 
1408
- const { relayName } = await inquirer.prompt([
1409
- {
1410
- type: 'list',
1411
- name: 'relayName',
1412
- message: '选择中转站:',
1413
- choices: relays.map(r => ({ name: r.name, value: r.name }))
1414
- }
1415
- ]);
1416
-
1417
- const { apiKey } = await inquirer.prompt([
1418
- {
1419
- type: 'password',
1420
- name: 'apiKey',
1421
- message: '输入 API Key:',
1422
- mask: '*',
1423
- validate: (input) => input.trim() !== '' || 'API Key 不能为空'
1424
- }
1425
- ]);
1426
-
1427
- await configManager.setApiKey(relayName, apiKey);
1428
- displaySuccess(`✅ API Key 已保存到 "${relayName}"`);
1429
- }
1430
-
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
1464
- async function viewApiKeys(configManager) {
1465
- const keys = await configManager.listApiKeys();
1466
-
1467
- if (keys.length === 0) {
1468
- displayInfo('没有配置任何 API Key 或 Token');
1469
- return;
1470
- }
1471
-
1472
- console.log(chalk.cyan('\n已配置的 API Keys / Tokens:\n'));
1473
- keys.forEach(k => {
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)}`);
1479
- });
1480
- }
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
390
+ // 主模型
391
+ const primary = config.agents?.defaults?.model?.primary || '未设置';
392
+ console.log(chalk.yellow('当前模型:'));
393
+ console.log(` ${primary}\n`);
394
+
395
+ // 节点信息
396
+ console.log(chalk.yellow('当前节点:'));
397
+ if (config.models?.providers) {
398
+ const provider = Object.entries(config.models.providers).find(([name]) => name.startsWith('yunyi-'));
399
+ if (provider) {
400
+ const [name, data] = provider;
401
+ const hasKey = data.apiKey ? chalk.green('✓ 已设置') : chalk.red('✗ 未设置');
402
+ console.log(` ${name}: ${data.baseUrl}`);
403
+ console.log(` API Key: ${hasKey}`);
404
+ } else {
405
+ console.log(' 未配置');
1507
406
  }
1508
- ]);
1509
-
1510
- if (confirm) {
1511
- await configManager.deleteToken(provider);
1512
- displaySuccess(`✅ Token 已删除`);
1513
- }
1514
- }
1515
-
1516
- // 删除 API Key
1517
- async function deleteApiKey(configManager) {
1518
- const keys = await configManager.listApiKeys();
1519
- const keysWithApiKey = keys.filter(k => k.key);
1520
-
1521
- if (keysWithApiKey.length === 0) {
1522
- displayInfo('没有可删除的 API Key');
1523
- return;
1524
407
  }
408
+ console.log('');
1525
409
 
1526
- const { provider } = await inquirer.prompt([
1527
- {
1528
- type: 'list',
1529
- name: 'provider',
1530
- message: '选择要删除的 API Key:',
1531
- choices: keysWithApiKey.map(k => ({ name: k.provider, value: k.provider }))
1532
- }
1533
- ]);
1534
-
1535
- const { confirm } = await inquirer.prompt([
1536
- {
1537
- type: 'confirm',
1538
- name: 'confirm',
1539
- message: `确定要删除 "${provider}" 的 API Key 吗?`,
1540
- default: false
1541
- }
1542
- ]);
1543
-
1544
- if (confirm) {
1545
- await configManager.deleteApiKey(provider);
1546
- displaySuccess(`✅ API Key 已删除`);
1547
- }
410
+ // 备份状态
411
+ const backupPath = path.join(paths.configDir, BACKUP_FILENAME);
412
+ console.log(chalk.yellow('备份状态:'));
413
+ console.log(` ${fs.existsSync(backupPath) ? chalk.green('✓ 已备份') : chalk.gray('未备份')}`);
1548
414
  }
1549
415
 
1550
- // 测速并切换中转站
1551
- async function speedTestAndSwitch(configManager) {
1552
- const relays = await configManager.listRelays();
416
+ // ============ 恢复默认配置 ============
417
+ async function restore(paths) {
418
+ const backupPath = path.join(paths.configDir, BACKUP_FILENAME);
1553
419
 
1554
- if (relays.length === 0) {
1555
- displayInfo('没有可测速的中转站');
420
+ if (!fs.existsSync(backupPath)) {
421
+ console.log(chalk.yellow('⚠️ 没有找到备份文件'));
1556
422
  return;
1557
423
  }
1558
424
 
1559
- console.log(chalk.cyan('\n⚡ 开始测速...\n'));
1560
-
1561
- // 测试所有中转站
1562
- const results = await testMultipleRelays(relays);
1563
-
1564
- // 显示测速结果
1565
- console.log(chalk.yellow('测速结果:\n'));
1566
- results.forEach((result, index) => {
1567
- const status = result.success ? chalk.green('✓') : chalk.red('✗');
1568
- const latency = result.success
1569
- ? `${result.latency}ms (${formatLatency(result.latency)})`
1570
- : chalk.red(result.error);
425
+ const { confirm } = await inquirer.prompt([{
426
+ type: 'confirm',
427
+ name: 'confirm',
428
+ message: '确定要恢复默认配置吗?当前配置将被覆盖。',
429
+ default: false
430
+ }]);
1571
431
 
1572
- console.log(` ${status} ${index + 1}. ${result.name}`);
1573
- console.log(` URL: ${result.url}`);
1574
- console.log(` 延迟: ${latency}\n`);
1575
- });
1576
-
1577
- // 按速度排序
1578
- const sorted = sortBySpeed(results);
1579
-
1580
- if (sorted.length === 0) {
1581
- displayError('所有中转站都无法访问');
432
+ if (!confirm) {
433
+ console.log(chalk.gray('已取消'));
1582
434
  return;
1583
435
  }
1584
436
 
1585
- // 显示推荐
1586
- const fastest = sorted[0];
1587
- console.log(chalk.green(`\n🏆 最快的中转站: ${fastest.name} (${fastest.latency}ms)\n`));
1588
-
1589
- // 询问是否切换
1590
- const { action } = await inquirer.prompt([
1591
- {
1592
- type: 'list',
1593
- name: 'action',
1594
- message: '选择操作:',
1595
- choices: [
1596
- { name: `🚀 切换到最快的中转站 (${fastest.name})`, value: 'fastest' },
1597
- { name: '📋 手动选择中转站', value: 'manual' },
1598
- { name: '⬅️ 返回', value: 'back' }
1599
- ]
1600
- }
1601
- ]);
1602
-
1603
- if (action === 'back') return;
1604
-
1605
- if (action === 'fastest') {
1606
- // 切换到最快的
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);
1621
-
1622
- // 设置其他可用的为备用
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}`);
1627
- await configManager.setFallbackModels(fallbacks);
1628
-
1629
- displaySuccess(`✅ 已切换到 "${fastest.name}" (${fastest.latency}ms)`);
1630
- if (fallbacks.length > 0) {
1631
- console.log(chalk.blue(`备用中转站: ${fallbacks.map(f => f.split('/')[0]).join(', ')}`));
1632
- }
1633
- } else if (action === 'manual') {
1634
- // 手动选择
1635
- const { selected } = await inquirer.prompt([
1636
- {
1637
- type: 'list',
1638
- name: 'selected',
1639
- message: '选择要切换的中转站:',
1640
- choices: sorted.map(r => ({
1641
- name: `${r.name} - ${r.latency}ms`,
1642
- value: r.name
1643
- }))
1644
- }
1645
- ]);
1646
-
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);
1661
-
1662
- // 设置其他为备用
1663
- const fallbacks = sorted
1664
- .filter(r => r.name !== selected)
1665
- .map(r => relays.find(rel => rel.name === r.name))
1666
- .filter(relay => relay && relay.modelId)
1667
- .map(relay => `${relay.name}/${relay.modelId}`);
1668
- await configManager.setFallbackModels(fallbacks);
1669
-
1670
- const selectedResult = results.find(r => r.name === selected);
1671
- displaySuccess(`✅ 已切换到 "${selected}" (${selectedResult.latency}ms)`);
1672
- if (fallbacks.length > 0) {
1673
- console.log(chalk.blue(`备用中转站: ${fallbacks.map(f => f.split('/')[0]).join(', ')}`));
1674
- }
1675
- }
1676
- }
1677
-
1678
- // 高级设置
1679
- async function manageAdvanced(configManager) {
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)}`);
437
+ if (restoreDefaultConfig(paths.openclawConfig, paths.configDir)) {
438
+ console.log(chalk.green('\n✅ 已恢复默认配置'));
1692
439
  } else {
1693
- console.log(` 网关 Token: ${chalk.gray('(未设置)')}`);
440
+ console.log(chalk.red('\n❌ 恢复失败'));
1694
441
  }
1695
- console.log(` 默认工作区: ${chalk.gray(defaultWorkspace)}\n`);
1696
-
1697
- const answers = await inquirer.prompt([
1698
- {
1699
- type: 'number',
1700
- name: 'maxConcurrent',
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
- }
1710
- },
1711
- {
1712
- type: 'number',
1713
- name: 'subagentMaxConcurrent',
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
- }
1723
- },
1724
- {
1725
- type: 'input',
1726
- name: '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: '*'
1747
- }
1748
- ]);
1749
-
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);
1766
- displaySuccess('✅ 高级设置已更新');
1767
- }
1768
-
1769
- // 查看当前配置
1770
- async function viewConfig(configManager) {
1771
- const config = await configManager.getCurrentConfig();
1772
-
1773
- console.log(chalk.cyan('\n📋 当前配置:\n'));
1774
- console.log(chalk.yellow('主模型:'));
1775
- console.log(` ${config.primary}\n`);
1776
-
1777
- console.log(chalk.yellow('备用模型:'));
1778
- config.fallbacks.forEach((f, i) => {
1779
- console.log(` ${i + 1}. ${f}`);
1780
- });
1781
-
1782
- console.log(chalk.yellow('\n中转站:'));
1783
- config.relays.forEach(r => {
1784
- console.log(` ${chalk.green('●')} ${r.name}`);
1785
- console.log(` URL: ${r.baseUrl}`);
1786
- console.log(` 模型: ${r.modelName} (${r.modelId})`);
1787
- const contextInfo = r.contextWindow ? `${r.contextWindow} tokens` : '-';
1788
- console.log(` 上下文: ${contextInfo}`);
1789
- });
1790
-
1791
- console.log(chalk.yellow('\n高级设置:'));
1792
- console.log(` 最大并发: ${config.advanced.maxConcurrent}`);
1793
- console.log(` 子代理并发: ${config.advanced.subagentMaxConcurrent}`);
1794
- console.log(` 工作区: ${config.advanced.workspace}`);
1795
442
  }
1796
443
 
1797
- // 启动程序
444
+ // 启动
1798
445
  main().catch(error => {
1799
- displayError(`程序错误: ${error.message}`);
446
+ console.error(chalk.red(`程序错误: ${error.message}`));
1800
447
  process.exit(1);
1801
448
  });