@tokenaut/opentoken 1.4.9 → 1.4.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -146,6 +146,40 @@ opentoken cx -k <api_key> -u https://gw.opentoken.io/v1 -m <model_id>
146
146
 
147
147
  ---
148
148
 
149
+ ## 连接测试
150
+
151
+ 验证各工具的 API Key、Base URL、模型是否配置正确,发送最小请求检测连通性:
152
+
153
+ ```bash
154
+ opentoken cc test
155
+ opentoken cc test -m <model_id>
156
+ opentoken cdt test
157
+ opentoken ac test
158
+ opentoken hm test
159
+ opentoken oc test
160
+ opentoken od test
161
+ opentoken cx test
162
+ ```
163
+
164
+ ---
165
+
166
+ ## 查看可用模型
167
+
168
+ 查看默认节点所有可用模型(自动使用已配置的 API Key):
169
+
170
+ ```bash
171
+ opentoken models
172
+ ```
173
+
174
+ 使用指定 Key 查询:
175
+
176
+ ```bash
177
+ opentoken models -k <api_key>
178
+ opentoken models --key <api_key>
179
+ ```
180
+
181
+ ---
182
+
149
183
  ## 环境
150
184
 
151
185
  Node.js >= 14 · MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenaut/opentoken",
3
- "version": "1.4.9",
3
+ "version": "1.4.11",
4
4
  "description": "OpenToken 一键接入 AI 模型 · https://docs.opentoken.io/",
5
5
  "main": "src/cli.js",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -12,6 +12,8 @@ const openclaw = require('./tools/openclaw');
12
12
  const opencode = require('./tools/opencode');
13
13
  const codex = require('./tools/codex');
14
14
  const { DEFAULT_BASE_URL } = require('./models');
15
+ const { fetchModelsForBaseUrl } = require('./fetch-models');
16
+ const { getCurrentConfig } = require('./config');
15
17
 
16
18
  function isCcCommand(tool) {
17
19
  return tool === 'cc' || tool === 'claude';
@@ -51,6 +53,8 @@ function printHelp() {
51
53
  console.log(chalk.bold('常用:'));
52
54
  console.log(' opentoken help | --help | -h 显示本帮助');
53
55
  console.log(' opentoken version | --version | -v | -V 显示版本号');
56
+ console.log(' opentoken models 查看默认节点所有可用模型');
57
+ console.log(' opentoken models -k <apikey> 使用指定 Key 查看模型');
54
58
  console.log('');
55
59
  console.log(chalk.bold('用法:'));
56
60
  console.log(' opentoken 进入主菜单');
@@ -97,6 +101,16 @@ function printHelp() {
97
101
  console.log(' opentoken cdt --url <baseurl> 设置 Base URL(inferenceGatewayBaseUrl)');
98
102
  console.log(' opentoken cdt -k <k> -u <u> 一键设置 Key 与 URL');
99
103
  console.log('');
104
+ console.log(chalk.bold('连接测试:'));
105
+ console.log(' opentoken cc test 测试 Claude Code 所有模型连接');
106
+ console.log(' opentoken cc test -m <model> 测试 Claude Code 指定模型连接');
107
+ console.log(' opentoken cdt test 测试 Claude Desktop 连接');
108
+ console.log(' opentoken ac test 测试 Atomcode 连接');
109
+ console.log(' opentoken hm test 测试 Hermes 连接');
110
+ console.log(' opentoken oc test 测试 OpenClaw 连接');
111
+ console.log(' opentoken od test 测试 OpenCode 连接');
112
+ console.log(' opentoken cx test 测试 Codex 连接');
113
+ console.log('');
100
114
  console.log(chalk.bold('快捷别名(--key / -k,--url / -u,--model / -m)'));
101
115
  console.log('');
102
116
  console.log(chalk.bold('示例:'));
@@ -120,6 +134,7 @@ function parseArgs(argv) {
120
134
  const args = argv.slice(2);
121
135
  const result = {
122
136
  tool: null,
137
+ subcommand: null,
123
138
  key: null,
124
139
  url: null,
125
140
  model: null,
@@ -134,6 +149,11 @@ function parseArgs(argv) {
134
149
  i = 1;
135
150
  }
136
151
 
152
+ if (args[i] && !args[i].startsWith('-')) {
153
+ result.subcommand = args[i].toLowerCase();
154
+ i++;
155
+ }
156
+
137
157
  for (; i < args.length; i++) {
138
158
  const a = args[i];
139
159
  const next = args[i + 1];
@@ -228,6 +248,47 @@ async function run() {
228
248
  return;
229
249
  }
230
250
 
251
+ if (args.tool === 'models') {
252
+ const apiKey = args.key || getCurrentConfig().apiKey;
253
+ const maskKey = (k) => {
254
+ if (!k || k.length < 8) return k || '';
255
+ return k.slice(0, 6) + '****' + k.slice(-4);
256
+ };
257
+ console.log('');
258
+ console.log(chalk.bold(chalk.cyan('可用模型列表')) + chalk.gray('(' + DEFAULT_BASE_URL + ')'));
259
+ console.log('');
260
+ if (!apiKey) {
261
+ console.log(chalk.red(' ✗ 未配置 API Key,请使用 opentoken models -k <apikey> 或先配置 Key'));
262
+ console.log('');
263
+ return;
264
+ }
265
+ const result = await fetchModelsForBaseUrl(DEFAULT_BASE_URL, apiKey);
266
+ if (result.ok && result.models.length > 0) {
267
+ result.models.forEach((id) => console.log(' ' + id));
268
+ console.log('');
269
+ console.log(chalk.gray('共 ' + result.models.length + ' 个模型'));
270
+ } else {
271
+ console.log(chalk.red(' ✗ 获取失败:' + (result.error || '未知错误')));
272
+ }
273
+ console.log(chalk.gray('API Key : ' + maskKey(apiKey)));
274
+ console.log('');
275
+ return;
276
+ }
277
+
278
+ if (args.subcommand === 'test') {
279
+ clearScreen();
280
+ printLogo();
281
+ if (isCcCommand(args.tool)) { await claude.test({ model: args.model }); return; }
282
+ if (isClaudeDesktopCommand(args.tool)) { await claudeDesktop.test(); return; }
283
+ if (isAtomcodeCommand(args.tool)) { await atomcode.test(); return; }
284
+ if (isHermesCommand(args.tool)) { await hermes.test(); return; }
285
+ if (isOpenclawCommand(args.tool)) { await openclaw.test(); return; }
286
+ if (isOpencodeCommand(args.tool)) { await opencode.test(); return; }
287
+ if (isCodexCommand(args.tool)) { await codex.test(); return; }
288
+ console.log(chalk.red('未知工具:' + args.tool));
289
+ return;
290
+ }
291
+
231
292
  if (isCcCommand(args.tool) && (args.key || args.url || args.model)) {
232
293
  clearScreen();
233
294
  printLogo();
package/src/config.js CHANGED
@@ -91,6 +91,7 @@ function applyConfig({ apiKey, baseUrl, model }) {
91
91
 
92
92
  module.exports = {
93
93
  SETTINGS_PATH,
94
+ readClaudeSettings,
94
95
  getCurrentConfig,
95
96
  applyConfig,
96
97
  };
@@ -0,0 +1,164 @@
1
+ 'use strict';
2
+
3
+ const https = require('https');
4
+ const http = require('http');
5
+ const { URL } = require('url');
6
+
7
+ const TIMEOUT_MS = 30000;
8
+
9
+ function request(method, urlStr, headers, body) {
10
+ return new Promise((resolve, reject) => {
11
+ const parsed = new URL(urlStr);
12
+ const mod = parsed.protocol === 'https:' ? https : http;
13
+ const opts = {
14
+ method,
15
+ hostname: parsed.hostname,
16
+ port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
17
+ path: parsed.pathname + parsed.search,
18
+ headers: {
19
+ ...headers,
20
+ 'Content-Type': 'application/json',
21
+ },
22
+ timeout: TIMEOUT_MS,
23
+ };
24
+
25
+ const req = mod.request(opts, (res) => {
26
+ let data = '';
27
+ res.on('data', (chunk) => { data += chunk; });
28
+ res.on('end', () => {
29
+ resolve({ statusCode: res.statusCode, body: data });
30
+ });
31
+ });
32
+
33
+ req.on('error', (err) => {
34
+ reject(err);
35
+ });
36
+
37
+ req.on('timeout', () => {
38
+ req.destroy();
39
+ reject(new Error('请求超时(' + TIMEOUT_MS / 1000 + '秒)'));
40
+ });
41
+
42
+ if (body) {
43
+ req.write(JSON.stringify(body));
44
+ }
45
+ req.end();
46
+ });
47
+ }
48
+
49
+ async function testAnthropic({ baseUrl, apiKey, model }) {
50
+ const start = Date.now();
51
+ try {
52
+ const url = baseUrl.replace(/\/+$/, '') + '/v1/messages';
53
+ const headers = {
54
+ 'x-api-key': apiKey,
55
+ 'anthropic-version': '2023-06-01',
56
+ };
57
+ const body = {
58
+ model,
59
+ max_tokens: 1,
60
+ messages: [{ role: 'user', content: 'hi' }],
61
+ };
62
+ const res = await request('POST', url, headers, body);
63
+ const latency = Date.now() - start;
64
+
65
+ if (res.statusCode >= 200 && res.statusCode < 300) {
66
+ return { ok: true, latency_ms: latency };
67
+ }
68
+
69
+ let errMsg = 'HTTP ' + res.statusCode;
70
+ try {
71
+ const parsed = JSON.parse(res.body);
72
+ if (parsed.error && parsed.error.message) {
73
+ errMsg += ' — ' + parsed.error.message;
74
+ }
75
+ } catch (_) {}
76
+ return { ok: false, error: errMsg };
77
+ } catch (err) {
78
+ return { ok: false, error: err.message };
79
+ }
80
+ }
81
+
82
+ async function testOpenAI({ baseUrl, apiKey, model }) {
83
+ const start = Date.now();
84
+ try {
85
+ const url = baseUrl.replace(/\/+$/, '') + '/chat/completions';
86
+ const headers = {
87
+ 'Authorization': 'Bearer ' + apiKey,
88
+ };
89
+ const body = {
90
+ model,
91
+ max_tokens: 1,
92
+ messages: [{ role: 'user', content: 'hi' }],
93
+ };
94
+ const res = await request('POST', url, headers, body);
95
+ const latency = Date.now() - start;
96
+
97
+ if (res.statusCode >= 200 && res.statusCode < 300) {
98
+ return { ok: true, latency_ms: latency };
99
+ }
100
+
101
+ let errMsg = 'HTTP ' + res.statusCode;
102
+ try {
103
+ const parsed = JSON.parse(res.body);
104
+ if (parsed.error && parsed.error.message) {
105
+ errMsg += ' — ' + parsed.error.message;
106
+ }
107
+ } catch (_) {}
108
+ return { ok: false, error: errMsg };
109
+ } catch (err) {
110
+ return { ok: false, error: err.message };
111
+ }
112
+ }
113
+
114
+ async function testOpenAIResponses({ baseUrl, apiKey, model }) {
115
+ const start = Date.now();
116
+ try {
117
+ const url = baseUrl.replace(/\/+$/, '') + '/responses';
118
+ const headers = {
119
+ 'Authorization': 'Bearer ' + apiKey,
120
+ };
121
+ const body = {
122
+ model,
123
+ input: 'hi',
124
+ max_output_tokens: 1,
125
+ };
126
+ const res = await request('POST', url, headers, body);
127
+ const latency = Date.now() - start;
128
+
129
+ if (res.statusCode >= 200 && res.statusCode < 300) {
130
+ return { ok: true, latency_ms: latency };
131
+ }
132
+
133
+ let errMsg = 'HTTP ' + res.statusCode;
134
+ try {
135
+ const parsed = JSON.parse(res.body);
136
+ if (parsed.error && parsed.error.message) {
137
+ errMsg += ' — ' + parsed.error.message;
138
+ }
139
+ } catch (_) {}
140
+ return { ok: false, error: errMsg };
141
+ } catch (err) {
142
+ return { ok: false, error: err.message };
143
+ }
144
+ }
145
+
146
+ const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
147
+
148
+ async function testWithSpinner({ label, testFn, args }) {
149
+ let frameIdx = 0;
150
+ const spinnerTimer = setInterval(() => {
151
+ const frame = SPINNER_FRAMES[frameIdx % SPINNER_FRAMES.length];
152
+ process.stdout.write('\x1b[1A\x1b[K');
153
+ process.stdout.write('\x1b[36m ' + frame + ' ' + label + ' 测试中...\x1b[0m\n');
154
+ frameIdx++;
155
+ }, 80);
156
+
157
+ const result = await testFn(args);
158
+ clearInterval(spinnerTimer);
159
+
160
+ process.stdout.write('\x1b[1A\x1b[K');
161
+ return result;
162
+ }
163
+
164
+ module.exports = { testAnthropic, testOpenAI, testOpenAIResponses, testWithSpinner };
@@ -11,6 +11,7 @@ const {
11
11
  } = require('../atomcode-config');
12
12
  const { printLogo } = require('../logo');
13
13
  const { clearScreen } = require('../screen');
14
+ const { testOpenAI, testWithSpinner } = require('../test-connection');
14
15
 
15
16
  let pendingConfig = { apiKey: null, model: null };
16
17
 
@@ -252,4 +253,38 @@ function quickSet({ key, model }) {
252
253
  return true;
253
254
  }
254
255
 
255
- module.exports = { run, quickSet };
256
+ async function test() {
257
+ const config = getCurrentAtomcodeConfig();
258
+
259
+ if (!config.apiKey) {
260
+ console.log(chalk.red(' ✗ 请先配置 API Key'));
261
+ return;
262
+ }
263
+ if (!config.model) {
264
+ console.log(chalk.red(' ✗ 请先配置模型'));
265
+ return;
266
+ }
267
+
268
+ console.log('');
269
+ console.log(chalk.bold(chalk.cyan('正在测试 Atomcode 连接...')));
270
+ console.log(chalk.gray(' Base URL : ') + ATOMCODE_OPENTOKEN_BASE_URL);
271
+ console.log(chalk.gray(' 模型 : ') + config.model);
272
+ console.log(chalk.gray(' API Key : ') + maskApiKey(config.apiKey));
273
+ console.log('');
274
+ console.log(chalk.gray(' ' + config.model + ' 等待测试'));
275
+
276
+ const result = await testWithSpinner({
277
+ label: config.model,
278
+ testFn: testOpenAI,
279
+ args: { baseUrl: ATOMCODE_OPENTOKEN_BASE_URL, apiKey: config.apiKey, model: config.model },
280
+ });
281
+
282
+ if (result.ok) {
283
+ console.log(chalk.green(' ✓ ' + config.model + ' 连接成功 ' + result.latency_ms + 'ms'));
284
+ } else {
285
+ console.log(chalk.red(' ✗ ' + config.model + ' ' + result.error));
286
+ }
287
+ console.log('');
288
+ }
289
+
290
+ module.exports = { run, quickSet, test };
@@ -4,9 +4,10 @@ const chalk = require('chalk');
4
4
  const inquirer = require('inquirer');
5
5
  const { API_NODES, DEFAULT_BASE_URL, CLAUDE_DEFAULT_MODEL } = require('../models');
6
6
  const { fetchModelsForBaseUrl } = require('../fetch-models');
7
- const { getCurrentConfig, applyConfig } = require('../config');
7
+ const { getCurrentConfig, applyConfig, readClaudeSettings } = require('../config');
8
8
  const { printLogo } = require('../logo');
9
9
  const { clearScreen } = require('../screen');
10
+ const { testAnthropic, testWithSpinner } = require('../test-connection');
10
11
 
11
12
  let pendingConfig = { apiKey: null, baseUrl: null, model: null };
12
13
 
@@ -294,12 +295,129 @@ function quickSet({ key, url, model }) {
294
295
 
295
296
  console.log('');
296
297
  console.log(chalk.bold(chalk.cyan('Claude Code 配置已更新:')));
297
- if (key) console.log(chalk.green(' ✓ API Key : ') + maskApiKey(key));
298
- console.log(chalk.green(' ✓ Base URL : ') + (toWrite.baseUrl || url));
299
- console.log(chalk.green(' ✓ 模型 : ') + (toWrite.model || model));
298
+ if (toWrite.apiKey) console.log(chalk.green(' ✓ API Key : ') + maskApiKey(toWrite.apiKey));
299
+ if (toWrite.baseUrl) console.log(chalk.green(' ✓ Base URL : ') + toWrite.baseUrl);
300
+ if (toWrite.model) console.log(chalk.green(' ✓ 模型 : ') + toWrite.model);
300
301
  console.log('');
301
302
 
302
303
  return true;
303
304
  }
304
305
 
305
- module.exports = { run, quickSet };
306
+ async function test({ model } = {}) {
307
+ const config = getCurrentConfig();
308
+
309
+ if (!config.apiKey) {
310
+ console.log(chalk.red(' ✗ 请先配置 API Key'));
311
+ return;
312
+ }
313
+ if (!config.baseUrl) {
314
+ console.log(chalk.red(' ✗ 请先配置 Base URL'));
315
+ return;
316
+ }
317
+
318
+ if (model) {
319
+ console.log('');
320
+ console.log(chalk.bold(chalk.cyan('正在测试 Claude Code 连接...')));
321
+ console.log(chalk.gray(' Base URL : ') + config.baseUrl);
322
+ console.log(chalk.gray(' API Key : ') + maskApiKey(config.apiKey));
323
+ console.log('');
324
+ console.log(chalk.gray(' ' + model + ' 等待测试'));
325
+
326
+ const result = await testWithSpinner({
327
+ label: model,
328
+ testFn: testAnthropic,
329
+ args: { baseUrl: config.baseUrl, apiKey: config.apiKey, model },
330
+ });
331
+
332
+ if (result.ok) {
333
+ console.log(chalk.green(' ✓ ' + model + ' 连接成功 ' + result.latency_ms + 'ms'));
334
+ } else {
335
+ console.log(chalk.red(' ✗ ' + model + ' ' + result.error));
336
+ }
337
+ console.log('');
338
+ return;
339
+ }
340
+
341
+ const settings = readClaudeSettings();
342
+ const env = settings.env || {};
343
+
344
+ const modelEntries = [
345
+ { label: 'Haiku', id: env.ANTHROPIC_DEFAULT_HAIKU_MODEL || 'claude-haiku-4-5' },
346
+ { label: 'Sonnet', id: env.ANTHROPIC_DEFAULT_SONNET_MODEL || 'claude-sonnet-4-6' },
347
+ { label: 'Opus', id: env.ANTHROPIC_DEFAULT_OPUS_MODEL || 'claude-opus-4-7' },
348
+ { label: 'Fable', id: env.ANTHROPIC_DEFAULT_FABLE_MODEL || 'claude-fable-5' },
349
+ ];
350
+
351
+ if (config.model && !modelEntries.some((e) => e.id === config.model)) {
352
+ modelEntries.push({ label: '主模型', id: config.model });
353
+ }
354
+
355
+ const seen = new Set();
356
+ const unique = [];
357
+ for (const e of modelEntries) {
358
+ if (!seen.has(e.id)) {
359
+ seen.add(e.id);
360
+ unique.push(e);
361
+ }
362
+ }
363
+
364
+ const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
365
+ const results = new Array(unique.length).fill(null);
366
+ let testingIdx = -1;
367
+ let frameIdx = 0;
368
+
369
+ function renderLines() {
370
+ const lines = unique.map((entry, i) => {
371
+ if (results[i]) {
372
+ return results[i].ok
373
+ ? chalk.green(' ✓ ' + entry.label + ' (' + entry.id + ') 连接成功 ' + results[i].latency_ms + 'ms')
374
+ : chalk.red(' ✗ ' + entry.label + ' (' + entry.id + ') ' + results[i].error);
375
+ }
376
+ if (i === testingIdx) {
377
+ const frame = SPINNER_FRAMES[frameIdx % SPINNER_FRAMES.length];
378
+ return chalk.cyan(' ' + frame + ' ' + entry.label + ' (' + entry.id + ') 测试中...');
379
+ }
380
+ return chalk.gray(' ' + entry.label + ' (' + entry.id + ') 等待测试');
381
+ });
382
+ process.stdout.write('\x1b[' + unique.length + 'A\x1b[J');
383
+ process.stdout.write(lines.join('\n') + '\n');
384
+ }
385
+
386
+ console.log('');
387
+ console.log(chalk.bold(chalk.cyan('正在测试 Claude Code 连接...')));
388
+ console.log(chalk.gray(' Base URL : ') + config.baseUrl);
389
+ console.log(chalk.gray(' API Key : ') + maskApiKey(config.apiKey));
390
+ console.log('');
391
+
392
+ for (const entry of unique) {
393
+ console.log(chalk.gray(' ' + entry.label + ' (' + entry.id + ') 等待测试'));
394
+ }
395
+
396
+ for (let i = 0; i < unique.length; i++) {
397
+ testingIdx = i;
398
+ frameIdx = 0;
399
+
400
+ const spinnerTimer = setInterval(() => {
401
+ frameIdx++;
402
+ renderLines();
403
+ }, 80);
404
+
405
+ const result = await testAnthropic({
406
+ baseUrl: config.baseUrl,
407
+ apiKey: config.apiKey,
408
+ model: unique[i].id,
409
+ });
410
+
411
+ clearInterval(spinnerTimer);
412
+ results[i] = result;
413
+ testingIdx = -1;
414
+ renderLines();
415
+ }
416
+
417
+ const okCount = results.filter((r) => r && r.ok).length;
418
+ console.log('');
419
+ console.log(chalk.gray(' ' + okCount + '/' + unique.length + ' 连接成功'));
420
+ console.log('');
421
+ }
422
+
423
+ module.exports = { run, quickSet, test };
@@ -6,6 +6,7 @@ const { API_NODES, DEFAULT_BASE_URL } = require('../models');
6
6
  const { getCurrentConfig, applyConfig } = require('../claude-desktop-config');
7
7
  const { printLogo } = require('../logo');
8
8
  const { clearScreen } = require('../screen');
9
+ const { testAnthropic, testWithSpinner } = require('../test-connection');
9
10
 
10
11
  let pendingConfig = { apiKey: null, baseUrl: null };
11
12
 
@@ -177,11 +178,46 @@ function quickSet({ key, url }) {
177
178
 
178
179
  console.log('');
179
180
  console.log(chalk.bold(chalk.cyan('Claude Desktop 配置已更新:')));
180
- if (key) console.log(chalk.green(' ✓ API Key : ') + maskApiKey(key));
181
- console.log(chalk.green(' ✓ Base URL : ') + (toWrite.baseUrl || url));
181
+ if (toWrite.apiKey) console.log(chalk.green(' ✓ API Key : ') + maskApiKey(toWrite.apiKey));
182
+ if (toWrite.baseUrl) console.log(chalk.green(' ✓ Base URL : ') + toWrite.baseUrl);
182
183
  console.log('');
183
184
 
184
185
  return true;
185
186
  }
186
187
 
187
- module.exports = { run, quickSet };
188
+ async function test() {
189
+ const config = getCurrentConfig();
190
+
191
+ if (!config.apiKey) {
192
+ console.log(chalk.red(' ✗ 请先配置 API Key'));
193
+ return;
194
+ }
195
+ if (!config.baseUrl) {
196
+ console.log(chalk.red(' ✗ 请先配置 Base URL'));
197
+ return;
198
+ }
199
+
200
+ const model = 'claude-sonnet-4-6';
201
+
202
+ console.log('');
203
+ console.log(chalk.bold(chalk.cyan('正在测试 Claude Desktop 连接...')));
204
+ console.log(chalk.gray(' Base URL : ') + config.baseUrl);
205
+ console.log(chalk.gray(' API Key : ') + maskApiKey(config.apiKey));
206
+ console.log('');
207
+ console.log(chalk.gray(' ' + model + ' 等待测试'));
208
+
209
+ const result = await testWithSpinner({
210
+ label: model,
211
+ testFn: testAnthropic,
212
+ args: { baseUrl: config.baseUrl, apiKey: config.apiKey, model },
213
+ });
214
+
215
+ if (result.ok) {
216
+ console.log(chalk.green(' ✓ ' + model + ' 连接成功 ' + result.latency_ms + 'ms'));
217
+ } else {
218
+ console.log(chalk.red(' ✗ ' + model + ' ' + result.error));
219
+ }
220
+ console.log('');
221
+ }
222
+
223
+ module.exports = { run, quickSet, test };
@@ -12,6 +12,7 @@ const {
12
12
  } = require('../codex-config');
13
13
  const { printLogo } = require('../logo');
14
14
  const { clearScreen } = require('../screen');
15
+ const { testOpenAIResponses, testWithSpinner } = require('../test-connection');
15
16
 
16
17
  let pendingConfig = { apiKey: null, baseUrl: null, model: null, models: null };
17
18
 
@@ -312,9 +313,9 @@ async function quickSet({ key, url, model }) {
312
313
 
313
314
  console.log('');
314
315
  console.log(chalk.bold(chalk.cyan('Codex 配置已更新:')));
315
- if (key) console.log(chalk.green(' ✓ API Key : ') + maskApiKey(key));
316
- console.log(chalk.green(' ✓ Base URL : ') + (toWrite.baseUrl || url));
317
- console.log(chalk.green(' ✓ 模型 : ') + toWrite.model);
316
+ if (toWrite.apiKey) console.log(chalk.green(' ✓ API Key : ') + maskApiKey(toWrite.apiKey));
317
+ if (toWrite.baseUrl) console.log(chalk.green(' ✓ Base URL : ') + toWrite.baseUrl);
318
+ if (toWrite.model) console.log(chalk.green(' ✓ 模型 : ') + toWrite.model);
318
319
  if (toWrite.models) {
319
320
  console.log(chalk.gray(' (包含 ' + toWrite.models.length + ' 个 responses 模型)'));
320
321
  }
@@ -326,4 +327,42 @@ async function quickSet({ key, url, model }) {
326
327
  return true;
327
328
  }
328
329
 
329
- module.exports = { run, quickSet };
330
+ async function test() {
331
+ const config = getCurrentCodexConfig();
332
+
333
+ if (!config.apiKey) {
334
+ console.log(chalk.red(' ✗ 请先配置 API Key'));
335
+ return;
336
+ }
337
+ if (!config.baseUrl) {
338
+ console.log(chalk.red(' ✗ 请先配置 Base URL'));
339
+ return;
340
+ }
341
+ if (!config.model) {
342
+ console.log(chalk.red(' ✗ 请先配置模型'));
343
+ return;
344
+ }
345
+
346
+ console.log('');
347
+ console.log(chalk.bold(chalk.cyan('正在测试 Codex 连接...')));
348
+ console.log(chalk.gray(' Base URL : ') + config.baseUrl);
349
+ console.log(chalk.gray(' 模型 : ') + config.model);
350
+ console.log(chalk.gray(' API Key : ') + maskApiKey(config.apiKey));
351
+ console.log('');
352
+ console.log(chalk.gray(' ' + config.model + ' 等待测试'));
353
+
354
+ const result = await testWithSpinner({
355
+ label: config.model,
356
+ testFn: testOpenAIResponses,
357
+ args: { baseUrl: config.baseUrl, apiKey: config.apiKey, model: config.model },
358
+ });
359
+
360
+ if (result.ok) {
361
+ console.log(chalk.green(' ✓ ' + config.model + ' 连接成功 ' + result.latency_ms + 'ms'));
362
+ } else {
363
+ console.log(chalk.red(' ✗ ' + config.model + ' ' + result.error));
364
+ }
365
+ console.log('');
366
+ }
367
+
368
+ module.exports = { run, quickSet, test };
@@ -12,6 +12,7 @@ const {
12
12
  } = require('../hermes-config');
13
13
  const { printLogo } = require('../logo');
14
14
  const { clearScreen } = require('../screen');
15
+ const { testOpenAI, testWithSpinner } = require('../test-connection');
15
16
 
16
17
  let pendingConfig = { apiKey: null, model: null };
17
18
 
@@ -273,4 +274,38 @@ function quickSet({ key, model }) {
273
274
  return true;
274
275
  }
275
276
 
276
- module.exports = { run, quickSet };
277
+ async function test() {
278
+ const config = getCurrentHermesConfig();
279
+
280
+ if (!config.apiKey) {
281
+ console.log(chalk.red(' ✗ 请先配置 API Key'));
282
+ return;
283
+ }
284
+ if (!config.model) {
285
+ console.log(chalk.red(' ✗ 请先配置模型'));
286
+ return;
287
+ }
288
+
289
+ console.log('');
290
+ console.log(chalk.bold(chalk.cyan('正在测试 Hermes 连接...')));
291
+ console.log(chalk.gray(' Base URL : ') + HERMES_OPENTOKEN_BASE_URL);
292
+ console.log(chalk.gray(' 模型 : ') + config.model);
293
+ console.log(chalk.gray(' API Key : ') + maskApiKey(config.apiKey));
294
+ console.log('');
295
+ console.log(chalk.gray(' ' + config.model + ' 等待测试'));
296
+
297
+ const result = await testWithSpinner({
298
+ label: config.model,
299
+ testFn: testOpenAI,
300
+ args: { baseUrl: HERMES_OPENTOKEN_BASE_URL, apiKey: config.apiKey, model: config.model },
301
+ });
302
+
303
+ if (result.ok) {
304
+ console.log(chalk.green(' ✓ ' + config.model + ' 连接成功 ' + result.latency_ms + 'ms'));
305
+ } else {
306
+ console.log(chalk.red(' ✗ ' + config.model + ' ' + result.error));
307
+ }
308
+ console.log('');
309
+ }
310
+
311
+ module.exports = { run, quickSet, test };
@@ -2,7 +2,7 @@
2
2
 
3
3
  const chalk = require('chalk');
4
4
  const inquirer = require('inquirer');
5
- const { DEFAULT_BASE_URL, OPENCLAW_PROVIDER_KEY, GENERAL_DEFAULT_MODEL } = require('../models');
5
+ const { DEFAULT_BASE_URL, OPENCLAW_PROVIDER_KEY, OPENCLAW_OPENTOKEN_BASE_URL, GENERAL_DEFAULT_MODEL } = require('../models');
6
6
  const { fetchModelsForBaseUrl } = require('../fetch-models');
7
7
  const {
8
8
  OPENCLAW_JSON_PATH,
@@ -11,6 +11,7 @@ const {
11
11
  } = require('../openclaw-config');
12
12
  const { printLogo } = require('../logo');
13
13
  const { clearScreen } = require('../screen');
14
+ const { testOpenAI, testWithSpinner } = require('../test-connection');
14
15
 
15
16
  let pendingConfig = { apiKey: null, model: null };
16
17
 
@@ -241,4 +242,40 @@ function quickSet({ key, model }) {
241
242
  return true;
242
243
  }
243
244
 
244
- module.exports = { run, quickSet };
245
+ async function test() {
246
+ const config = getCurrentOpenclawOpentokenConfig();
247
+
248
+ if (!config.apiKey) {
249
+ console.log(chalk.red(' ✗ 请先配置 API Key'));
250
+ return;
251
+ }
252
+ if (!config.model) {
253
+ console.log(chalk.red(' ✗ 请先配置模型'));
254
+ return;
255
+ }
256
+
257
+ const modelLabel = OPENCLAW_PROVIDER_KEY + '/' + config.model;
258
+
259
+ console.log('');
260
+ console.log(chalk.bold(chalk.cyan('正在测试 OpenClaw 连接...')));
261
+ console.log(chalk.gray(' Base URL : ') + OPENCLAW_OPENTOKEN_BASE_URL);
262
+ console.log(chalk.gray(' 模型 : ') + modelLabel);
263
+ console.log(chalk.gray(' API Key : ') + maskApiKey(config.apiKey));
264
+ console.log('');
265
+ console.log(chalk.gray(' ' + modelLabel + ' 等待测试'));
266
+
267
+ const result = await testWithSpinner({
268
+ label: modelLabel,
269
+ testFn: testOpenAI,
270
+ args: { baseUrl: OPENCLAW_OPENTOKEN_BASE_URL, apiKey: config.apiKey, model: config.model },
271
+ });
272
+
273
+ if (result.ok) {
274
+ console.log(chalk.green(' ✓ ' + modelLabel + ' 连接成功 ' + result.latency_ms + 'ms'));
275
+ } else {
276
+ console.log(chalk.red(' ✗ ' + modelLabel + ' ' + result.error));
277
+ }
278
+ console.log('');
279
+ }
280
+
281
+ module.exports = { run, quickSet, test };
@@ -2,7 +2,7 @@
2
2
 
3
3
  const chalk = require('chalk');
4
4
  const inquirer = require('inquirer');
5
- const { DEFAULT_BASE_URL, OPENCODE_PROVIDER_KEY, GENERAL_DEFAULT_MODEL } = require('../models');
5
+ const { DEFAULT_BASE_URL, OPENCODE_PROVIDER_KEY, OPENCODE_OPENTOKEN_BASE_URL, GENERAL_DEFAULT_MODEL } = require('../models');
6
6
  const { fetchModelsForBaseUrl } = require('../fetch-models');
7
7
  const {
8
8
  OPENCODE_JSON_PATH,
@@ -11,6 +11,7 @@ const {
11
11
  } = require('../opencode-config');
12
12
  const { printLogo } = require('../logo');
13
13
  const { clearScreen } = require('../screen');
14
+ const { testOpenAI, testWithSpinner } = require('../test-connection');
14
15
 
15
16
  let pendingConfig = { apiKey: null, model: null };
16
17
 
@@ -255,4 +256,38 @@ async function quickSet({ key, model }) {
255
256
  return true;
256
257
  }
257
258
 
258
- module.exports = { run, quickSet };
259
+ async function test() {
260
+ const config = getCurrentOpencodeOpentokenConfig();
261
+
262
+ if (!config.apiKey) {
263
+ console.log(chalk.red(' ✗ 请先配置 API Key'));
264
+ return;
265
+ }
266
+ if (!config.model) {
267
+ console.log(chalk.red(' ✗ 请先配置模型'));
268
+ return;
269
+ }
270
+
271
+ console.log('');
272
+ console.log(chalk.bold(chalk.cyan('正在测试 OpenCode 连接...')));
273
+ console.log(chalk.gray(' Base URL : ') + OPENCODE_OPENTOKEN_BASE_URL);
274
+ console.log(chalk.gray(' 模型 : ') + config.model);
275
+ console.log(chalk.gray(' API Key : ') + maskApiKey(config.apiKey));
276
+ console.log('');
277
+ console.log(chalk.gray(' ' + config.model + ' 等待测试'));
278
+
279
+ const result = await testWithSpinner({
280
+ label: config.model,
281
+ testFn: testOpenAI,
282
+ args: { baseUrl: OPENCODE_OPENTOKEN_BASE_URL, apiKey: config.apiKey, model: config.model },
283
+ });
284
+
285
+ if (result.ok) {
286
+ console.log(chalk.green(' ✓ ' + config.model + ' 连接成功 ' + result.latency_ms + 'ms'));
287
+ } else {
288
+ console.log(chalk.red(' ✗ ' + config.model + ' ' + result.error));
289
+ }
290
+ console.log('');
291
+ }
292
+
293
+ module.exports = { run, quickSet, test };