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.
|
|
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:
|
|
102
|
+
apiFormat: anthropic
|
|
103
103
|
needsApiKey: true
|
|
104
104
|
websiteUrl: https://platform.deepseek.com
|
|
105
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
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
|
-
|
|
520
|
-
|
|
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
|
-
|
|
531
|
+
throw err
|
|
530
532
|
}
|
|
531
533
|
}
|
|
532
534
|
|
|
@@ -553,14 +555,19 @@ export class ProviderService {
|
|
|
553
555
|
}
|
|
554
556
|
}
|
|
555
557
|
|
|
556
|
-
//
|
|
558
|
+
// 兜底:动态拉取失败时,按 apiFormat 使用通用 fallback 模型做连通性测试
|
|
557
559
|
if (!modelId) {
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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
|
|