bingocode 1.1.63 → 1.1.65

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bingocode",
3
- "version": "1.1.63",
3
+ "version": "1.1.65",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "claude": "bin/claude-win.cjs",
@@ -55,6 +55,7 @@
55
55
  "chokidar": "^5.0.0",
56
56
  "cli-boxes": "^4.0.1",
57
57
  "code-excerpt": "^4.0.0",
58
+ "commander": "^14.0.3",
58
59
  "diff": "^8.0.4",
59
60
  "emoji-regex": "^10.6.0",
60
61
  "env-paths": "^4.0.0",
@@ -285,7 +285,6 @@ export const CliMenuManager: React.FC = () => {
285
285
  let mounted = true;
286
286
  (async () => {
287
287
  if (apiUrl) return;
288
-
289
288
  const entry = path.resolve(import.meta.dir, '../server/index.ts');
290
289
  const MAX_RETRIES = 3;
291
290
  const RETRY_DELAYS = [0, 2000, 5000]; // 首次无延迟,第2次2秒,第3次5秒
@@ -99,10 +99,11 @@ presets:
99
99
  - id: deepseek
100
100
  name: DeepSeek
101
101
  baseUrl: https://api.deepseek.com/anthropic
102
- apiFormat: openai_chat
102
+ apiFormat: anthropic
103
103
  needsApiKey: true
104
104
  websiteUrl: https://platform.deepseek.com
105
- modelsUrl: /v1/models
105
+ # 模型列表走 OpenAI 端(/anthropic 路径下无 /v1/models
106
+ modelsUrl: https://api.deepseek.com/v1/models
106
107
  modelsAuthStyle: bearer
107
108
  modelsDataPath: data
108
109
  fields:
@@ -123,7 +124,7 @@ presets:
123
124
  apiFormat: openai_chat
124
125
  needsApiKey: true
125
126
  websiteUrl: https://open.bigmodel.cn
126
- modelsUrl: /models
127
+ modelsUrl: /v4/models
127
128
  modelsAuthStyle: bearer
128
129
  modelsDataPath: data
129
130
  fields:
@@ -136,11 +137,11 @@ presets:
136
137
  label: API Key
137
138
  required: true
138
139
  secret: true
139
- placeholder: '智谱 API Key'
140
+ placeholder: '智谱 API Key (glm-5.1)'
140
141
 
141
142
  - id: kimi
142
143
  name: Kimi
143
- baseUrl: https://api.moonshot.cn/v1
144
+ baseUrl: https://api.moonshot.ai/v1
144
145
  apiFormat: openai_chat
145
146
  needsApiKey: true
146
147
  websiteUrl: https://platform.moonshot.cn
@@ -157,11 +158,11 @@ presets:
157
158
  label: API Key
158
159
  required: true
159
160
  secret: true
160
- placeholder: 'Moonshot API Key'
161
+ placeholder: 'Moonshot API Key (kimi-k2.6)'
161
162
 
162
163
  - id: minimax
163
164
  name: MiniMax
164
- baseUrl: https://api.minimaxi.com/v1
165
+ baseUrl: https://api.minimax.io/v1
165
166
  apiFormat: openai_chat
166
167
  needsApiKey: true
167
168
  websiteUrl: https://platform.minimaxi.com
@@ -178,7 +179,7 @@ presets:
178
179
  label: API Key
179
180
  required: true
180
181
  secret: true
181
- placeholder: 'MiniMax API Key'
182
+ placeholder: 'MiniMax API Key (MiniMax-M2.7)'
182
183
 
183
184
  - id: custom
184
185
  name: Custom
@@ -1,256 +1,160 @@
1
- // server/ensureSingletonLocalServer.ts
2
- import { spawn, spawnSync } from 'child_process';
3
- import fs from 'fs';
4
- import fsp from 'fs/promises';
5
- import path from 'path';
6
- import os from 'os';
7
- import axios from 'axios';
8
-
9
- const LOG_FILE = path.join(os.homedir(), '.claude-cli', 'runtime', 'boot.log');
10
- function log(msg: string) {
11
- try {
12
- const line = `[${new Date().toISOString()}] ${msg}\n`;
13
- fs.mkdirSync(path.dirname(LOG_FILE), { recursive: true });
14
- fs.appendFileSync(LOG_FILE, line);
15
- } catch {}
16
- }
17
-
18
- type Handle = {
19
- baseUrl: string;
20
- stopIfLast: () => Promise<void>;
21
- pid?: number;
22
- };
23
-
24
- const RUNTIME_DIR = path.join(os.homedir(), '.claude-cli', 'runtime');
25
- const LOCK_JSON = path.join(RUNTIME_DIR, 'server.lock.json');
26
- const BOOT_LOCK = path.join(RUNTIME_DIR, 'server.boot.lock');
27
- const LEASES_DIR = path.join(RUNTIME_DIR, 'leases');
28
-
29
- const DEFAULT_HOST = '127.0.0.1';
30
- const DEFAULT_PORT = Number(process.env.SERVER_PORT || 3456);
31
- const HEALTH_TIMEOUT_MS = Number(process.env.HEALTH_TIMEOUT_MS || 20000);
32
- const HEALTH_RETRY_MS = 300;
33
-
34
- function mkdirp(p: string) { fs.mkdirSync(p, { recursive: true }); }
35
- function atomicCreate(p: string): boolean {
36
- try { const fd = fs.openSync(p, 'wx'); fs.closeSync(fd); return true; } catch { return false; }
37
- }
38
- function rmSafe(p: string) { try { fs.rmSync(p, { force: true, recursive: true }); } catch {} }
39
- function readJson<T>(p: string): T | null { try { return JSON.parse(fs.readFileSync(p, 'utf-8')) as T; } catch { return null; } }
40
- function writeJson(p: string, data: any) { fs.writeFileSync(p, JSON.stringify(data, null, 2), 'utf-8'); }
41
- function isPidAlive(pid: number): boolean { try { process.kill(pid, 0); return true; } catch { return false; } }
42
- async function waitHealthy(baseUrl: string, timeoutMs: number) {
43
- const start = Date.now(); let lastErr: any = null;
44
- while (Date.now() - start < timeoutMs) {
45
- try {
46
- const r = await axios.get(baseUrl.replace(/\/+$/, '') + '/health', { timeout: 1500 });
47
- if (r.status === 200 && r.data?.status === 'ok') return;
48
- } catch (e) { lastErr = e; }
49
- await new Promise(r => setTimeout(r, HEALTH_RETRY_MS));
50
- }
51
- throw new Error(`健康检查超时:${lastErr?.message || 'unknown'}`);
52
- }
53
- function resolveBunPath() {
54
- const fromEnv = process.env.BUN_PATH;
55
- if (fromEnv) return fromEnv;
56
- const r = spawnSync('bun', ['--version'], { stdio: 'ignore' });
57
- if (r.status === 0) return 'bun';
58
- throw new Error('未检测到 bun,请安装 https://bun.sh 或设置 BUN_PATH');
59
- }
60
-
61
- async function acquireLease(): Promise<string> {
62
- mkdirp(LEASES_DIR);
63
- const lease = path.join(LEASES_DIR, `${process.pid}.json`);
64
- await fsp.writeFile(lease, JSON.stringify({ pid: process.pid, ts: Date.now() }));
65
- const cleanup = () => rmSafe(lease);
66
- process.once('exit', cleanup);
67
- process.once('SIGINT', () => { cleanup(); process.exit(130); });
68
- process.once('SIGTERM', () => { cleanup(); process.exit(143); });
69
- return lease;
70
- }
71
- function countValidLeases(): number {
72
- try {
73
- const files = fs.readdirSync(LEASES_DIR);
74
- let alive = 0;
75
- for (const f of files) {
76
- const p = path.join(LEASES_DIR, f);
77
- const data = readJson<{pid:number, ts:number}>(p);
78
- if (data?.pid && isPidAlive(data.pid)) alive++;
79
- else rmSafe(p);
80
- }
81
- return alive;
82
- } catch { return 0; }
83
- }
84
-
85
- export async function ensureSingletonLocalServer(opts: {
86
- serverEntry: string;
87
- host?: string;
88
- port?: number;
89
- baseUrlEnv?: string;
90
- passEnv?: Record<string, string>;
91
- }): Promise<Handle> {
92
- log(`=== ensureSingletonLocalServer 启动 ===`);
93
- log(`serverEntry: ${opts.serverEntry}`);
94
- log(`serverEntry exists: ${fs.existsSync(opts.serverEntry)}`);
95
- log(`process.cwd: ${process.cwd()}`);
96
- log(`process.execPath: ${process.execPath}`);
97
- log(`platform: ${process.platform} arch: ${process.arch}`);
98
- log(`node version: ${process.version}`);
99
-
100
- const preset = (opts.baseUrlEnv || process.env.BASE_API_URL || '').trim();
101
- if (preset) {
102
- log(`使用预设 baseUrl: ${preset}`);
103
- try { await waitHealthy(preset, 3000); } catch {}
104
- await acquireLease();
105
- return { baseUrl: preset.replace(/\/+$/, ''), stopIfLast: async () => {} };
106
- }
107
-
108
- const host = opts.host || DEFAULT_HOST;
109
- const port = Number(opts.port ?? DEFAULT_PORT);
110
- const baseUrl = `http://${host}:${port}`;
111
- log(`目标 baseUrl: ${baseUrl}`);
112
- mkdirp(RUNTIME_DIR);
113
-
114
- const lock = readJson<{ pid:number, port:number }>(LOCK_JSON);
115
- log(`现有锁文件: ${JSON.stringify(lock)}`);
116
- if (lock && lock.port === port) {
117
- const healthy = await (async () => { try { await waitHealthy(baseUrl, 1200); return true; } catch { return false; } })();
118
- log(`已有锁 pid=${lock.pid} healthy=${healthy} alive=${lock.pid ? isPidAlive(lock.pid) : false}`);
119
- if (healthy && isPidAlive(lock.pid)) {
120
- await acquireLease();
121
- return { baseUrl, stopIfLast: makeStopIfLast(lock.pid, baseUrl), pid: lock.pid };
122
- }
123
- log(`锁文件无效,清除`);
124
- rmSafe(LOCK_JSON);
125
- }
126
-
127
- const iAmSpawner = atomicCreate(BOOT_LOCK);
128
- log(`iAmSpawner: ${iAmSpawner}`);
129
- if (!iAmSpawner) {
130
- log(`等待其他进程启动服务...`);
131
- try { await waitHealthy(baseUrl, HEALTH_TIMEOUT_MS); } catch {
132
- log(`等待超时,清除 boot lock 后重试`);
133
- rmSafe(BOOT_LOCK); rmSafe(LOCK_JSON);
134
- return await ensureSingletonLocalServer(opts);
135
- }
136
- await acquireLease();
137
- const live = readJson<{pid:number}>(LOCK_JSON);
138
- return { baseUrl, stopIfLast: makeStopIfLast(live?.pid || 0, baseUrl), pid: live?.pid };
139
- }
140
-
141
- let child: any = null;
142
- try {
143
- let bun: string;
144
- try {
145
- bun = resolveBunPath();
146
- log(`bun 路径: ${bun}`);
147
- } catch (e: any) {
148
- log(`resolveBunPath 失败: ${e?.message}`);
149
- throw e;
150
- }
151
-
152
- // 检查 serverEntry 文件是否存在
153
- if (!fs.existsSync(opts.serverEntry)) {
154
- log(`错误:serverEntry 文件不存在: ${opts.serverEntry}`);
155
- // 尝试列出父目录内容帮助诊断
156
- try {
157
- const parentDir = path.dirname(opts.serverEntry);
158
- log(`父目录 ${parentDir} 存在: ${fs.existsSync(parentDir)}`);
159
- if (fs.existsSync(parentDir)) {
160
- const files = fs.readdirSync(parentDir);
161
- log(`父目录内容: ${files.join(', ')}`);
162
- }
163
- // 列出祖父目录
164
- const grandParentDir = path.dirname(parentDir);
165
- log(`祖父目录 ${grandParentDir} 存在: ${fs.existsSync(grandParentDir)}`);
166
- if (fs.existsSync(grandParentDir)) {
167
- const files2 = fs.readdirSync(grandParentDir);
168
- log(`祖父目录内容: ${files2.join(', ')}`);
169
- }
170
- } catch (dirErr: any) {
171
- log(`目录列举失败: ${dirErr?.message}`);
172
- }
173
- throw new Error(`服务器入口文件不存在: ${opts.serverEntry}`);
174
- }
175
-
176
- const spawnArgs = [opts.serverEntry, '--host', host, '--port', String(port)];
177
- const spawnEnv = { ...process.env, SERVER_AUTH_REQUIRED: '0', ...(opts.passEnv || {}) };
178
- // cwd 设为包根目录(serverEntry 的两级上级:src/server -> src -> 包根),
179
- // 确保 bun 能从正确位置查找 node_modules,无论用户在哪个目录启动
180
- const serverCwd = path.dirname(path.dirname(path.dirname(opts.serverEntry)));
181
- log(`spawn: ${bun} ${spawnArgs.join(' ')}`);
182
- log(`spawn cwd: ${serverCwd}`);
183
-
184
- child = spawn(
185
- bun,
186
- spawnArgs,
187
- {
188
- env: spawnEnv,
189
- cwd: serverCwd,
190
- stdio: ['ignore', 'pipe', 'pipe'],
191
- detached: false,
192
- }
193
- );
194
-
195
- log(`子进程 pid: ${child.pid}`);
196
-
197
- // 收集子进程输出用于诊断
198
- let childStdout = '';
199
- let childStderr = '';
200
- child.stdout?.on('data', (d: Buffer) => {
201
- const s = d.toString();
202
- childStdout += s;
203
- log(`[child stdout] ${s.trim()}`);
204
- });
205
- child.stderr?.on('data', (d: Buffer) => {
206
- const s = d.toString();
207
- childStderr += s;
208
- log(`[child stderr] ${s.trim()}`);
209
- });
210
- child.on('exit', (code: number | null, signal: string | null) => {
211
- log(`子进程退出: code=${code} signal=${signal}`);
212
- log(`子进程 stdout 总计: ${childStdout.slice(0, 500)}`);
213
- log(`子进程 stderr 总计: ${childStderr.slice(0, 500)}`);
214
- });
215
- child.on('error', (err: Error) => {
216
- log(`子进程 spawn error: ${err.message}`);
217
- });
218
-
219
- log(`开始健康检查,超时 ${HEALTH_TIMEOUT_MS}ms`);
220
- await waitHealthy(baseUrl, HEALTH_TIMEOUT_MS);
221
- log(`健康检查通过`);
222
- writeJson(LOCK_JSON, { pid: child.pid, port, startedAt: new Date().toISOString() });
223
- } catch (e: any) {
224
- log(`启动失败: ${e?.message}`);
225
- if (child?.pid) {
226
- log(`清理子进程 pid=${child.pid}`);
227
- try {
228
- if (process.platform === 'win32') spawn('taskkill', ['/PID', String(child.pid), '/T', '/F']);
229
- else process.kill(child.pid, 'SIGTERM');
230
- } catch {}
231
- }
232
- throw e;
233
- } finally {
234
- rmSafe(BOOT_LOCK);
235
- }
236
-
237
- await acquireLease();
238
- log(`启动完成 pid=${child.pid} baseUrl=${baseUrl}`);
239
- return { baseUrl, stopIfLast: makeStopIfLast(child.pid, baseUrl), pid: child.pid };
240
- }
241
-
242
- function makeStopIfLast(serverPid: number, baseUrl: string) {
243
- return async () => {
244
- const rest = countValidLeases();
245
- if (rest > 0) return;
246
- let healthy = false;
247
- try { await waitHealthy(baseUrl, 800); healthy = true; } catch {}
248
- if (healthy && serverPid && isPidAlive(serverPid)) {
249
- try {
250
- if (process.platform === 'win32') spawn('taskkill', ['/PID', String(serverPid), '/T', '/F']);
251
- else process.kill(serverPid, 'SIGTERM');
252
- } catch {}
253
- }
254
- rmSafe(LOCK_JSON);
255
- };
256
- }
1
+ // server/ensureSingletonLocalServer.ts
2
+ import { spawn, spawnSync } from 'child_process';
3
+ import fs from 'fs';
4
+ import fsp from 'fs/promises';
5
+ import path from 'path';
6
+ import os from 'os';
7
+ import axios from 'axios';
8
+
9
+ type Handle = {
10
+ baseUrl: string;
11
+ stopIfLast: () => Promise<void>;
12
+ pid?: number;
13
+ };
14
+
15
+ const RUNTIME_DIR = path.join(os.homedir(), '.claude-cli', 'runtime');
16
+ const LOCK_JSON = path.join(RUNTIME_DIR, 'server.lock.json');
17
+ const BOOT_LOCK = path.join(RUNTIME_DIR, 'server.boot.lock');
18
+ const LEASES_DIR = path.join(RUNTIME_DIR, 'leases');
19
+
20
+ const DEFAULT_HOST = '127.0.0.1';
21
+ const DEFAULT_PORT = Number(process.env.SERVER_PORT || 3456);
22
+ const HEALTH_TIMEOUT_MS = Number(process.env.HEALTH_TIMEOUT_MS || 20000);
23
+ const HEALTH_RETRY_MS = 300;
24
+
25
+ function mkdirp(p: string) { fs.mkdirSync(p, { recursive: true }); }
26
+ function atomicCreate(p: string): boolean {
27
+ try { const fd = fs.openSync(p, 'wx'); fs.closeSync(fd); return true; } catch { return false; }
28
+ }
29
+ function rmSafe(p: string) { try { fs.rmSync(p, { force: true, recursive: true }); } catch {} }
30
+ function readJson<T>(p: string): T | null { try { return JSON.parse(fs.readFileSync(p, 'utf-8')) as T; } catch { return null; } }
31
+ function writeJson(p: string, data: any) { fs.writeFileSync(p, JSON.stringify(data, null, 2), 'utf-8'); }
32
+ function isPidAlive(pid: number): boolean { try { process.kill(pid, 0); return true; } catch { return false; } }
33
+ async function waitHealthy(baseUrl: string, timeoutMs: number) {
34
+ const start = Date.now(); let lastErr: any = null;
35
+ while (Date.now() - start < timeoutMs) {
36
+ try {
37
+ const r = await axios.get(baseUrl.replace(/\/+$/, '') + '/health', { timeout: 1500 });
38
+ if (r.status === 200 && r.data?.status === 'ok') return;
39
+ } catch (e) { lastErr = e; }
40
+ await new Promise(r => setTimeout(r, HEALTH_RETRY_MS));
41
+ }
42
+ throw new Error(`健康检查超时:${lastErr?.message || 'unknown'}`);
43
+ }
44
+ function resolveBunPath() {
45
+ const fromEnv = process.env.BUN_PATH;
46
+ if (fromEnv) return fromEnv;
47
+ const r = spawnSync('bun', ['--version'], { stdio: 'ignore' });
48
+ if (r.status === 0) return 'bun';
49
+ throw new Error('未检测到 bun,请安装 https://bun.sh 或设置 BUN_PATH');
50
+ }
51
+
52
+ async function acquireLease(): Promise<string> {
53
+ mkdirp(LEASES_DIR);
54
+ const lease = path.join(LEASES_DIR, `${process.pid}.json`);
55
+ await fsp.writeFile(lease, JSON.stringify({ pid: process.pid, ts: Date.now() }));
56
+ const cleanup = () => rmSafe(lease);
57
+ process.once('exit', cleanup);
58
+ process.once('SIGINT', () => { cleanup(); process.exit(130); });
59
+ process.once('SIGTERM', () => { cleanup(); process.exit(143); });
60
+ return lease;
61
+ }
62
+ function countValidLeases(): number {
63
+ try {
64
+ const files = fs.readdirSync(LEASES_DIR);
65
+ let alive = 0;
66
+ for (const f of files) {
67
+ const p = path.join(LEASES_DIR, f);
68
+ const data = readJson<{pid:number, ts:number}>(p);
69
+ if (data?.pid && isPidAlive(data.pid)) alive++;
70
+ else rmSafe(p);
71
+ }
72
+ return alive;
73
+ } catch { return 0; }
74
+ }
75
+
76
+ export async function ensureSingletonLocalServer(opts: {
77
+ serverEntry: string;
78
+ host?: string;
79
+ port?: number;
80
+ baseUrlEnv?: string;
81
+ passEnv?: Record<string, string>;
82
+ }): Promise<Handle> {
83
+ const preset = (opts.baseUrlEnv || process.env.BASE_API_URL || '').trim();
84
+ if (preset) {
85
+ try { await waitHealthy(preset, 3000); } catch {}
86
+ await acquireLease();
87
+ return { baseUrl: preset.replace(/\/+$/, ''), stopIfLast: async () => {} };
88
+ }
89
+
90
+ const host = opts.host || DEFAULT_HOST;
91
+ const port = Number(opts.port ?? DEFAULT_PORT);
92
+ const baseUrl = `http://${host}:${port}`;
93
+ mkdirp(RUNTIME_DIR);
94
+
95
+ const lock = readJson<{ pid:number, port:number }>(LOCK_JSON);
96
+ if (lock && lock.port === port) {
97
+ const healthy = await (async () => { try { await waitHealthy(baseUrl, 1200); return true; } catch { return false; } })();
98
+ if (healthy && isPidAlive(lock.pid)) {
99
+ await acquireLease();
100
+ return { baseUrl, stopIfLast: makeStopIfLast(lock.pid, baseUrl), pid: lock.pid };
101
+ }
102
+ rmSafe(LOCK_JSON);
103
+ }
104
+
105
+ const iAmSpawner = atomicCreate(BOOT_LOCK);
106
+ if (!iAmSpawner) {
107
+ try { await waitHealthy(baseUrl, HEALTH_TIMEOUT_MS); } catch {
108
+ rmSafe(BOOT_LOCK); rmSafe(LOCK_JSON);
109
+ return await ensureSingletonLocalServer(opts);
110
+ }
111
+ await acquireLease();
112
+ const live = readJson<{pid:number}>(LOCK_JSON);
113
+ return { baseUrl, stopIfLast: makeStopIfLast(live?.pid || 0, baseUrl), pid: live?.pid };
114
+ }
115
+
116
+ let child: any = null;
117
+ try {
118
+ const bun = resolveBunPath();
119
+ child = spawn(
120
+ bun,
121
+ [opts.serverEntry, '--host', host, '--port', String(port)],
122
+ {
123
+ env: { ...process.env, SERVER_AUTH_REQUIRED: '0', ...(opts.passEnv || {}) },
124
+ stdio: 'ignore',
125
+ detached: false,
126
+ }
127
+ );
128
+ await waitHealthy(baseUrl, HEALTH_TIMEOUT_MS);
129
+ writeJson(LOCK_JSON, { pid: child.pid, port, startedAt: new Date().toISOString() });
130
+ } catch (e) {
131
+ if (child?.pid) {
132
+ try {
133
+ if (process.platform === 'win32') spawn('taskkill', ['/PID', String(child.pid), '/T', '/F']);
134
+ else process.kill(child.pid, 'SIGTERM');
135
+ } catch {}
136
+ }
137
+ throw e;
138
+ } finally {
139
+ rmSafe(BOOT_LOCK);
140
+ }
141
+
142
+ await acquireLease();
143
+ return { baseUrl, stopIfLast: makeStopIfLast(child.pid, baseUrl), pid: child.pid };
144
+ }
145
+
146
+ function makeStopIfLast(serverPid: number, baseUrl: string) {
147
+ return async () => {
148
+ const rest = countValidLeases();
149
+ if (rest > 0) return;
150
+ let healthy = false;
151
+ try { await waitHealthy(baseUrl, 800); healthy = true; } catch {}
152
+ if (healthy && serverPid && isPidAlive(serverPid)) {
153
+ try {
154
+ if (process.platform === 'win32') spawn('taskkill', ['/PID', String(serverPid), '/T', '/F']);
155
+ else process.kill(serverPid, 'SIGTERM');
156
+ } catch {}
157
+ }
158
+ rmSafe(LOCK_JSON);
159
+ };
160
+ }
@@ -498,7 +498,8 @@ export class ProviderService {
498
498
  }
499
499
 
500
500
  const modelsUrl = preset?.modelsUrl || '/v1/models'
501
- const url = `${base}${modelsUrl}`
501
+ // modelsUrl 为绝对 URL 时直接使用(如 DeepSeek: baseUrl 是 anthropic 端,模型列表需走 OpenAI 端)
502
+ const url = modelsUrl.startsWith('http') ? modelsUrl : `${base}${modelsUrl}`
502
503
 
503
504
  const headers: Record<string, string> = {
504
505
  'Content-Type': 'application/json',
@@ -516,8 +517,9 @@ export class ProviderService {
516
517
  const directOpts = getDirectFetchOptions()
517
518
  const res = await fetch(url, { headers, signal: AbortSignal.timeout(10000), ...directOpts })
518
519
  if (!res.ok) {
519
- console.error(`[ProviderService] Failed to fetch models from ${url}: ${res.status}`)
520
- return []
520
+ const errText = await res.text().catch(() => '')
521
+ console.error(`[ProviderService] Failed to fetch models from ${url}: ${res.status} ${errText}`)
522
+ throw new Error(`HTTP ${res.status}: ${errText.slice(0, 200)}`)
521
523
  }
522
524
  const data = await res.json() as any
523
525
  const dataPath = preset?.modelsDataPath || 'data'
@@ -526,7 +528,7 @@ export class ProviderService {
526
528
  return list.map((m: any) => (typeof m === 'string' ? m : m.id)).filter(Boolean)
527
529
  } catch (err) {
528
530
  console.error(`[ProviderService] Error fetching models from ${url}:`, err)
529
- return []
531
+ throw err
530
532
  }
531
533
  }
532
534
 
@@ -553,14 +555,19 @@ export class ProviderService {
553
555
  }
554
556
  }
555
557
 
556
- // 兜底:如果仍然没有有效的 modelId,直接返回有意义的错误
558
+ // 兜底:动态拉取失败时,按 apiFormat 使用通用 fallback 模型做连通性测试
557
559
  if (!modelId) {
558
- return {
559
- connectivity: {
560
- success: false,
561
- latencyMs: 0,
562
- error: '无法确定测试用模型:models.main 为空且自动拉取模型列表失败。请先在槽位配置中选择模型,或检查 API Key 和网络连接。',
563
- },
560
+ if (apiFormat === 'anthropic') {
561
+ modelId = 'claude-3-5-haiku-20241022'
562
+ } else {
563
+ // openai_chat / openai_responses: 无法确定模型,返回有意义的错误
564
+ return {
565
+ connectivity: {
566
+ success: false,
567
+ latencyMs: 0,
568
+ error: '无法确定测试用模型:models.main 为空且自动拉取模型列表失败。请先在槽位配置中选择模型,或检查 API Key 和网络连接。',
569
+ },
570
+ }
564
571
  }
565
572
  }
566
573