openclawapi 1.0.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/cli.js ADDED
@@ -0,0 +1,662 @@
1
+ #!/usr/bin/env node
2
+
3
+ const inquirer = require('inquirer');
4
+ const chalk = require('chalk');
5
+ const path = require('path');
6
+ const os = require('os');
7
+ const { ConfigManager } = require('./lib/config-manager');
8
+ const { displayMenu, displaySuccess, displayError, displayInfo } = require('./lib/ui');
9
+ const { testMultipleRelays, sortBySpeed, formatLatency } = require('./lib/speed-test');
10
+
11
+ // 获取配置文件路径(跨平台)
12
+ function getConfigPath() {
13
+ const homeDir = os.homedir();
14
+ const configDir = path.join(homeDir, '.clawdbot');
15
+ return {
16
+ openclawConfig: path.join(configDir, 'openclaw.json'),
17
+ authProfiles: path.join(configDir, 'agents', 'main', 'agent', 'auth-profiles.json')
18
+ };
19
+ }
20
+
21
+ async function main() {
22
+ console.clear();
23
+ console.log(chalk.cyan.bold('\n🔧 OpenClaw 配置管理工具\n'));
24
+
25
+ const configPaths = getConfigPath();
26
+ const configManager = new ConfigManager(configPaths);
27
+
28
+ // 检查配置文件是否存在
29
+ const exists = await configManager.checkConfigExists();
30
+ if (!exists.openclaw) {
31
+ displayError(`配置文件不存在: ${configPaths.openclawConfig}`);
32
+ displayInfo('请先运行 clawdbot onboard 初始化配置');
33
+ process.exit(1);
34
+ }
35
+
36
+ while (true) {
37
+ const { action } = await inquirer.prompt([
38
+ {
39
+ type: 'list',
40
+ name: 'action',
41
+ message: '请选择操作:',
42
+ choices: [
43
+ { name: '📡 管理中转站配置', value: 'relay' },
44
+ { name: '🤖 管理模型配置', value: 'model' },
45
+ { name: '🔑 管理 API Keys', value: 'apikey' },
46
+ { name: '⚡ 测速并切换中转站', value: 'speedtest' },
47
+ { name: '⚙️ 高级设置', value: 'advanced' },
48
+ { name: '📄 查看当前配置', value: 'view' },
49
+ new inquirer.Separator(),
50
+ { name: '❌ 退出', value: 'exit' }
51
+ ]
52
+ }
53
+ ]);
54
+
55
+ if (action === 'exit') {
56
+ console.log(chalk.green('\n👋 再见!\n'));
57
+ process.exit(0);
58
+ }
59
+
60
+ try {
61
+ switch (action) {
62
+ case 'relay':
63
+ await manageRelay(configManager);
64
+ break;
65
+ case 'model':
66
+ await manageModel(configManager);
67
+ break;
68
+ case 'apikey':
69
+ await manageApiKey(configManager);
70
+ break;
71
+ case 'speedtest':
72
+ await speedTestAndSwitch(configManager);
73
+ break;
74
+ case 'advanced':
75
+ await manageAdvanced(configManager);
76
+ break;
77
+ case 'view':
78
+ await viewConfig(configManager);
79
+ break;
80
+ }
81
+ } catch (error) {
82
+ displayError(`操作失败: ${error.message}`);
83
+ }
84
+
85
+ console.log('\n');
86
+ }
87
+ }
88
+
89
+ // 管理中转站配置
90
+ async function manageRelay(configManager) {
91
+ const { relayAction } = await inquirer.prompt([
92
+ {
93
+ type: 'list',
94
+ name: 'relayAction',
95
+ message: '中转站管理:',
96
+ choices: [
97
+ { name: '➕ 添加新中转站', value: 'add' },
98
+ { name: '📝 编辑现有中转站', value: 'edit' },
99
+ { name: '🗑️ 删除中转站', value: 'delete' },
100
+ { name: '🔄 切换主中转站', value: 'switch' },
101
+ { name: '⬅️ 返回', value: 'back' }
102
+ ]
103
+ }
104
+ ]);
105
+
106
+ if (relayAction === 'back') return;
107
+
108
+ switch (relayAction) {
109
+ case 'add':
110
+ await addRelay(configManager);
111
+ break;
112
+ case 'edit':
113
+ await editRelay(configManager);
114
+ break;
115
+ case 'delete':
116
+ await deleteRelay(configManager);
117
+ break;
118
+ case 'switch':
119
+ await switchRelay(configManager);
120
+ break;
121
+ }
122
+ }
123
+
124
+ // 添加新中转站
125
+ async function addRelay(configManager) {
126
+ console.log(chalk.yellow('\n添加新中转站\n'));
127
+
128
+ const answers = await inquirer.prompt([
129
+ {
130
+ type: 'input',
131
+ name: 'name',
132
+ message: '中转站名称 (例如: claude-relay-1):',
133
+ validate: (input) => input.trim() !== '' || '名称不能为空'
134
+ },
135
+ {
136
+ type: 'input',
137
+ name: 'baseUrl',
138
+ message: 'Base URL (例如: https://yunyi.cfd/claude/v1):',
139
+ validate: (input) => {
140
+ try {
141
+ new URL(input);
142
+ return true;
143
+ } catch {
144
+ return '请输入有效的 URL';
145
+ }
146
+ }
147
+ },
148
+ {
149
+ type: 'list',
150
+ name: 'modelType',
151
+ message: '模型类型:',
152
+ choices: [
153
+ { name: 'Claude (Anthropic)', value: 'claude' },
154
+ { name: 'GPT (OpenAI)', value: 'gpt' },
155
+ { name: 'Gemini (Google)', value: 'gemini' },
156
+ { name: '其他', value: 'other' }
157
+ ]
158
+ }
159
+ ]);
160
+
161
+ let modelConfig;
162
+ if (answers.modelType === 'claude') {
163
+ const { modelId } = await inquirer.prompt([
164
+ {
165
+ type: 'list',
166
+ name: 'modelId',
167
+ message: '选择 Claude 模型:',
168
+ choices: [
169
+ { name: 'Claude Sonnet 4.5', value: 'claude-sonnet-4-5-20250929' },
170
+ { name: 'Claude Sonnet 3.7', value: 'claude-3-7-sonnet-latest' },
171
+ { name: 'Claude Opus 4.5', value: 'claude-opus-4-5-20251101' },
172
+ { name: '自定义', value: 'custom' }
173
+ ]
174
+ }
175
+ ]);
176
+
177
+ if (modelId === 'custom') {
178
+ const { customId } = await inquirer.prompt([
179
+ {
180
+ type: 'input',
181
+ name: 'customId',
182
+ message: '输入自定义模型 ID:',
183
+ validate: (input) => input.trim() !== '' || '模型 ID 不能为空'
184
+ }
185
+ ]);
186
+ modelConfig = { id: customId, name: customId };
187
+ } else {
188
+ modelConfig = { id: modelId, name: 'Claude Sonnet 4.5' };
189
+ }
190
+ } else if (answers.modelType === 'gpt') {
191
+ modelConfig = { id: 'gpt-4o', name: 'GPT-4o' };
192
+ } else if (answers.modelType === 'gemini') {
193
+ modelConfig = { id: 'gemini-3-pro', name: 'Gemini 3 Pro' };
194
+ } else {
195
+ const { customId, customName } = await inquirer.prompt([
196
+ {
197
+ type: 'input',
198
+ name: 'customId',
199
+ message: '模型 ID:',
200
+ validate: (input) => input.trim() !== '' || '模型 ID 不能为空'
201
+ },
202
+ {
203
+ type: 'input',
204
+ name: 'customName',
205
+ message: '模型显示名称:',
206
+ validate: (input) => input.trim() !== '' || '名称不能为空'
207
+ }
208
+ ]);
209
+ modelConfig = { id: customId, name: customName };
210
+ }
211
+
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
+ await configManager.addRelay({
228
+ name: answers.name,
229
+ baseUrl: answers.baseUrl,
230
+ model: {
231
+ id: modelConfig.id,
232
+ name: modelConfig.name,
233
+ contextWindow,
234
+ maxTokens
235
+ }
236
+ });
237
+
238
+ displaySuccess(`✅ 中转站 "${answers.name}" 添加成功!`);
239
+ }
240
+
241
+ // 编辑中转站
242
+ async function editRelay(configManager) {
243
+ const relays = await configManager.listRelays();
244
+ if (relays.length === 0) {
245
+ displayInfo('没有可编辑的中转站');
246
+ return;
247
+ }
248
+
249
+ const { relayName } = await inquirer.prompt([
250
+ {
251
+ type: 'list',
252
+ name: 'relayName',
253
+ message: '选择要编辑的中转站:',
254
+ choices: relays.map(r => ({ name: `${r.name} (${r.baseUrl})`, value: r.name }))
255
+ }
256
+ ]);
257
+
258
+ const relay = relays.find(r => r.name === relayName);
259
+
260
+ const answers = await inquirer.prompt([
261
+ {
262
+ type: 'input',
263
+ name: 'baseUrl',
264
+ message: 'Base URL:',
265
+ default: relay.baseUrl
266
+ },
267
+ {
268
+ type: 'number',
269
+ name: 'contextWindow',
270
+ message: '上下文窗口:',
271
+ default: relay.contextWindow
272
+ },
273
+ {
274
+ type: 'number',
275
+ name: 'maxTokens',
276
+ message: '最大输出 tokens:',
277
+ default: relay.maxTokens
278
+ }
279
+ ]);
280
+
281
+ await configManager.updateRelay(relayName, answers);
282
+ displaySuccess(`✅ 中转站 "${relayName}" 更新成功!`);
283
+ }
284
+
285
+ // 删除中转站
286
+ async function deleteRelay(configManager) {
287
+ const relays = await configManager.listRelays();
288
+ if (relays.length === 0) {
289
+ displayInfo('没有可删除的中转站');
290
+ return;
291
+ }
292
+
293
+ const { relayName } = await inquirer.prompt([
294
+ {
295
+ type: 'list',
296
+ name: 'relayName',
297
+ message: '选择要删除的中转站:',
298
+ choices: relays.map(r => ({ name: `${r.name} (${r.baseUrl})`, value: r.name }))
299
+ }
300
+ ]);
301
+
302
+ const { confirm } = await inquirer.prompt([
303
+ {
304
+ type: 'confirm',
305
+ name: 'confirm',
306
+ message: `确定要删除 "${relayName}" 吗?`,
307
+ default: false
308
+ }
309
+ ]);
310
+
311
+ if (confirm) {
312
+ await configManager.deleteRelay(relayName);
313
+ displaySuccess(`✅ 中转站 "${relayName}" 已删除`);
314
+ }
315
+ }
316
+
317
+ // 切换主中转站
318
+ async function switchRelay(configManager) {
319
+ const relays = await configManager.listRelays();
320
+ if (relays.length === 0) {
321
+ displayInfo('没有可用的中转站');
322
+ return;
323
+ }
324
+
325
+ const currentPrimary = await configManager.getPrimaryModel();
326
+
327
+ const { relayName } = await inquirer.prompt([
328
+ {
329
+ type: 'list',
330
+ name: 'relayName',
331
+ message: '选择主中转站:',
332
+ choices: relays.map(r => ({
333
+ name: `${r.name} - ${r.modelName} ${r.name === currentPrimary.provider ? chalk.green('(当前)') : ''}`,
334
+ value: r.name
335
+ }))
336
+ }
337
+ ]);
338
+
339
+ await configManager.setPrimaryModel(relayName);
340
+ displaySuccess(`✅ 已切换到 "${relayName}"`);
341
+ }
342
+
343
+ // 管理模型配置
344
+ async function manageModel(configManager) {
345
+ const { modelAction } = await inquirer.prompt([
346
+ {
347
+ type: 'list',
348
+ name: 'modelAction',
349
+ message: '模型管理:',
350
+ choices: [
351
+ { name: '🔄 切换主模型', value: 'switch' },
352
+ { name: '📋 管理备用模型', value: 'fallback' },
353
+ { name: '⬅️ 返回', value: 'back' }
354
+ ]
355
+ }
356
+ ]);
357
+
358
+ if (modelAction === 'back') return;
359
+
360
+ switch (modelAction) {
361
+ case 'switch':
362
+ await switchRelay(configManager);
363
+ break;
364
+ case 'fallback':
365
+ await manageFallback(configManager);
366
+ break;
367
+ }
368
+ }
369
+
370
+ // 管理备用模型
371
+ async function manageFallback(configManager) {
372
+ const relays = await configManager.listRelays();
373
+ const currentFallbacks = await configManager.getFallbackModels();
374
+
375
+ const { selected } = await inquirer.prompt([
376
+ {
377
+ type: 'checkbox',
378
+ name: 'selected',
379
+ message: '选择备用模型(按优先级排序):',
380
+ choices: relays.map(r => ({
381
+ name: `${r.name} - ${r.modelName}`,
382
+ value: `${r.name}/${r.modelId}`,
383
+ checked: currentFallbacks.includes(`${r.name}/${r.modelId}`)
384
+ }))
385
+ }
386
+ ]);
387
+
388
+ await configManager.setFallbackModels(selected);
389
+ displaySuccess('✅ 备用模型配置已更新');
390
+ }
391
+
392
+ // 管理 API Keys
393
+ async function manageApiKey(configManager) {
394
+ const { keyAction } = await inquirer.prompt([
395
+ {
396
+ type: 'list',
397
+ name: 'keyAction',
398
+ message: 'API Key 管理:',
399
+ choices: [
400
+ { name: '➕ 添加/更新 API Key', value: 'add' },
401
+ { name: '👁️ 查看已配置的 Keys', value: 'view' },
402
+ { name: '🗑️ 删除 API Key', value: 'delete' },
403
+ { name: '⬅️ 返回', value: 'back' }
404
+ ]
405
+ }
406
+ ]);
407
+
408
+ if (keyAction === 'back') return;
409
+
410
+ switch (keyAction) {
411
+ case 'add':
412
+ await addApiKey(configManager);
413
+ break;
414
+ case 'view':
415
+ await viewApiKeys(configManager);
416
+ break;
417
+ case 'delete':
418
+ await deleteApiKey(configManager);
419
+ break;
420
+ }
421
+ }
422
+
423
+ // 添加 API Key
424
+ async function addApiKey(configManager) {
425
+ const relays = await configManager.listRelays();
426
+
427
+ const { relayName } = await inquirer.prompt([
428
+ {
429
+ type: 'list',
430
+ name: 'relayName',
431
+ message: '选择中转站:',
432
+ choices: relays.map(r => ({ name: r.name, value: r.name }))
433
+ }
434
+ ]);
435
+
436
+ const { apiKey } = await inquirer.prompt([
437
+ {
438
+ type: 'password',
439
+ name: 'apiKey',
440
+ message: '输入 API Key:',
441
+ mask: '*',
442
+ validate: (input) => input.trim() !== '' || 'API Key 不能为空'
443
+ }
444
+ ]);
445
+
446
+ await configManager.setApiKey(relayName, apiKey);
447
+ displaySuccess(`✅ API Key 已保存到 "${relayName}"`);
448
+ }
449
+
450
+ // 查看 API Keys
451
+ async function viewApiKeys(configManager) {
452
+ const keys = await configManager.listApiKeys();
453
+
454
+ if (keys.length === 0) {
455
+ displayInfo('没有配置任何 API Key');
456
+ return;
457
+ }
458
+
459
+ console.log(chalk.cyan('\n已配置的 API Keys:\n'));
460
+ keys.forEach(k => {
461
+ const masked = k.key ? `${k.key.substring(0, 8)}...${k.key.substring(k.key.length - 4)}` : '未设置';
462
+ console.log(` ${chalk.green('●')} ${k.provider}: ${masked}`);
463
+ });
464
+ }
465
+
466
+ // 删除 API Key
467
+ async function deleteApiKey(configManager) {
468
+ const keys = await configManager.listApiKeys();
469
+
470
+ if (keys.length === 0) {
471
+ displayInfo('没有可删除的 API Key');
472
+ return;
473
+ }
474
+
475
+ const { provider } = await inquirer.prompt([
476
+ {
477
+ type: 'list',
478
+ name: 'provider',
479
+ message: '选择要删除的 API Key:',
480
+ choices: keys.map(k => ({ name: k.provider, value: k.provider }))
481
+ }
482
+ ]);
483
+
484
+ const { confirm } = await inquirer.prompt([
485
+ {
486
+ type: 'confirm',
487
+ name: 'confirm',
488
+ message: `确定要删除 "${provider}" 的 API Key 吗?`,
489
+ default: false
490
+ }
491
+ ]);
492
+
493
+ if (confirm) {
494
+ await configManager.deleteApiKey(provider);
495
+ displaySuccess(`✅ API Key 已删除`);
496
+ }
497
+ }
498
+
499
+ // 测速并切换中转站
500
+ async function speedTestAndSwitch(configManager) {
501
+ const relays = await configManager.listRelays();
502
+
503
+ if (relays.length === 0) {
504
+ displayInfo('没有可测速的中转站');
505
+ return;
506
+ }
507
+
508
+ console.log(chalk.cyan('\n⚡ 开始测速...\n'));
509
+
510
+ // 测试所有中转站
511
+ const results = await testMultipleRelays(relays);
512
+
513
+ // 显示测速结果
514
+ console.log(chalk.yellow('测速结果:\n'));
515
+ results.forEach((result, index) => {
516
+ const status = result.success ? chalk.green('✓') : chalk.red('✗');
517
+ const latency = result.success
518
+ ? `${result.latency}ms (${formatLatency(result.latency)})`
519
+ : chalk.red(result.error);
520
+
521
+ console.log(` ${status} ${index + 1}. ${result.name}`);
522
+ console.log(` URL: ${result.url}`);
523
+ console.log(` 延迟: ${latency}\n`);
524
+ });
525
+
526
+ // 按速度排序
527
+ const sorted = sortBySpeed(results);
528
+
529
+ if (sorted.length === 0) {
530
+ displayError('所有中转站都无法访问');
531
+ return;
532
+ }
533
+
534
+ // 显示推荐
535
+ const fastest = sorted[0];
536
+ console.log(chalk.green(`\n🏆 最快的中转站: ${fastest.name} (${fastest.latency}ms)\n`));
537
+
538
+ // 询问是否切换
539
+ const { action } = await inquirer.prompt([
540
+ {
541
+ type: 'list',
542
+ name: 'action',
543
+ message: '选择操作:',
544
+ choices: [
545
+ { name: `🚀 切换到最快的中转站 (${fastest.name})`, value: 'fastest' },
546
+ { name: '📋 手动选择中转站', value: 'manual' },
547
+ { name: '⬅️ 返回', value: 'back' }
548
+ ]
549
+ }
550
+ ]);
551
+
552
+ if (action === 'back') return;
553
+
554
+ if (action === 'fastest') {
555
+ // 切换到最快的
556
+ await configManager.setPrimaryModel(fastest.name);
557
+
558
+ // 设置其他可用的为备用
559
+ const fallbacks = sorted.slice(1).map(r => {
560
+ const relay = relays.find(rel => rel.name === r.name);
561
+ return `${relay.name}/${relay.modelId}`;
562
+ });
563
+ await configManager.setFallbackModels(fallbacks);
564
+
565
+ displaySuccess(`✅ 已切换到 "${fastest.name}" (${fastest.latency}ms)`);
566
+ if (fallbacks.length > 0) {
567
+ console.log(chalk.blue(`备用中转站: ${fallbacks.map(f => f.split('/')[0]).join(', ')}`));
568
+ }
569
+ } else if (action === 'manual') {
570
+ // 手动选择
571
+ const { selected } = await inquirer.prompt([
572
+ {
573
+ type: 'list',
574
+ name: 'selected',
575
+ message: '选择要切换的中转站:',
576
+ choices: sorted.map(r => ({
577
+ name: `${r.name} - ${r.latency}ms`,
578
+ value: r.name
579
+ }))
580
+ }
581
+ ]);
582
+
583
+ await configManager.setPrimaryModel(selected);
584
+
585
+ // 设置其他为备用
586
+ const fallbacks = sorted
587
+ .filter(r => r.name !== selected)
588
+ .map(r => {
589
+ const relay = relays.find(rel => rel.name === r.name);
590
+ return `${relay.name}/${relay.modelId}`;
591
+ });
592
+ await configManager.setFallbackModels(fallbacks);
593
+
594
+ const selectedResult = results.find(r => r.name === selected);
595
+ displaySuccess(`✅ 已切换到 "${selected}" (${selectedResult.latency}ms)`);
596
+ if (fallbacks.length > 0) {
597
+ console.log(chalk.blue(`备用中转站: ${fallbacks.map(f => f.split('/')[0]).join(', ')}`));
598
+ }
599
+ }
600
+ }
601
+
602
+ // 高级设置
603
+ async function manageAdvanced(configManager) {
604
+ const currentConfig = await configManager.getAdvancedSettings();
605
+
606
+ const answers = await inquirer.prompt([
607
+ {
608
+ type: 'number',
609
+ name: 'maxConcurrent',
610
+ message: '最大并发任务数:',
611
+ default: currentConfig.maxConcurrent || 4
612
+ },
613
+ {
614
+ type: 'number',
615
+ name: 'subagentMaxConcurrent',
616
+ message: '子代理最大并发数:',
617
+ default: currentConfig.subagentMaxConcurrent || 8
618
+ },
619
+ {
620
+ type: 'input',
621
+ name: 'workspace',
622
+ message: '工作区路径:',
623
+ default: currentConfig.workspace || path.join(os.homedir(), '.openclaw', 'workspace')
624
+ }
625
+ ]);
626
+
627
+ await configManager.setAdvancedSettings(answers);
628
+ displaySuccess('✅ 高级设置已更新');
629
+ }
630
+
631
+ // 查看当前配置
632
+ async function viewConfig(configManager) {
633
+ const config = await configManager.getCurrentConfig();
634
+
635
+ console.log(chalk.cyan('\n📋 当前配置:\n'));
636
+ console.log(chalk.yellow('主模型:'));
637
+ console.log(` ${config.primary}\n`);
638
+
639
+ console.log(chalk.yellow('备用模型:'));
640
+ config.fallbacks.forEach((f, i) => {
641
+ console.log(` ${i + 1}. ${f}`);
642
+ });
643
+
644
+ console.log(chalk.yellow('\n中转站:'));
645
+ config.relays.forEach(r => {
646
+ console.log(` ${chalk.green('●')} ${r.name}`);
647
+ console.log(` URL: ${r.baseUrl}`);
648
+ console.log(` 模型: ${r.modelName} (${r.modelId})`);
649
+ console.log(` 上下文: ${r.contextWindow} tokens`);
650
+ });
651
+
652
+ console.log(chalk.yellow('\n高级设置:'));
653
+ console.log(` 最大并发: ${config.advanced.maxConcurrent}`);
654
+ console.log(` 子代理并发: ${config.advanced.subagentMaxConcurrent}`);
655
+ console.log(` 工作区: ${config.advanced.workspace}`);
656
+ }
657
+
658
+ // 启动程序
659
+ main().catch(error => {
660
+ displayError(`程序错误: ${error.message}`);
661
+ process.exit(1);
662
+ });