@spacex110/core 0.1.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/dist/index.d.ts +620 -0
- package/dist/index.js +6401 -0
- package/dist/index.js.map +1 -0
- package/dist/index.umd.js +16 -0
- package/dist/index.umd.js.map +1 -0
- package/package.json +41 -0
- package/src/api.ts +158 -0
- package/src/channel-manager.ts +790 -0
- package/src/config.ts +104 -0
- package/src/i18n.ts +66 -0
- package/src/index.ts +97 -0
- package/src/models.ts +202 -0
- package/src/providers.ts +249 -0
- package/src/storage.ts +135 -0
- package/src/strategies.ts +469 -0
- package/src/styles.css +246 -0
- package/src/types.ts +163 -0
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
import type { Provider, Model, ModelCapability } from './types';
|
|
2
|
+
|
|
3
|
+
export interface ProviderStrategy {
|
|
4
|
+
format: string;
|
|
5
|
+
// 模型列表
|
|
6
|
+
getModelsEndpoint?: (baseUrl: string, apiKey: string) => string;
|
|
7
|
+
parseModelsResponse?: (data: any) => Model[];
|
|
8
|
+
// 聊天 (也用于连接测试)
|
|
9
|
+
getChatEndpoint: (baseUrl: string, apiKey: string, model: string) => string;
|
|
10
|
+
buildChatPayload: (model: string, messages: Array<{ role: string; content: string }>, maxTokens?: number) => Record<string, unknown>;
|
|
11
|
+
parseChatResponse: (data: any) => string;
|
|
12
|
+
/** 从一个 SSE data 块中提取增量文本(流式)。返回空字符串表示无内容。 */
|
|
13
|
+
parseStreamChunk?: (data: any) => string;
|
|
14
|
+
// Headers
|
|
15
|
+
buildHeaders: (apiKey: string) => Record<string, string>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 判断模型是否为 OpenAI o 系列 / 推理模型(只接受 max_completion_tokens)。
|
|
20
|
+
* 其余 OpenAI 兼容厂商(DeepSeek/Qwen/Moonshot 等)只认 max_tokens。
|
|
21
|
+
*/
|
|
22
|
+
function prefersCompletionTokens(model: string): boolean {
|
|
23
|
+
return /^o\d/.test(model) || /reasoner|thinking|o1|o3|o4/i.test(model);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 从 API 响应中提取模型能力
|
|
28
|
+
* 支持多种格式:
|
|
29
|
+
* - OmniRoute: input_modalities, output_modalities, capabilities
|
|
30
|
+
* - OpenAI: capabilities
|
|
31
|
+
* - 关键字降级(vision/reasoning 等出现在 id 或 name 中)
|
|
32
|
+
*/
|
|
33
|
+
function detectCapabilities(m: any): ModelCapability[] {
|
|
34
|
+
const caps = new Set<ModelCapability>();
|
|
35
|
+
|
|
36
|
+
// 1. 优先从 input_modalities 推断(OmniRoute 格式)
|
|
37
|
+
const inputMods: string[] = m.input_modalities || [];
|
|
38
|
+
if (inputMods.length > 0) {
|
|
39
|
+
if (inputMods.some((s: string) => /text/i.test(s))) caps.add('text');
|
|
40
|
+
if (inputMods.some((s: string) => /image|vision/i.test(s))) caps.add('vision');
|
|
41
|
+
if (inputMods.some((s: string) => /audio/i.test(s))) caps.add('audio');
|
|
42
|
+
if (inputMods.some((s: string) => /video/i.test(s))) caps.add('video');
|
|
43
|
+
// 如果没有任何明确的修饰符,默认是文本
|
|
44
|
+
if (caps.size === 0) caps.add('text');
|
|
45
|
+
return Array.from(caps);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 2. 从 capabilities 对象推断(OpenAI/OmniRoute 格式)
|
|
49
|
+
const cap = m.capabilities;
|
|
50
|
+
if (cap) {
|
|
51
|
+
if (cap.vision || cap.image) {
|
|
52
|
+
caps.add('vision');
|
|
53
|
+
caps.add('text');
|
|
54
|
+
}
|
|
55
|
+
if (cap.audio) caps.add('audio');
|
|
56
|
+
if (cap.video) caps.add('video');
|
|
57
|
+
if (cap.embeddings) caps.add('embedding');
|
|
58
|
+
if (caps.size > 0) return Array.from(caps);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 3. 关键字降级(从 id/name 推断)
|
|
62
|
+
const modelId = (m.id || '').toLowerCase();
|
|
63
|
+
const modelName = (m.name || '').toLowerCase();
|
|
64
|
+
const combined = `${modelId} ${modelName}`;
|
|
65
|
+
|
|
66
|
+
caps.add('text'); // 默认至少是文本
|
|
67
|
+
|
|
68
|
+
if (/vision|image|visual|multimodal|vl|pixtral|llava|bakllava/.test(combined)) {
|
|
69
|
+
caps.add('vision');
|
|
70
|
+
}
|
|
71
|
+
if (/audio|speech|whisper|tts|stt/.test(combined)) {
|
|
72
|
+
caps.add('audio');
|
|
73
|
+
}
|
|
74
|
+
if (/video/.test(combined)) {
|
|
75
|
+
caps.add('video');
|
|
76
|
+
}
|
|
77
|
+
if (/embedding|embed/.test(combined)) {
|
|
78
|
+
caps.add('embedding');
|
|
79
|
+
}
|
|
80
|
+
if (/reasoner|reasoning|thinking|qwq/.test(combined)) {
|
|
81
|
+
caps.add('reasoning');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return Array.from(caps);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const defaultParseModelsResponse = (data: any): Model[] => {
|
|
88
|
+
if (Array.isArray(data?.data)) {
|
|
89
|
+
return data.data
|
|
90
|
+
.filter((m: any) => m.id)
|
|
91
|
+
.map((m: any) => {
|
|
92
|
+
const name =
|
|
93
|
+
m.name || m.display_name ||
|
|
94
|
+
(m.id.split('/').pop() ?? '')
|
|
95
|
+
.replace(/[-_]/g, ' ')
|
|
96
|
+
.replace(/\b\w/g, (c: string) => c.toUpperCase());
|
|
97
|
+
return {
|
|
98
|
+
id: m.id,
|
|
99
|
+
name,
|
|
100
|
+
created: m.created || 0,
|
|
101
|
+
contextLength: m.context_length || m.max_context || undefined,
|
|
102
|
+
capabilities: detectCapabilities(m),
|
|
103
|
+
} as any;
|
|
104
|
+
})
|
|
105
|
+
.sort((a: any, b: any) => {
|
|
106
|
+
const diff = (b.created || 0) - (a.created || 0);
|
|
107
|
+
if (diff !== 0) return diff;
|
|
108
|
+
return (b.id || '').localeCompare(a.id || '');
|
|
109
|
+
}); // 最新的排在前面
|
|
110
|
+
}
|
|
111
|
+
return [];
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const openaiStrategy: ProviderStrategy = {
|
|
115
|
+
format: 'openai',
|
|
116
|
+
getModelsEndpoint: (baseUrl) => `${baseUrl}/models`,
|
|
117
|
+
getChatEndpoint: (baseUrl) => `${baseUrl}/chat/completions`,
|
|
118
|
+
buildHeaders: (apiKey) => {
|
|
119
|
+
const headers: Record<string, string> = {
|
|
120
|
+
'Content-Type': 'application/json',
|
|
121
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
122
|
+
};
|
|
123
|
+
// OpenRouter compatibility & Best Practices
|
|
124
|
+
if (typeof window !== 'undefined' && window.location) {
|
|
125
|
+
headers['HTTP-Referer'] = window.location.origin;
|
|
126
|
+
headers['X-Title'] = document.title || 'AI Selector';
|
|
127
|
+
}
|
|
128
|
+
return headers;
|
|
129
|
+
},
|
|
130
|
+
buildChatPayload: (model, messages, maxTokens) => {
|
|
131
|
+
const payload: any = {
|
|
132
|
+
model,
|
|
133
|
+
messages,
|
|
134
|
+
};
|
|
135
|
+
if (maxTokens) {
|
|
136
|
+
// o 系列 / 推理模型只接受 max_completion_tokens;DeepSeek/Qwen 等国产兼容厂商只认 max_tokens
|
|
137
|
+
payload[prefersCompletionTokens(model) ? 'max_completion_tokens' : 'max_tokens'] = maxTokens;
|
|
138
|
+
}
|
|
139
|
+
return payload;
|
|
140
|
+
},
|
|
141
|
+
parseChatResponse: (data) => {
|
|
142
|
+
return data.choices?.[0]?.message?.content || '';
|
|
143
|
+
},
|
|
144
|
+
parseStreamChunk: (data) => {
|
|
145
|
+
// OpenAI SSE: { choices: [{ delta: { content } }] },[DONE] 由调用方过滤
|
|
146
|
+
return data.choices?.[0]?.delta?.content || '';
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const anthropicStrategy: ProviderStrategy = {
|
|
151
|
+
format: 'anthropic',
|
|
152
|
+
getModelsEndpoint: (baseUrl) => `${baseUrl}/models`,
|
|
153
|
+
getChatEndpoint: (baseUrl) => `${baseUrl}/messages`,
|
|
154
|
+
buildHeaders: (apiKey) => ({
|
|
155
|
+
'Content-Type': 'application/json',
|
|
156
|
+
'x-api-key': apiKey,
|
|
157
|
+
'anthropic-version': '2023-06-01',
|
|
158
|
+
'anthropic-dangerous-direct-browser-access': 'true', // 启用浏览器直连 CORS
|
|
159
|
+
}),
|
|
160
|
+
buildChatPayload: (model, messages, maxTokens) => {
|
|
161
|
+
const payload: any = {
|
|
162
|
+
model,
|
|
163
|
+
messages,
|
|
164
|
+
max_tokens: maxTokens || 1024, // Anthropic API 强制要求 max_tokens
|
|
165
|
+
};
|
|
166
|
+
return payload;
|
|
167
|
+
},
|
|
168
|
+
parseChatResponse: (data) => {
|
|
169
|
+
return data.content?.[0]?.text || '';
|
|
170
|
+
},
|
|
171
|
+
parseStreamChunk: (data) => {
|
|
172
|
+
// Anthropic SSE 事件:content_block_delta -> { delta: { text } }
|
|
173
|
+
if (data.type === 'content_block_delta') {
|
|
174
|
+
return data.delta?.text || '';
|
|
175
|
+
}
|
|
176
|
+
return '';
|
|
177
|
+
},
|
|
178
|
+
// Anthropic 返回: { data: [{ id, display_name, created_at }] }
|
|
179
|
+
parseModelsResponse: (data) => {
|
|
180
|
+
if (Array.isArray(data.data)) {
|
|
181
|
+
return data.data
|
|
182
|
+
.filter((m: any) => m.id)
|
|
183
|
+
.map((m: any) => ({
|
|
184
|
+
id: m.id,
|
|
185
|
+
name: m.display_name || m.id,
|
|
186
|
+
created: m.created_at ? new Date(m.created_at).getTime() / 1000 : 0,
|
|
187
|
+
capabilities: detectCapabilities(m),
|
|
188
|
+
}))
|
|
189
|
+
.sort((a: any, b: any) => (b.created || 0) - (a.created || 0));
|
|
190
|
+
}
|
|
191
|
+
return [];
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const geminiStrategy: ProviderStrategy = {
|
|
196
|
+
format: 'gemini',
|
|
197
|
+
getModelsEndpoint: (baseUrl, apiKey) => `${baseUrl}/models?key=${apiKey}`,
|
|
198
|
+
getChatEndpoint: (baseUrl, apiKey, model) => `${baseUrl}/models/${model}:generateContent?key=${apiKey}`,
|
|
199
|
+
buildHeaders: () => ({
|
|
200
|
+
'Content-Type': 'application/json',
|
|
201
|
+
}),
|
|
202
|
+
buildChatPayload: (_model, messages, maxTokens) => {
|
|
203
|
+
// 转换 OpenAI 格式的 messages 为 Gemini 格式
|
|
204
|
+
const contents = messages.map(m => ({
|
|
205
|
+
role: m.role === 'assistant' ? 'model' : 'user',
|
|
206
|
+
parts: [{ text: m.content }],
|
|
207
|
+
}));
|
|
208
|
+
const payload: any = { contents };
|
|
209
|
+
if (maxTokens) {
|
|
210
|
+
payload.generationConfig = { maxOutputTokens: maxTokens };
|
|
211
|
+
}
|
|
212
|
+
return payload;
|
|
213
|
+
},
|
|
214
|
+
parseChatResponse: (data) => {
|
|
215
|
+
return data.candidates?.[0]?.content?.parts?.[0]?.text || '';
|
|
216
|
+
},
|
|
217
|
+
parseStreamChunk: (data) => {
|
|
218
|
+
// Gemini streamGenerateContent SSE
|
|
219
|
+
return data.candidates?.[0]?.content?.parts?.[0]?.text || '';
|
|
220
|
+
},
|
|
221
|
+
// Gemini 返回格式: { models: [{ name: "models/gemini-pro", ... }] }
|
|
222
|
+
parseModelsResponse: (data) => {
|
|
223
|
+
if (Array.isArray(data.models)) {
|
|
224
|
+
return data.models
|
|
225
|
+
.filter((m: any) => m.supportedGenerationMethods?.includes('generateContent'))
|
|
226
|
+
.map((m: any) => ({
|
|
227
|
+
id: m.name.replace('models/', ''), // "models/gemini-pro" -> "gemini-pro"
|
|
228
|
+
name: m.displayName || m.name.replace('models/', ''),
|
|
229
|
+
created: m.created || 0,
|
|
230
|
+
capabilities: detectCapabilities(m),
|
|
231
|
+
}))
|
|
232
|
+
.sort((a: any, b: any) => (b.id || '').localeCompare(a.id || ''));
|
|
233
|
+
}
|
|
234
|
+
return [];
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const cohereStrategy: ProviderStrategy = {
|
|
239
|
+
format: 'cohere',
|
|
240
|
+
getModelsEndpoint: (baseUrl) => `${baseUrl}/models`,
|
|
241
|
+
getChatEndpoint: (baseUrl) => `${baseUrl}/chat`,
|
|
242
|
+
buildHeaders: (apiKey) => ({
|
|
243
|
+
'Content-Type': 'application/json',
|
|
244
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
245
|
+
}),
|
|
246
|
+
buildChatPayload: (model, messages, maxTokens) => {
|
|
247
|
+
// Cohere v2 API 使用 messages 格式
|
|
248
|
+
const payload: any = {
|
|
249
|
+
model,
|
|
250
|
+
messages,
|
|
251
|
+
};
|
|
252
|
+
if (maxTokens) {
|
|
253
|
+
payload.max_tokens = maxTokens;
|
|
254
|
+
}
|
|
255
|
+
return payload;
|
|
256
|
+
},
|
|
257
|
+
parseChatResponse: (data) => {
|
|
258
|
+
// Cohere v2 响应格式
|
|
259
|
+
return data.message?.content?.[0]?.text || '';
|
|
260
|
+
},
|
|
261
|
+
parseStreamChunk: (data) => {
|
|
262
|
+
// Cohere v2 SSE: { type: 'content-delta', delta: { message: { content: [{ text }] } } }
|
|
263
|
+
if (data.type === 'content-delta') {
|
|
264
|
+
return data.delta?.message?.content?.[0]?.text || '';
|
|
265
|
+
}
|
|
266
|
+
return '';
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
export const strategyRegistry: Record<string, ProviderStrategy> = {
|
|
271
|
+
openai: openaiStrategy,
|
|
272
|
+
anthropic: anthropicStrategy,
|
|
273
|
+
gemini: geminiStrategy,
|
|
274
|
+
cohere: cohereStrategy,
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
export function getStrategy(format: string): ProviderStrategy {
|
|
278
|
+
return strategyRegistry[format] || openaiStrategy;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ============================================================================
|
|
282
|
+
// 纯前端直连聊天函数
|
|
283
|
+
// ============================================================================
|
|
284
|
+
|
|
285
|
+
export interface DirectChatOptions {
|
|
286
|
+
apiFormat: string;
|
|
287
|
+
baseUrl: string;
|
|
288
|
+
apiKey: string;
|
|
289
|
+
model: string;
|
|
290
|
+
messages: Array<{ role: string; content: string }>;
|
|
291
|
+
maxTokens?: number;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export interface DirectChatResult {
|
|
295
|
+
success: boolean;
|
|
296
|
+
content?: string;
|
|
297
|
+
message?: string;
|
|
298
|
+
latencyMs?: number;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* 纯前端直连 AI 厂商进行聊天
|
|
303
|
+
* 注意: 这会将 API Key 暴露在浏览器中,仅适用于 Demo/测试场景
|
|
304
|
+
*/
|
|
305
|
+
export async function sendDirectChat(options: DirectChatOptions): Promise<DirectChatResult> {
|
|
306
|
+
const { apiFormat, baseUrl, apiKey, model, messages, maxTokens } = options;
|
|
307
|
+
const strategy = getStrategy(apiFormat);
|
|
308
|
+
|
|
309
|
+
const endpoint = strategy.getChatEndpoint(baseUrl, apiKey, model);
|
|
310
|
+
const headers = strategy.buildHeaders(apiKey);
|
|
311
|
+
const payload = strategy.buildChatPayload(model, messages, maxTokens);
|
|
312
|
+
|
|
313
|
+
const startTime = performance.now();
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
const response = await fetch(endpoint, {
|
|
317
|
+
method: 'POST',
|
|
318
|
+
headers,
|
|
319
|
+
body: JSON.stringify(payload),
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const latencyMs = Math.round(performance.now() - startTime);
|
|
323
|
+
|
|
324
|
+
if (!response.ok) {
|
|
325
|
+
const errorData = await response.json().catch(() => ({}));
|
|
326
|
+
return {
|
|
327
|
+
success: false,
|
|
328
|
+
message: errorData.error?.message || `HTTP ${response.status}: ${response.statusText}`,
|
|
329
|
+
latencyMs,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const data = await response.json();
|
|
334
|
+
const content = strategy.parseChatResponse(data);
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
success: true,
|
|
338
|
+
content,
|
|
339
|
+
latencyMs,
|
|
340
|
+
};
|
|
341
|
+
} catch (e) {
|
|
342
|
+
return {
|
|
343
|
+
success: false,
|
|
344
|
+
message: e instanceof Error ? e.message : '网络错误',
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ============================================================================
|
|
350
|
+
// 纯前端直连流式聊天
|
|
351
|
+
// ============================================================================
|
|
352
|
+
|
|
353
|
+
export interface DirectChatStreamOptions extends DirectChatOptions {
|
|
354
|
+
/** 每收到增量文本时回调,参数为「累计完整文本」 */
|
|
355
|
+
onDelta: (full: string) => void;
|
|
356
|
+
/** 可选 AbortSignal */
|
|
357
|
+
signal?: AbortSignal;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* 纯前端直连流式聊天。逐增量回调 onDelta(累计文本),返回最终完整文本。
|
|
362
|
+
* 注意: 与 sendDirectChat 一样会把 API Key 暴露在浏览器,仅适用于 Demo/本地场景。
|
|
363
|
+
*/
|
|
364
|
+
export async function sendDirectChatStream(options: DirectChatStreamOptions): Promise<DirectChatResult> {
|
|
365
|
+
const { apiFormat, baseUrl, apiKey, model, messages, maxTokens, onDelta, signal } = options;
|
|
366
|
+
const strategy = getStrategy(apiFormat);
|
|
367
|
+
|
|
368
|
+
// 计算流式端点:gemini 需改为 streamGenerateContent?alt=sse
|
|
369
|
+
let endpoint = strategy.getChatEndpoint(baseUrl, apiKey, model);
|
|
370
|
+
if (apiFormat === 'gemini') {
|
|
371
|
+
endpoint = `${baseUrl}/models/${model}:streamGenerateContent?alt=sse&key=${encodeURIComponent(apiKey)}`;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const headers = strategy.buildHeaders(apiKey);
|
|
375
|
+
const payload = strategy.buildChatPayload(model, messages, maxTokens);
|
|
376
|
+
(payload as any).stream = true; // gemini 通过端点参数控制流式,stream 字段会被忽略,无副作用
|
|
377
|
+
|
|
378
|
+
const startTime = performance.now();
|
|
379
|
+
|
|
380
|
+
try {
|
|
381
|
+
const response = await fetch(endpoint, {
|
|
382
|
+
method: 'POST',
|
|
383
|
+
headers,
|
|
384
|
+
body: JSON.stringify(payload),
|
|
385
|
+
signal,
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
if (!response.ok || !response.body) {
|
|
389
|
+
const text = await response.text();
|
|
390
|
+
let m = `HTTP ${response.status}`;
|
|
391
|
+
try { const j = JSON.parse(text); m = j.error?.message || j.error || j.message || m; } catch { if (text) m = text.slice(0, 200); }
|
|
392
|
+
return { success: false, message: m, latencyMs: Math.round(performance.now() - startTime) };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const reader = response.body.getReader();
|
|
396
|
+
const decoder = new TextDecoder();
|
|
397
|
+
let buf = '';
|
|
398
|
+
let full = '';
|
|
399
|
+
|
|
400
|
+
for (;;) {
|
|
401
|
+
const { done, value } = await reader.read();
|
|
402
|
+
if (done) break;
|
|
403
|
+
buf += decoder.decode(value, { stream: true });
|
|
404
|
+
const lines = buf.split('\n');
|
|
405
|
+
buf = lines.pop() || '';
|
|
406
|
+
for (const line of lines) {
|
|
407
|
+
const s = line.trim();
|
|
408
|
+
if (!s.startsWith('data:') && !s.startsWith('event:')) continue;
|
|
409
|
+
// Anthropic SSE 有 event: 行,data: 行才是 JSON
|
|
410
|
+
if (s.startsWith('event:')) continue;
|
|
411
|
+
const data = s.slice(5).trim();
|
|
412
|
+
if (!data || data === '[DONE]') continue;
|
|
413
|
+
try {
|
|
414
|
+
const json = JSON.parse(data);
|
|
415
|
+
const delta = strategy.parseStreamChunk ? strategy.parseStreamChunk(json) : '';
|
|
416
|
+
if (delta) {
|
|
417
|
+
full += delta;
|
|
418
|
+
onDelta(full);
|
|
419
|
+
}
|
|
420
|
+
} catch {
|
|
421
|
+
// 非 JSON 行(心跳等),忽略
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return { success: true, content: full, latencyMs: Math.round(performance.now() - startTime) };
|
|
427
|
+
} catch (e) {
|
|
428
|
+
if (e instanceof Error && e.name === 'AbortError') {
|
|
429
|
+
return { success: false, message: '请求已取消', latencyMs: Math.round(performance.now() - startTime) };
|
|
430
|
+
}
|
|
431
|
+
return {
|
|
432
|
+
success: false,
|
|
433
|
+
message: e instanceof Error ? e.message : '网络错误',
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export interface TestConnectionOptions {
|
|
439
|
+
apiFormat: string;
|
|
440
|
+
baseUrl: string;
|
|
441
|
+
apiKey: string;
|
|
442
|
+
model: string;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export interface TestConnectionResult {
|
|
446
|
+
success: boolean;
|
|
447
|
+
latencyMs?: number;
|
|
448
|
+
message?: string;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* 测试 AI 厂商连接 (通过发送一个简单的聊天请求)
|
|
453
|
+
*/
|
|
454
|
+
export async function testDirectConnection(options: TestConnectionOptions): Promise<TestConnectionResult> {
|
|
455
|
+
const result = await sendDirectChat({
|
|
456
|
+
apiFormat: options.apiFormat,
|
|
457
|
+
baseUrl: options.baseUrl,
|
|
458
|
+
apiKey: options.apiKey,
|
|
459
|
+
model: options.model,
|
|
460
|
+
messages: [{ role: 'user', content: 'Hi' }],
|
|
461
|
+
// maxTokens: 5, // 不设置 maxTokens 以兼容 o1 等不支持该参数的模型
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
return {
|
|
465
|
+
success: result.success,
|
|
466
|
+
latencyMs: result.latencyMs,
|
|
467
|
+
message: result.success ? undefined : result.message,
|
|
468
|
+
};
|
|
469
|
+
}
|