@myassis/gateway 1.0.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.
- package/README.md +194 -0
- package/dist/.env +6 -0
- package/dist/api/index.js +182 -0
- package/dist/config/index.js +41 -0
- package/dist/index.js +183 -0
- package/dist/middleware/auth.js +53 -0
- package/dist/middleware/errorHandler.js +20 -0
- package/dist/routes/agent.js +513 -0
- package/dist/routes/auth.js +172 -0
- package/dist/routes/chat.js +45 -0
- package/dist/routes/config.js +21 -0
- package/dist/routes/models.js +123 -0
- package/dist/routes/service.js +240 -0
- package/dist/routes/settings.js +101 -0
- package/dist/routes/skillHub.js +126 -0
- package/dist/routes/skills.js +159 -0
- package/dist/routes/tasks.js +149 -0
- package/dist/routes/upload.js +129 -0
- package/dist/routes/version.js +66 -0
- package/dist/services/HMSPushService.js +24 -0
- package/dist/services/LocalTaskService.js +223 -0
- package/dist/services/NotificationService.js +242 -0
- package/dist/services/ServiceManager.js +348 -0
- package/dist/services/TaskSchedulerService.js +195 -0
- package/dist/services/TaskService.js +240 -0
- package/dist/services/WebSocketService.js +236 -0
- package/dist/services/agent/Agent.js +120 -0
- package/dist/services/agent/AgentManager.js +265 -0
- package/dist/services/agent/AgentStore.js +73 -0
- package/dist/services/dataService.js +293 -0
- package/dist/services/index.js +15 -0
- package/dist/services/llm/LLMClient.js +724 -0
- package/dist/services/memory/MemoryManager.js +117 -0
- package/dist/services/model/ModelCapabilities.js +141 -0
- package/dist/services/model/index.js +4 -0
- package/dist/services/models.js +16 -0
- package/dist/services/session/MigrationManager.js +176 -0
- package/dist/services/session/Session.js +733 -0
- package/dist/services/session/SessionManager.js +255 -0
- package/dist/services/session/SessionStore.js +186 -0
- package/dist/services/session/index.js +3 -0
- package/dist/services/skills.js +34 -0
- package/dist/services/systemPrompt.js +150 -0
- package/dist/services/task/PushTokenStore.js +124 -0
- package/dist/services/task/TaskStore.js +143 -0
- package/dist/services/tools/calculator.js +27 -0
- package/dist/services/tools/edit.js +318 -0
- package/dist/services/tools/exec.js +119 -0
- package/dist/services/tools/fetch.js +155 -0
- package/dist/services/tools/file.js +315 -0
- package/dist/services/tools/index.js +48 -0
- package/dist/services/tools/keyboard.js +145 -0
- package/dist/services/tools/model.js +86 -0
- package/dist/services/tools/mouse.js +55 -0
- package/dist/services/tools/screenshot.js +19 -0
- package/dist/services/tools/search.js +53 -0
- package/dist/services/tools/skill.js +108 -0
- package/dist/services/tools/task.js +110 -0
- package/dist/services/tools/types.js +1 -0
- package/dist/services/tools/webFetch.js +34 -0
- package/dist/stores/authStore.js +178 -0
- package/dist/stores/index.js +6 -0
- package/dist/stores/memoryStore.js +191 -0
- package/dist/stores/persistStore.js +317 -0
- package/package.json +94 -0
|
@@ -0,0 +1,724 @@
|
|
|
1
|
+
import { persistStore } from "@/stores";
|
|
2
|
+
import { getLogger } from '@pocketclaw/shared';
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
|
+
const logger = getLogger('LLMClient');
|
|
5
|
+
/**
|
|
6
|
+
* LLM 调用器类
|
|
7
|
+
* 提供 Chat 和 streamChat 两种调用方式
|
|
8
|
+
*/
|
|
9
|
+
export class LLMClient {
|
|
10
|
+
models;
|
|
11
|
+
messages;
|
|
12
|
+
tools;
|
|
13
|
+
preferredModelId;
|
|
14
|
+
signal;
|
|
15
|
+
timeoutMs = 60000;
|
|
16
|
+
constructor(models, messages, signal, tools) {
|
|
17
|
+
this.models = models;
|
|
18
|
+
this.messages = messages;
|
|
19
|
+
this.tools = tools;
|
|
20
|
+
this.signal = signal;
|
|
21
|
+
}
|
|
22
|
+
getActionName(params) {
|
|
23
|
+
const args = parseAruments(params);
|
|
24
|
+
return args?.action;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* 设置首选模型
|
|
28
|
+
*/
|
|
29
|
+
setPreferredModel(modelId) {
|
|
30
|
+
this.preferredModelId = modelId;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 非流式 Chat 调用
|
|
34
|
+
* 返回完整的 content、reasoningContent 和 toolCalls
|
|
35
|
+
*/
|
|
36
|
+
async Chat() {
|
|
37
|
+
const selector = this.createSelector();
|
|
38
|
+
let lastError;
|
|
39
|
+
selector.resetTriedModels();
|
|
40
|
+
while (true) {
|
|
41
|
+
if (this.signal.aborted) {
|
|
42
|
+
throw Error('aborted');
|
|
43
|
+
}
|
|
44
|
+
const model = selector.getNext();
|
|
45
|
+
if (!model)
|
|
46
|
+
break;
|
|
47
|
+
const controller = new AbortController();
|
|
48
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
49
|
+
try {
|
|
50
|
+
const request = await this.buildRequest(model, false);
|
|
51
|
+
// 组合外部 signal 和内部 controller.signal,任一 abort 都取消请求
|
|
52
|
+
const combinedSignal = this.signal
|
|
53
|
+
? (this.signal.aborted ? this.signal : AbortSignal.any([this.signal, controller.signal]))
|
|
54
|
+
: controller.signal;
|
|
55
|
+
const response = await fetch(request.url, {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers: request.headers,
|
|
58
|
+
body: JSON.stringify(request.body),
|
|
59
|
+
signal: combinedSignal,
|
|
60
|
+
});
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
const errorText = await response.text();
|
|
63
|
+
logger.error(errorText);
|
|
64
|
+
throw new Error(`${response.status} - ${errorText}`);
|
|
65
|
+
}
|
|
66
|
+
const responseText = await response.text();
|
|
67
|
+
const data = responseText ? JSON.parse(responseText) : {};
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
return this.parseResponse(data, model.modelName);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (error.message === 'exceed max message tokens') {
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
selector.markTried(model.id);
|
|
76
|
+
clearTimeout(timer);
|
|
77
|
+
lastError = error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
throw lastError || new Error('All models are unavailable or has been aborted');
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* 流式 streamChat 调用
|
|
84
|
+
* 通过回调函数推送 content 和 reasoningContent
|
|
85
|
+
*/
|
|
86
|
+
async streamChat(callbacks) {
|
|
87
|
+
const selector = this.createSelector();
|
|
88
|
+
selector.resetTriedModels();
|
|
89
|
+
while (selector.hasNext()) {
|
|
90
|
+
if (this.signal.aborted) {
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
const model = selector.getNext();
|
|
94
|
+
if (!model)
|
|
95
|
+
break;
|
|
96
|
+
try {
|
|
97
|
+
await this.streamRequest(model, callbacks);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
selector.markTried(model.id);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
throw new Error('All models are unavailable');
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* 创建模型选择器(全局单例)
|
|
108
|
+
*/
|
|
109
|
+
createSelector() {
|
|
110
|
+
const selector = ModelSelectorWrapper.getInstance(this.models, this.preferredModelId);
|
|
111
|
+
selector.resetTriedModels();
|
|
112
|
+
return selector;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* 构建请求
|
|
116
|
+
*/
|
|
117
|
+
async buildRequest(model, stream) {
|
|
118
|
+
const apiKey = persistStore.getModelApiKey(model.id);
|
|
119
|
+
if (!apiKey) {
|
|
120
|
+
throw new Error('No API Key');
|
|
121
|
+
}
|
|
122
|
+
if (model.baseUrl.includes('coze.com')) {
|
|
123
|
+
// Coze API: 使用 query 字段,只支持文本
|
|
124
|
+
const lastMessage = this.messages[this.messages.length - 1];
|
|
125
|
+
let queryContent = lastMessage?.content || '';
|
|
126
|
+
// 如果有附件,在查询中添加附件信息
|
|
127
|
+
if (lastMessage?.attachments && lastMessage.attachments.length > 0) {
|
|
128
|
+
const attachmentInfos = lastMessage.attachments.map(a => `[${a.type === 'image' ? '图片' : '文件'}: ${a.name}](${a.url})`).join('\n');
|
|
129
|
+
queryContent += `\n\n${attachmentInfos}`;
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
url: `${model.baseUrl}/chat`,
|
|
133
|
+
headers: {
|
|
134
|
+
'Content-Type': 'application/json',
|
|
135
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
136
|
+
},
|
|
137
|
+
body: {
|
|
138
|
+
bot_id: model.modelId,
|
|
139
|
+
conversation_id: '', // 非会话模式
|
|
140
|
+
user: '',
|
|
141
|
+
query: queryContent,
|
|
142
|
+
stream,
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
else if (model.baseUrl.includes('anthropic.com')) {
|
|
147
|
+
return {
|
|
148
|
+
url: `${model.baseUrl}/v1/messages`,
|
|
149
|
+
headers: {
|
|
150
|
+
'Content-Type': 'application/json',
|
|
151
|
+
'x-api-key': apiKey || '',
|
|
152
|
+
'anthropic-version': '2023-06-01',
|
|
153
|
+
'anthropic-dangerous-direct-browser-access': 'true',
|
|
154
|
+
},
|
|
155
|
+
body: {
|
|
156
|
+
model: model.modelId,
|
|
157
|
+
messages: this.messages.filter((m) => m.role !== 'system').map((m) => {
|
|
158
|
+
// 处理工具消息
|
|
159
|
+
if (m.role === 'tool') {
|
|
160
|
+
return {
|
|
161
|
+
role: 'user',
|
|
162
|
+
content: [
|
|
163
|
+
{
|
|
164
|
+
type: 'tool_result',
|
|
165
|
+
tool_use_id: m.tool_call_id,
|
|
166
|
+
content: m.content,
|
|
167
|
+
}
|
|
168
|
+
],
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
// 处理用户消息中的附件
|
|
172
|
+
if (m.role === 'user' && m.attachments && m.attachments.length > 0) {
|
|
173
|
+
const contentArray = [];
|
|
174
|
+
// 如果有文本内容,先添加文本
|
|
175
|
+
if (m.content && m.content.trim()) {
|
|
176
|
+
contentArray.push({
|
|
177
|
+
type: 'text',
|
|
178
|
+
text: m.content,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
// 添加附件
|
|
182
|
+
for (const attachment of m.attachments) {
|
|
183
|
+
if (attachment.type === 'image') {
|
|
184
|
+
// Anthropic 支持 base64 图片
|
|
185
|
+
// 假设 attachment.url 可能是 base64 格式或 URL 格式
|
|
186
|
+
if (attachment.url.startsWith('data:')) {
|
|
187
|
+
// base64 格式
|
|
188
|
+
const base64Data = attachment.url.split(',')[1];
|
|
189
|
+
const mimeType = attachment.mimeType || 'image/png';
|
|
190
|
+
contentArray.push({
|
|
191
|
+
type: 'image',
|
|
192
|
+
source: {
|
|
193
|
+
type: 'base64',
|
|
194
|
+
media_type: mimeType,
|
|
195
|
+
data: base64Data,
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
// URL 格式 - Anthropic 也支持
|
|
201
|
+
contentArray.push({
|
|
202
|
+
type: 'image',
|
|
203
|
+
source: {
|
|
204
|
+
type: 'url',
|
|
205
|
+
media_type: attachment.mimeType || 'image/png',
|
|
206
|
+
url: attachment.url,
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
// 其他文件类型 - Anthropic 不直接支持,添加为文本引用
|
|
213
|
+
const fileInfo = `[附件: ${attachment.name}](${attachment.url})`;
|
|
214
|
+
if (contentArray.length === 0) {
|
|
215
|
+
contentArray.push({
|
|
216
|
+
type: 'text',
|
|
217
|
+
text: fileInfo,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
const lastItem = contentArray[contentArray.length - 1];
|
|
222
|
+
if (lastItem.type === 'text') {
|
|
223
|
+
lastItem.text += `\n${fileInfo}`;
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
contentArray.push({
|
|
227
|
+
type: 'text',
|
|
228
|
+
text: fileInfo,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
role: 'user',
|
|
236
|
+
content: contentArray,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
// 普通用户消息(无附件)
|
|
240
|
+
return {
|
|
241
|
+
role: m.role,
|
|
242
|
+
content: m.content,
|
|
243
|
+
};
|
|
244
|
+
}),
|
|
245
|
+
stream,
|
|
246
|
+
max_tokens: model.maxTokens || 4096,
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
// OpenAI compatible API
|
|
252
|
+
const body = {
|
|
253
|
+
model: model.modelId,
|
|
254
|
+
messages: this.messages.map((m) => {
|
|
255
|
+
if (m.role === 'tool') {
|
|
256
|
+
return [{
|
|
257
|
+
role: 'tool',
|
|
258
|
+
tool_call_id: m.tool_call_id,
|
|
259
|
+
content: m.content,
|
|
260
|
+
}];
|
|
261
|
+
}
|
|
262
|
+
// 助手调用工具的消息:必须保留 tool_calls!!(你之前丢了,导致报错)
|
|
263
|
+
if (m.role === 'assistant' && m.tool_calls) {
|
|
264
|
+
return [{
|
|
265
|
+
role: 'assistant',
|
|
266
|
+
content: m.content || null,
|
|
267
|
+
tool_calls: m.tool_calls.map(m => {
|
|
268
|
+
return {
|
|
269
|
+
id: m.id,
|
|
270
|
+
function: {
|
|
271
|
+
name: m.toolName,
|
|
272
|
+
arguments: m.input
|
|
273
|
+
},
|
|
274
|
+
type: 'function'
|
|
275
|
+
};
|
|
276
|
+
}),
|
|
277
|
+
}];
|
|
278
|
+
}
|
|
279
|
+
if (m.role === 'assistant' && m.toolCalls) {
|
|
280
|
+
let items = [];
|
|
281
|
+
for (let toolCall of m.toolCalls) {
|
|
282
|
+
const item = {
|
|
283
|
+
role: 'assistant',
|
|
284
|
+
content: toolCall.content,
|
|
285
|
+
reasoning_content: toolCall.reasoningContent,
|
|
286
|
+
tool_calls: []
|
|
287
|
+
};
|
|
288
|
+
const tools = [];
|
|
289
|
+
for (let i = 0; i < toolCall.toolCalls.length; i++) {
|
|
290
|
+
const toolCallItem = toolCall.toolCalls[i];
|
|
291
|
+
if (toolCallItem.output != null) {
|
|
292
|
+
item.tool_calls.push({
|
|
293
|
+
id: toolCallItem.id,
|
|
294
|
+
function: {
|
|
295
|
+
name: toolCallItem.toolName,
|
|
296
|
+
arguments: toolCallItem.input
|
|
297
|
+
},
|
|
298
|
+
type: 'function'
|
|
299
|
+
});
|
|
300
|
+
tools.push({
|
|
301
|
+
role: 'tool',
|
|
302
|
+
tool_call_id: toolCallItem.id,
|
|
303
|
+
content: toolCallItem.output.substring(0, 100) + '...' + '(内容过长已截断)'
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (item.tool_calls.length > 0) {
|
|
308
|
+
items.push(item);
|
|
309
|
+
items = items.concat(tools);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (m.content) {
|
|
313
|
+
items.push({
|
|
314
|
+
role: 'assistant',
|
|
315
|
+
content: m.content
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
return items;
|
|
319
|
+
}
|
|
320
|
+
// 处理用户消息中的附件
|
|
321
|
+
if (m.role === 'user' && m.attachments && m.attachments.length > 0) {
|
|
322
|
+
const contentArray = [];
|
|
323
|
+
// 如果有文本内容,先添加文本
|
|
324
|
+
if (m.content && m.content.trim()) {
|
|
325
|
+
contentArray.push({
|
|
326
|
+
type: 'text',
|
|
327
|
+
text: m.content,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
// 添加附件
|
|
331
|
+
for (const attachment of m.attachments) {
|
|
332
|
+
if (attachment.type === 'image') {
|
|
333
|
+
// 图片附件
|
|
334
|
+
contentArray.push({
|
|
335
|
+
type: 'image_url',
|
|
336
|
+
image_url: {
|
|
337
|
+
url: attachment.url,
|
|
338
|
+
detail: 'high',
|
|
339
|
+
},
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
else if (attachment.type === 'audio') {
|
|
343
|
+
// 音频附件 - 使用 file 类型
|
|
344
|
+
contentArray.push({
|
|
345
|
+
type: 'input_audio',
|
|
346
|
+
input_audio: {
|
|
347
|
+
data: attachment.url, // 如果是 base64 格式
|
|
348
|
+
format: attachment.mimeType?.split('/')[1] || 'wav',
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
// 其他文件类型:video, file - 添加为文本引用
|
|
354
|
+
const fileInfo = `[附件: ${attachment.name}](${attachment.url})`;
|
|
355
|
+
if (contentArray.length === 0) {
|
|
356
|
+
contentArray.push({
|
|
357
|
+
type: 'text',
|
|
358
|
+
text: fileInfo,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
// 如果已经有文本,将文件信息追加到文本中
|
|
363
|
+
const lastItem = contentArray[contentArray.length - 1];
|
|
364
|
+
if (lastItem.type === 'text') {
|
|
365
|
+
lastItem.text += `\n${fileInfo}`;
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
contentArray.push({
|
|
369
|
+
type: 'text',
|
|
370
|
+
text: fileInfo,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return [{
|
|
377
|
+
role: m.role,
|
|
378
|
+
content: contentArray,
|
|
379
|
+
}];
|
|
380
|
+
}
|
|
381
|
+
// 普通消息(无附件)
|
|
382
|
+
return [{
|
|
383
|
+
role: m.role,
|
|
384
|
+
content: m.content,
|
|
385
|
+
}];
|
|
386
|
+
}).flatMap(x => x),
|
|
387
|
+
stream,
|
|
388
|
+
enable_thinking: true,
|
|
389
|
+
};
|
|
390
|
+
if (this.tools && this.tools.length > 0) {
|
|
391
|
+
body.tools = this.tools;
|
|
392
|
+
}
|
|
393
|
+
const tokens = JSON.stringify(body).length;
|
|
394
|
+
logger.debug('tokens', tokens);
|
|
395
|
+
if (tokens > 100000) {
|
|
396
|
+
logger.warn('message is too large and has writen to the file debug.json');
|
|
397
|
+
await fs.writeFile('debug.json', JSON.stringify(body.messages), 'utf-8');
|
|
398
|
+
throw Error('exceed max message tokens');
|
|
399
|
+
}
|
|
400
|
+
return {
|
|
401
|
+
url: `${model.baseUrl}/chat/completions`,
|
|
402
|
+
headers: {
|
|
403
|
+
'Content-Type': 'application/json',
|
|
404
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
405
|
+
},
|
|
406
|
+
body,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* 执行流式请求
|
|
412
|
+
*/
|
|
413
|
+
async streamRequest(model, callbacks) {
|
|
414
|
+
const request = await this.buildRequest(model, true);
|
|
415
|
+
const response = await fetch(request.url, {
|
|
416
|
+
method: 'POST',
|
|
417
|
+
headers: request.headers,
|
|
418
|
+
body: JSON.stringify(request.body),
|
|
419
|
+
signal: this.signal
|
|
420
|
+
});
|
|
421
|
+
if (!response.ok) {
|
|
422
|
+
const errorText = await response.text();
|
|
423
|
+
throw new Error(`${response.status} - ${errorText}`);
|
|
424
|
+
}
|
|
425
|
+
const reader = response.body?.getReader();
|
|
426
|
+
if (!reader) {
|
|
427
|
+
throw new Error('Response body is not readable');
|
|
428
|
+
}
|
|
429
|
+
const decoder = new TextDecoder();
|
|
430
|
+
let buffer = '';
|
|
431
|
+
let reasoningContent = '';
|
|
432
|
+
// 解析 SSE 行
|
|
433
|
+
const parseSSELine = (line) => {
|
|
434
|
+
if (line.startsWith('data: ')) {
|
|
435
|
+
return { type: 'data', data: line.slice(6) };
|
|
436
|
+
}
|
|
437
|
+
return null;
|
|
438
|
+
};
|
|
439
|
+
// 解析增量内容
|
|
440
|
+
const parseDelta = (data) => {
|
|
441
|
+
// Coze 格式
|
|
442
|
+
if (data.type === 'conversation_message.delta') {
|
|
443
|
+
return { content: data.message?.content?.[0]?.text || '' };
|
|
444
|
+
}
|
|
445
|
+
if (data.type === 'conversation_message.completed') {
|
|
446
|
+
if (data.message?.thinking) {
|
|
447
|
+
reasoningContent = data.message.thinking;
|
|
448
|
+
return { reasoningContent };
|
|
449
|
+
}
|
|
450
|
+
return { content: data.message?.content?.[0]?.text || '' };
|
|
451
|
+
}
|
|
452
|
+
// OpenAI/DeepSeek 格式
|
|
453
|
+
if (data.choices?.[0]?.delta) {
|
|
454
|
+
const delta = data.choices[0].delta;
|
|
455
|
+
// reasoning_content (DeepSeek)
|
|
456
|
+
if (delta.reasoning_content) {
|
|
457
|
+
return { reasoningContent: delta.reasoning_content };
|
|
458
|
+
}
|
|
459
|
+
// content
|
|
460
|
+
if (delta.content) {
|
|
461
|
+
return { content: delta.content };
|
|
462
|
+
}
|
|
463
|
+
// tool_calls
|
|
464
|
+
if (delta.tool_calls) {
|
|
465
|
+
return {
|
|
466
|
+
toolCalls: delta.tool_calls.map((tc) => ({
|
|
467
|
+
id: tc.id || '',
|
|
468
|
+
name: tc.function?.name || '',
|
|
469
|
+
arguments: tc.function?.arguments || '',
|
|
470
|
+
})),
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
// Claude 格式
|
|
475
|
+
if (data.type === 'message_delta') {
|
|
476
|
+
return {};
|
|
477
|
+
}
|
|
478
|
+
if (data.type === 'content_block_start') {
|
|
479
|
+
if (data.content_block?.type === 'thinking') {
|
|
480
|
+
return {};
|
|
481
|
+
}
|
|
482
|
+
if (data.content_block?.type === 'text') {
|
|
483
|
+
return {};
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
if (data.type === 'content_block_delta') {
|
|
487
|
+
if (data.delta?.type === 'thinking') {
|
|
488
|
+
return { reasoningContent: data.delta.thinking };
|
|
489
|
+
}
|
|
490
|
+
if (data.delta?.type === 'text_delta') {
|
|
491
|
+
return { content: data.delta.text };
|
|
492
|
+
}
|
|
493
|
+
if (data.delta?.type === 'tool_use') {
|
|
494
|
+
return {
|
|
495
|
+
toolCalls: [{
|
|
496
|
+
id: data.delta.id || '',
|
|
497
|
+
toolName: data.delta.name || '',
|
|
498
|
+
input: data.delta.input || '',
|
|
499
|
+
}],
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return {};
|
|
504
|
+
};
|
|
505
|
+
try {
|
|
506
|
+
while (true) {
|
|
507
|
+
const { done, value } = await reader.read();
|
|
508
|
+
if (done)
|
|
509
|
+
break;
|
|
510
|
+
buffer += decoder.decode(value, { stream: true });
|
|
511
|
+
const lines = buffer.split('\n');
|
|
512
|
+
buffer = lines.pop() || '';
|
|
513
|
+
for (const line of lines) {
|
|
514
|
+
const parsed = parseSSELine(line);
|
|
515
|
+
if (!parsed || parsed.data === '[DONE]')
|
|
516
|
+
continue;
|
|
517
|
+
try {
|
|
518
|
+
const data = JSON.parse(parsed.data);
|
|
519
|
+
const result = parseDelta(data);
|
|
520
|
+
if (result.content) {
|
|
521
|
+
callbacks.onContent?.(result.content);
|
|
522
|
+
}
|
|
523
|
+
if (result.reasoningContent) {
|
|
524
|
+
reasoningContent += result.reasoningContent;
|
|
525
|
+
callbacks.onReasoningContent?.(result.reasoningContent);
|
|
526
|
+
}
|
|
527
|
+
if (result.toolCalls) {
|
|
528
|
+
for (const tc of result.toolCalls) {
|
|
529
|
+
callbacks.onToolCall?.(tc);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
catch (e) {
|
|
534
|
+
// Ignore parse errors
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
finally {
|
|
540
|
+
reader.releaseLock();
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* 解析非流式响应
|
|
545
|
+
*/
|
|
546
|
+
parseResponse(data, modelName) {
|
|
547
|
+
// Coze 格式
|
|
548
|
+
if (data.type === 'conversation_message.completed' || data.message) {
|
|
549
|
+
const messageData = data.message || {};
|
|
550
|
+
return {
|
|
551
|
+
content: messageData.content?.[0]?.text || '',
|
|
552
|
+
reasoningContent: messageData.thinking || undefined,
|
|
553
|
+
modelName
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
// OpenAI/DeepSeek 格式
|
|
557
|
+
if (data.choices?.[0]?.message) {
|
|
558
|
+
const messageData = data.choices[0].message;
|
|
559
|
+
return {
|
|
560
|
+
content: messageData.content || '',
|
|
561
|
+
reasoningContent: messageData.reasoning_content || undefined,
|
|
562
|
+
toolCalls: messageData.tool_calls?.map((tc) => ({
|
|
563
|
+
id: tc.id,
|
|
564
|
+
toolName: tc.function?.name,
|
|
565
|
+
input: tc.function?.arguments,
|
|
566
|
+
actionName: this.getActionName(tc.function?.arguments)
|
|
567
|
+
})),
|
|
568
|
+
modelName
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
// Claude 格式
|
|
572
|
+
if (data.content) {
|
|
573
|
+
const toolCalls = [];
|
|
574
|
+
let content = '';
|
|
575
|
+
let reasoningContent = '';
|
|
576
|
+
for (const block of data.content) {
|
|
577
|
+
if (block.type === 'text') {
|
|
578
|
+
content += block.text;
|
|
579
|
+
}
|
|
580
|
+
else if (block.type === 'thinking') {
|
|
581
|
+
reasoningContent += block.thinking;
|
|
582
|
+
}
|
|
583
|
+
else if (block.type === 'tool_use') {
|
|
584
|
+
toolCalls.push({
|
|
585
|
+
id: block.id,
|
|
586
|
+
toolName: block.name,
|
|
587
|
+
input: typeof block.input === 'string' ? block.input : JSON.stringify(block.input),
|
|
588
|
+
actionName: this.getActionName(typeof block.input === 'string' ? block.input : JSON.stringify(block.input))
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return {
|
|
593
|
+
content,
|
|
594
|
+
reasoningContent: reasoningContent || undefined,
|
|
595
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
596
|
+
modelName
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
return { content: '', modelName };
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* 检查是否是可用性错误
|
|
603
|
+
*/
|
|
604
|
+
isAvailabilityError(error) {
|
|
605
|
+
if (!error)
|
|
606
|
+
return false;
|
|
607
|
+
const errorStr = String(error);
|
|
608
|
+
const availabilityCodes = [
|
|
609
|
+
'ECONNREFUSED',
|
|
610
|
+
'ETIMEDOUT',
|
|
611
|
+
'ENOTFOUND',
|
|
612
|
+
'429',
|
|
613
|
+
'500',
|
|
614
|
+
'502',
|
|
615
|
+
'503',
|
|
616
|
+
'504',
|
|
617
|
+
'insufficient_quota',
|
|
618
|
+
'rate_limit_exceeded',
|
|
619
|
+
'model_not_found',
|
|
620
|
+
'context_length_exceeded',
|
|
621
|
+
'Model call failed',
|
|
622
|
+
'No API Key',
|
|
623
|
+
'InvalidEndpointOrModel',
|
|
624
|
+
'AuthenticationError',
|
|
625
|
+
"model service has been paused",
|
|
626
|
+
"SetLimitExceeded"
|
|
627
|
+
];
|
|
628
|
+
return availabilityCodes.some(code => errorStr.includes(code));
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* 模型选择器包装器(全局单例)
|
|
633
|
+
*/
|
|
634
|
+
let modelSelectorInstance = null;
|
|
635
|
+
class ModelSelectorWrapper {
|
|
636
|
+
models;
|
|
637
|
+
triedModels;
|
|
638
|
+
preferredModelId;
|
|
639
|
+
scoreModels;
|
|
640
|
+
constructor(models, preferredModelId) {
|
|
641
|
+
this.models = models;
|
|
642
|
+
this.preferredModelId = preferredModelId;
|
|
643
|
+
this.triedModels = new Map();
|
|
644
|
+
this.scoreModels = new Map();
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* 获取全局单例实例
|
|
648
|
+
*/
|
|
649
|
+
static getInstance(models, preferredModelId) {
|
|
650
|
+
if (!modelSelectorInstance) {
|
|
651
|
+
if (!models) {
|
|
652
|
+
throw new Error('Models are required for initializing ModelSelectorWrapper');
|
|
653
|
+
}
|
|
654
|
+
modelSelectorInstance = new ModelSelectorWrapper(models, preferredModelId);
|
|
655
|
+
}
|
|
656
|
+
else if (models) {
|
|
657
|
+
// 传入新参数时更新实例配置
|
|
658
|
+
modelSelectorInstance.models = models;
|
|
659
|
+
if (preferredModelId) {
|
|
660
|
+
modelSelectorInstance.preferredModelId = preferredModelId;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return modelSelectorInstance;
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* 重置模型尝试记录
|
|
667
|
+
*/
|
|
668
|
+
resetTriedModels() {
|
|
669
|
+
this.triedModels.clear();
|
|
670
|
+
}
|
|
671
|
+
//模型调用次数小于2均可再次调用
|
|
672
|
+
canModelTry(id) {
|
|
673
|
+
return !this.triedModels.has(id) || this.triedModels.get(id) < 2;
|
|
674
|
+
}
|
|
675
|
+
getScore(model) {
|
|
676
|
+
return model.score + (this.scoreModels.has(model.id) ? this.scoreModels.get(model.id) : 0) + (model.isPrimary ? 1 : 0);
|
|
677
|
+
}
|
|
678
|
+
getNext() {
|
|
679
|
+
// 优先返回首选模型
|
|
680
|
+
if (this.preferredModelId && this.canModelTry(this.preferredModelId)) {
|
|
681
|
+
const preferred = this.models.find((m) => m.id === this.preferredModelId && m.isActive);
|
|
682
|
+
if (preferred)
|
|
683
|
+
return preferred;
|
|
684
|
+
}
|
|
685
|
+
// 按评分选择
|
|
686
|
+
const availableModels = this.models.filter((m) => m.isActive && this.canModelTry(m.id));
|
|
687
|
+
if (availableModels.length === 0)
|
|
688
|
+
return null;
|
|
689
|
+
availableModels.sort((a, b) => this.getScore(b) - this.getScore(a));
|
|
690
|
+
return availableModels[0] || null;
|
|
691
|
+
}
|
|
692
|
+
markTried(id) {
|
|
693
|
+
if (this.triedModels.has(id)) {
|
|
694
|
+
this.triedModels.set(id, this.triedModels.get(id) + 1);
|
|
695
|
+
}
|
|
696
|
+
else {
|
|
697
|
+
this.triedModels.set(id, 1);
|
|
698
|
+
}
|
|
699
|
+
if (this.scoreModels.has(id)) {
|
|
700
|
+
this.scoreModels.set(id, this.scoreModels.get(id) - 1);
|
|
701
|
+
}
|
|
702
|
+
else {
|
|
703
|
+
this.scoreModels.set(id, -1);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
hasNext() {
|
|
707
|
+
return this.getNext() !== null;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
export function parseAruments(params) {
|
|
711
|
+
try {
|
|
712
|
+
return typeof params === 'string' ? JSON.parse(params) : params;
|
|
713
|
+
}
|
|
714
|
+
catch {
|
|
715
|
+
return params;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* 全局模型选择器单例访问入口
|
|
720
|
+
* @param models 模型列表(首次初始化时必填)
|
|
721
|
+
* @param preferredModelId 首选模型ID
|
|
722
|
+
* @returns 全局唯一的模型选择器实例
|
|
723
|
+
*/
|
|
724
|
+
export const getGlobalModelSelector = ModelSelectorWrapper.getInstance;
|