openclawapi 1.2.0 → 1.3.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.
Files changed (2) hide show
  1. package/cli.js +173 -137
  2. package/package.json +1 -1
package/cli.js CHANGED
@@ -20,13 +20,37 @@ const ENDPOINTS = [
20
20
  { name: '备用节点2', url: 'http://47.97.100.10' }
21
21
  ];
22
22
 
23
- // 模型预设
24
- const MODELS = [
23
+ // Claude 模型预设
24
+ const CLAUDE_MODELS = [
25
25
  { id: 'claude-opus-4-5', name: 'Claude Opus 4.5' },
26
26
  { id: 'claude-sonnet-4-5', name: 'Claude Sonnet 4.5' },
27
27
  { id: 'claude-haiku-4-5', name: 'Claude Haiku 4.5' }
28
28
  ];
29
29
 
30
+ // Codex 模型预设
31
+ const CODEX_MODELS = [
32
+ { id: 'gpt-5.2', name: 'GPT 5.2' },
33
+ { id: 'gpt-5.2-codex', name: 'GPT 5.2 Codex' }
34
+ ];
35
+
36
+ // API 类型配置
37
+ const API_CONFIG = {
38
+ claude: {
39
+ urlSuffix: '/claude/v1/messages',
40
+ api: 'anthropic-messages',
41
+ contextWindow: 200000,
42
+ maxTokens: 8192,
43
+ providerName: 'yunyi-claude'
44
+ },
45
+ codex: {
46
+ urlSuffix: '/codex/response',
47
+ api: 'openai-responses',
48
+ contextWindow: 128000,
49
+ maxTokens: 32768,
50
+ providerName: 'yunyi-codex'
51
+ }
52
+ };
53
+
30
54
  // 备份文件名
31
55
  const BACKUP_FILENAME = 'openclaw-default.json.bak';
32
56
 
@@ -88,7 +112,6 @@ function getConfigPath() {
88
112
 
89
113
  const configDir = path.dirname(openclawConfig);
90
114
 
91
- // 查找 auth-profiles 路径
92
115
  const authCandidates = [
93
116
  path.join(openclawStateDir, 'agents', 'main', 'agent', 'auth-profiles.json'),
94
117
  path.join(clawdbotStateDir, 'agents', 'main', 'agent', 'auth-profiles.json'),
@@ -136,17 +159,17 @@ function restoreDefaultConfig(configPath, configDir) {
136
159
  }
137
160
 
138
161
  // ============ URL 构建 ============
139
- function buildFullUrl(baseUrl) {
162
+ function buildFullUrl(baseUrl, type) {
140
163
  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';
164
+ const suffix = API_CONFIG[type].urlSuffix;
165
+ if (trimmed.includes(suffix)) return trimmed;
166
+ return trimmed + suffix;
144
167
  }
145
168
 
146
169
  // ============ 主程序 ============
147
170
  async function main() {
148
171
  console.clear();
149
- console.log(chalk.cyan.bold('\n🔧 OpenClaw 配置工具 (简化版)\n'));
172
+ console.log(chalk.cyan.bold('\n🔧 OpenClaw 配置工具\n'));
150
173
 
151
174
  const paths = getConfigPath();
152
175
  console.log(chalk.gray(`配置文件: ${paths.openclawConfig}\n`));
@@ -162,8 +185,12 @@ async function main() {
162
185
  name: 'action',
163
186
  message: '请选择操作:',
164
187
  choices: [
165
- { name: '🌐 选择节点 (测速 + 自动配置备用)', value: 'select_node' },
166
- { name: '🤖 选择默认模型', value: 'select_model' },
188
+ { name: '🔵 选择 Claude 节点', value: 'select_claude' },
189
+ { name: '🟢 选择 Codex 节点', value: 'select_codex' },
190
+ new inquirer.Separator(),
191
+ { name: '⚡ 激活 Claude', value: 'activate_claude' },
192
+ { name: '⚡ 激活 Codex', value: 'activate_codex' },
193
+ new inquirer.Separator(),
167
194
  { name: '🔑 设置 API Key', value: 'set_apikey' },
168
195
  { name: '📋 查看当前配置', value: 'view_config' },
169
196
  { name: '🔄 恢复默认配置', value: 'restore' },
@@ -176,11 +203,17 @@ async function main() {
176
203
 
177
204
  try {
178
205
  switch (action) {
179
- case 'select_node':
180
- await selectNode(paths);
206
+ case 'select_claude':
207
+ await selectNode(paths, 'claude');
208
+ break;
209
+ case 'select_codex':
210
+ await selectNode(paths, 'codex');
181
211
  break;
182
- case 'select_model':
183
- await selectModel(paths);
212
+ case 'activate_claude':
213
+ await activate(paths, 'claude');
214
+ break;
215
+ case 'activate_codex':
216
+ await activate(paths, 'codex');
184
217
  break;
185
218
  case 'set_apikey':
186
219
  await setApiKey(paths);
@@ -203,13 +236,16 @@ async function main() {
203
236
  }
204
237
  }
205
238
 
206
- // ============ 选择节点 ============
207
- async function selectNode(paths) {
208
- console.log(chalk.cyan('📡 节点测速中...\n'));
239
+ // ============ 选择节点 (Claude/Codex) ============
240
+ async function selectNode(paths, type) {
241
+ const typeLabel = type === 'claude' ? 'Claude' : 'Codex';
242
+ const models = type === 'claude' ? CLAUDE_MODELS : CODEX_MODELS;
243
+ const apiConfig = API_CONFIG[type];
244
+
245
+ console.log(chalk.cyan(`📡 ${typeLabel} 节点测速中...\n`));
209
246
 
210
247
  const results = await testAllEndpoints();
211
248
 
212
- // 按延迟排序
213
249
  const sorted = results
214
250
  .filter(r => r.success)
215
251
  .sort((a, b) => a.latency - b.latency);
@@ -221,11 +257,11 @@ async function selectNode(paths) {
221
257
 
222
258
  console.log(chalk.green(`\n🏆 最快节点: ${sorted[0].name} (${sorted[0].latency}ms)\n`));
223
259
 
224
- // 选择主节点
260
+ // 选择节点
225
261
  const { selectedIndex } = await inquirer.prompt([{
226
262
  type: 'list',
227
263
  name: 'selectedIndex',
228
- message: '选择主节点 (其他自动设为备用):',
264
+ message: '选择节点:',
229
265
  choices: [
230
266
  { name: `🚀 使用最快节点 (${sorted[0].name})`, value: -1 },
231
267
  new inquirer.Separator('--- 或手动选择 ---'),
@@ -237,22 +273,20 @@ async function selectNode(paths) {
237
273
  }]);
238
274
 
239
275
  const primaryIndex = selectedIndex === -1 ? 0 : selectedIndex;
240
- const primaryEndpoint = sorted[primaryIndex];
241
- const fallbackEndpoints = sorted.filter((_, i) => i !== primaryIndex);
276
+ const selectedEndpoint = sorted[primaryIndex];
242
277
 
243
- // 读取或创建配置
244
- let config = readConfig(paths.openclawConfig) || {};
278
+ // 选择模型
279
+ const { selectedModel } = await inquirer.prompt([{
280
+ type: 'list',
281
+ name: 'selectedModel',
282
+ message: `选择 ${typeLabel} 模型:`,
283
+ choices: models.map(m => ({ name: m.name, value: m.id }))
284
+ }]);
245
285
 
246
- // 获取当前模型(保持不变)
247
- const currentModelId = config.agents?.defaults?.model?.primary?.split('/')[1] || MODELS[0].id;
248
- const modelConfig = MODELS.find(m => m.id === currentModelId) || MODELS[0];
286
+ const modelConfig = models.find(m => m.id === selectedModel);
249
287
 
250
- // 清除旧的 yunyi 配置
251
- if (config.models?.providers) {
252
- Object.keys(config.models.providers).forEach(key => {
253
- if (key.startsWith('yunyi-')) delete config.models.providers[key];
254
- });
255
- }
288
+ // 读取或创建配置
289
+ let config = readConfig(paths.openclawConfig) || {};
256
290
 
257
291
  // 初始化结构
258
292
  if (!config.models) config.models = {};
@@ -262,101 +296,93 @@ async function selectNode(paths) {
262
296
  if (!config.agents.defaults.model) config.agents.defaults.model = {};
263
297
  if (!config.agents.defaults.models) config.agents.defaults.models = {};
264
298
 
265
- // 添加主节点
266
- const primaryName = 'yunyi-001';
267
- config.models.providers[primaryName] = {
268
- baseUrl: buildFullUrl(primaryEndpoint.url),
269
- api: 'anthropic-messages',
299
+ // 保留旧的 API Key
300
+ const oldProvider = config.models.providers[apiConfig.providerName];
301
+ const oldApiKey = oldProvider?.apiKey;
302
+
303
+ // 添加/更新节点
304
+ config.models.providers[apiConfig.providerName] = {
305
+ baseUrl: buildFullUrl(selectedEndpoint.url, type),
306
+ api: apiConfig.api,
307
+ apiKey: oldApiKey,
270
308
  models: [{
271
309
  id: modelConfig.id,
272
310
  name: modelConfig.name,
273
- contextWindow: 200000,
274
- maxTokens: 8192
311
+ contextWindow: apiConfig.contextWindow,
312
+ maxTokens: apiConfig.maxTokens
275
313
  }]
276
314
  };
277
315
 
278
- // 设置主模型
279
- config.agents.defaults.model.primary = `${primaryName}/${modelConfig.id}`;
280
- config.agents.defaults.models[`${primaryName}/${modelConfig.id}`] = { alias: primaryName };
281
-
282
- // 添加备用节点
283
- const fallbacks = [];
284
- fallbackEndpoints.forEach((endpoint, i) => {
285
- const name = `yunyi-${String(i + 2).padStart(3, '0')}`;
286
- config.models.providers[name] = {
287
- baseUrl: buildFullUrl(endpoint.url),
288
- api: 'anthropic-messages',
289
- models: [{
290
- id: modelConfig.id,
291
- name: modelConfig.name,
292
- contextWindow: 200000,
293
- maxTokens: 8192
294
- }]
295
- };
296
- fallbacks.push(`${name}/${modelConfig.id}`);
297
- config.agents.defaults.models[`${name}/${modelConfig.id}`] = { alias: name };
298
- });
299
-
300
- config.agents.defaults.model.fallbacks = fallbacks;
316
+ // 注册模型
317
+ const modelKey = `${apiConfig.providerName}/${modelConfig.id}`;
318
+ config.agents.defaults.models[modelKey] = { alias: apiConfig.providerName };
301
319
 
302
320
  writeConfig(paths.openclawConfig, config);
303
321
 
304
- console.log(chalk.green(`\n✅ 配置完成!`));
305
- console.log(chalk.cyan(` 主节点: ${primaryEndpoint.name} (${primaryEndpoint.url})`));
306
- console.log(chalk.gray(` 备用节点: ${fallbackEndpoints.length} 个`));
307
- console.log(chalk.gray(` 当前模型: ${modelConfig.name}`));
322
+ console.log(chalk.green(`\n✅ ${typeLabel} 节点配置完成!`));
323
+ console.log(chalk.cyan(` 节点: ${selectedEndpoint.name} (${selectedEndpoint.url})`));
324
+ console.log(chalk.gray(` 模型: ${modelConfig.name}`));
325
+ console.log(chalk.gray(` API Key: ${oldApiKey ? '已设置' : '未设置'}`));
308
326
  }
309
327
 
310
- // ============ 选择模型 ============
311
- async function selectModel(paths) {
312
- console.log(chalk.cyan('🤖 选择默认模型\n'));
328
+ // ============ 激活 (Claude/Codex) ============
329
+ async function activate(paths, type) {
330
+ const typeLabel = type === 'claude' ? 'Claude' : 'Codex';
331
+ const apiConfig = API_CONFIG[type];
332
+ const models = type === 'claude' ? CLAUDE_MODELS : CODEX_MODELS;
313
333
 
314
- const { selectedModel } = await inquirer.prompt([{
315
- type: 'list',
316
- name: 'selectedModel',
317
- message: '选择模型:',
318
- choices: MODELS.map(m => ({ name: m.name, value: m.id }))
319
- }]);
320
-
321
- const modelConfig = MODELS.find(m => m.id === selectedModel);
322
334
  let config = readConfig(paths.openclawConfig);
323
335
 
324
- if (!config?.models?.providers) {
325
- console.log(chalk.yellow('⚠️ 请先选择节点'));
336
+ if (!config?.models?.providers?.[apiConfig.providerName]) {
337
+ console.log(chalk.yellow(`⚠️ 请先选择 ${typeLabel} 节点`));
326
338
  return;
327
339
  }
328
340
 
329
- // 更新所有 yunyi 节点的模型
330
- Object.keys(config.models.providers).forEach(name => {
331
- if (name.startsWith('yunyi-')) {
332
- config.models.providers[name].models = [{
333
- id: modelConfig.id,
334
- name: modelConfig.name,
335
- contextWindow: 200000,
336
- maxTokens: 8192
337
- }];
338
- }
339
- });
341
+ const provider = config.models.providers[apiConfig.providerName];
342
+ const currentModelId = provider.models?.[0]?.id || models[0].id;
343
+ const modelConfig = models.find(m => m.id === currentModelId) || models[0];
340
344
 
341
- // 更新主模型和备用模型引用
342
- const providers = Object.keys(config.models.providers).filter(k => k.startsWith('yunyi-')).sort();
343
- if (providers.length > 0) {
344
- config.agents.defaults.model.primary = `${providers[0]}/${modelConfig.id}`;
345
- config.agents.defaults.model.fallbacks = providers.slice(1).map(p => `${p}/${modelConfig.id}`);
346
- config.agents.defaults.models = {};
347
- providers.forEach(p => {
348
- config.agents.defaults.models[`${p}/${modelConfig.id}`] = { alias: p };
349
- });
350
- }
345
+ // 设置为主模型
346
+ const modelKey = `${apiConfig.providerName}/${modelConfig.id}`;
347
+ config.agents.defaults.model.primary = modelKey;
348
+ config.agents.defaults.model.fallbacks = [];
351
349
 
352
350
  writeConfig(paths.openclawConfig, config);
353
- console.log(chalk.green(`\n✅ 模型已切换为: ${modelConfig.name}`));
351
+
352
+ console.log(chalk.green(`✅ 已激活 ${typeLabel}`));
353
+ console.log(chalk.cyan(` 节点: ${provider.baseUrl}`));
354
+ console.log(chalk.gray(` 模型: ${modelConfig.name}`));
354
355
  }
355
356
 
356
357
  // ============ 设置 API Key ============
357
358
  async function setApiKey(paths) {
358
359
  console.log(chalk.cyan('🔑 设置 API Key\n'));
359
360
 
361
+ let config = readConfig(paths.openclawConfig);
362
+
363
+ // 检查已配置的节点
364
+ const claudeConfigured = config?.models?.providers?.['yunyi-claude'];
365
+ const codexConfigured = config?.models?.providers?.['yunyi-codex'];
366
+
367
+ if (!claudeConfigured && !codexConfigured) {
368
+ console.log(chalk.yellow('⚠️ 请先选择节点'));
369
+ return;
370
+ }
371
+
372
+ const choices = [];
373
+ if (claudeConfigured) choices.push({ name: '🔵 Claude', value: 'claude' });
374
+ if (codexConfigured) choices.push({ name: '🟢 Codex', value: 'codex' });
375
+ if (claudeConfigured && codexConfigured) {
376
+ choices.push({ name: '🔷 两者都设置 (相同 Key)', value: 'both' });
377
+ }
378
+
379
+ const { target } = await inquirer.prompt([{
380
+ type: 'list',
381
+ name: 'target',
382
+ message: '为哪个设置 API Key?',
383
+ choices
384
+ }]);
385
+
360
386
  const { apiKey } = await inquirer.prompt([{
361
387
  type: 'password',
362
388
  name: 'apiKey',
@@ -365,17 +391,13 @@ async function setApiKey(paths) {
365
391
  validate: input => input.trim() !== '' || 'API Key 不能为空'
366
392
  }]);
367
393
 
368
- let config = readConfig(paths.openclawConfig);
369
-
370
- if (!config?.models?.providers) {
371
- console.log(chalk.yellow('⚠️ 请先选择节点'));
372
- return;
373
- }
394
+ const providers = [];
395
+ if (target === 'claude' || target === 'both') providers.push('yunyi-claude');
396
+ if (target === 'codex' || target === 'both') providers.push('yunyi-codex');
374
397
 
375
- // 为所有 yunyi 节点设置 API Key
376
- Object.keys(config.models.providers).forEach(name => {
377
- if (name.startsWith('yunyi-')) {
378
- config.models.providers[name].apiKey = apiKey.trim();
398
+ providers.forEach(providerName => {
399
+ if (config.models.providers[providerName]) {
400
+ config.models.providers[providerName].apiKey = apiKey.trim();
379
401
  }
380
402
  });
381
403
 
@@ -392,10 +414,8 @@ async function setApiKey(paths) {
392
414
  try { authProfiles = JSON.parse(fs.readFileSync(paths.authProfiles, 'utf8')); } catch {}
393
415
  }
394
416
 
395
- Object.keys(config.models.providers).forEach(name => {
396
- if (name.startsWith('yunyi-')) {
397
- authProfiles[`${name}:default`] = { apiKey: apiKey.trim() };
398
- }
417
+ providers.forEach(providerName => {
418
+ authProfiles[`${providerName}:default`] = { apiKey: apiKey.trim() };
399
419
  });
400
420
 
401
421
  fs.writeFileSync(paths.authProfiles, JSON.stringify(authProfiles, null, 2), 'utf8');
@@ -414,30 +434,46 @@ async function viewConfig(paths) {
414
434
  return;
415
435
  }
416
436
 
417
- // 主模型
437
+ // 当前激活
418
438
  const primary = config.agents?.defaults?.model?.primary || '未设置';
419
- console.log(chalk.yellow('主模型:'));
420
- console.log(` ${primary}\n`);
421
-
422
- // 备用模型
423
- const fallbacks = config.agents?.defaults?.model?.fallbacks || [];
424
- console.log(chalk.yellow(`备用模型 (${fallbacks.length}):`));
425
- fallbacks.slice(0, 3).forEach((f, i) => console.log(` ${i + 1}. ${f}`));
426
- if (fallbacks.length > 3) console.log(chalk.gray(` ... 还有 ${fallbacks.length - 3} 个`));
439
+ const isClaudeActive = primary.startsWith('yunyi-claude');
440
+ const isCodexActive = primary.startsWith('yunyi-codex');
441
+
442
+ console.log(chalk.yellow('当前激活:'));
443
+ if (isClaudeActive) {
444
+ console.log(chalk.blue(` 🔵 Claude: ${primary}`));
445
+ } else if (isCodexActive) {
446
+ console.log(chalk.green(` 🟢 Codex: ${primary}`));
447
+ } else {
448
+ console.log(` ${primary}`);
449
+ }
427
450
  console.log('');
428
451
 
429
- // 节点列表
430
- console.log(chalk.yellow('节点列表:'));
431
- if (config.models?.providers) {
432
- const providers = Object.entries(config.models.providers).filter(([name]) => name.startsWith('yunyi-'));
433
- providers.slice(0, 5).forEach(([name, provider], i) => {
434
- const isPrimary = primary.startsWith(name);
435
- const marker = isPrimary ? chalk.green('★') : chalk.gray('○');
436
- const hasKey = provider.apiKey ? chalk.green('✓') : chalk.red('✗');
437
- console.log(` ${marker} ${name}: ${provider.baseUrl}`);
438
- console.log(` API Key: ${hasKey}`);
439
- });
440
- if (providers.length > 5) console.log(chalk.gray(` ... 还有 ${providers.length - 5} 个节点`));
452
+ // Claude 节点
453
+ console.log(chalk.yellow('Claude 节点:'));
454
+ const claudeProvider = config.models?.providers?.['yunyi-claude'];
455
+ if (claudeProvider) {
456
+ const hasKey = claudeProvider.apiKey ? chalk.green('✓') : chalk.red('✗');
457
+ const model = claudeProvider.models?.[0]?.name || 'N/A';
458
+ console.log(` URL: ${claudeProvider.baseUrl}`);
459
+ console.log(` 模型: ${model}`);
460
+ console.log(` API Key: ${hasKey}`);
461
+ } else {
462
+ console.log(chalk.gray(' 未配置'));
463
+ }
464
+ console.log('');
465
+
466
+ // Codex 节点
467
+ console.log(chalk.yellow('Codex 节点:'));
468
+ const codexProvider = config.models?.providers?.['yunyi-codex'];
469
+ if (codexProvider) {
470
+ const hasKey = codexProvider.apiKey ? chalk.green('✓') : chalk.red('✗');
471
+ const model = codexProvider.models?.[0]?.name || 'N/A';
472
+ console.log(` URL: ${codexProvider.baseUrl}`);
473
+ console.log(` 模型: ${model}`);
474
+ console.log(` API Key: ${hasKey}`);
475
+ } else {
476
+ console.log(chalk.gray(' 未配置'));
441
477
  }
442
478
  console.log('');
443
479
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclawapi",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "跨平台 OpenClaw/Clawdbot 配置管理工具 - 管理中转地址、模型切换、API Keys、测速优化",
5
5
  "main": "cli.js",
6
6
  "bin": {