@myassis/gateway 1.0.28 → 1.0.30
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.
|
@@ -8,6 +8,13 @@ const index_js_1 = require("../../stores/index.js");
|
|
|
8
8
|
const shared_1 = require("@myassis/shared");
|
|
9
9
|
const promises_1 = __importDefault(require("fs/promises"));
|
|
10
10
|
const logger = (0, shared_1.getLogger)('LLMClient');
|
|
11
|
+
function convertBase64ToImage(base64Img) {
|
|
12
|
+
// 去掉data:image/xxx;base64,前缀
|
|
13
|
+
const pureBase64 = base64Img.replace(/^data:image\/\w+;base64,/, '');
|
|
14
|
+
const buf = Buffer.from(pureBase64, 'base64');
|
|
15
|
+
// 写入文件
|
|
16
|
+
promises_1.default.writeFile('screenshot.png', buf);
|
|
17
|
+
}
|
|
11
18
|
/**
|
|
12
19
|
* LLM 调用器类
|
|
13
20
|
* 提供 Chat 和 streamChat 两种调用方式
|
|
@@ -18,7 +25,7 @@ class LLMClient {
|
|
|
18
25
|
tools;
|
|
19
26
|
preferredModelId;
|
|
20
27
|
signal;
|
|
21
|
-
timeoutMs =
|
|
28
|
+
timeoutMs = 120000;
|
|
22
29
|
constructor(models, messages, signal, tools) {
|
|
23
30
|
this.models = models;
|
|
24
31
|
this.messages = messages;
|
|
@@ -80,7 +87,12 @@ class LLMClient {
|
|
|
80
87
|
}
|
|
81
88
|
selector.markTried(model.id);
|
|
82
89
|
clearTimeout(timer);
|
|
83
|
-
|
|
90
|
+
if (controller.signal.aborted) {
|
|
91
|
+
lastError = new Error('Request timed out');
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
lastError = error;
|
|
95
|
+
}
|
|
84
96
|
}
|
|
85
97
|
}
|
|
86
98
|
throw lastError || new Error('All models are unavailable or has been aborted');
|
|
@@ -125,295 +137,189 @@ class LLMClient {
|
|
|
125
137
|
if (!apiKey) {
|
|
126
138
|
throw new Error('No API Key');
|
|
127
139
|
}
|
|
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
|
-
else if (model.baseUrl.includes('anthropic.com')) {
|
|
153
|
-
return {
|
|
154
|
-
url: `${model.baseUrl}/v1/messages`,
|
|
155
|
-
headers: {
|
|
156
|
-
'Content-Type': 'application/json',
|
|
157
|
-
'x-api-key': apiKey || '',
|
|
158
|
-
'anthropic-version': '2023-06-01',
|
|
159
|
-
'anthropic-dangerous-direct-browser-access': 'true',
|
|
160
|
-
},
|
|
161
|
-
body: {
|
|
162
|
-
model: model.modelId,
|
|
163
|
-
messages: this.messages.filter((m) => m.role !== 'system').map((m) => {
|
|
164
|
-
// 处理工具消息
|
|
165
|
-
if (m.role === 'tool') {
|
|
166
|
-
return {
|
|
167
|
-
role: 'user',
|
|
168
|
-
content: [
|
|
169
|
-
{
|
|
170
|
-
type: 'tool_result',
|
|
171
|
-
tool_use_id: m.tool_call_id,
|
|
172
|
-
content: m.content,
|
|
173
|
-
}
|
|
174
|
-
],
|
|
175
|
-
};
|
|
140
|
+
// OpenAI compatible API
|
|
141
|
+
let existAttachments = false;
|
|
142
|
+
let existScreenShot = false;
|
|
143
|
+
const body = {
|
|
144
|
+
model: model.modelId,
|
|
145
|
+
messages: this.messages.map((m, index) => {
|
|
146
|
+
if (m.role === 'tool') {
|
|
147
|
+
if (m.tool_call_name == 'screenshot') {
|
|
148
|
+
if (index === this.messages.length - 1) {
|
|
149
|
+
existScreenShot = true;
|
|
150
|
+
const contentObj = JSON.parse(m.content);
|
|
151
|
+
return [{
|
|
152
|
+
role: 'tool',
|
|
153
|
+
tool_call_id: m.tool_call_id,
|
|
154
|
+
content: JSON.stringify({
|
|
155
|
+
success: contentObj.success,
|
|
156
|
+
result_type: 'image',
|
|
157
|
+
image_url: contentObj.output,
|
|
158
|
+
text_for_llm: '工具生成了一张图片,图片地址已返回。'
|
|
159
|
+
}),
|
|
160
|
+
}];
|
|
176
161
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
type: 'text',
|
|
184
|
-
text: m.content,
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
// 添加附件
|
|
188
|
-
for (const attachment of m.attachments) {
|
|
189
|
-
if (attachment.type === 'image') {
|
|
190
|
-
// Anthropic 支持 base64 图片
|
|
191
|
-
// 假设 attachment.url 可能是 base64 格式或 URL 格式
|
|
192
|
-
if (attachment.url.startsWith('data:')) {
|
|
193
|
-
// base64 格式
|
|
194
|
-
const base64Data = attachment.url.split(',')[1];
|
|
195
|
-
const mimeType = attachment.mimeType || 'image/png';
|
|
196
|
-
contentArray.push({
|
|
197
|
-
type: 'image',
|
|
198
|
-
source: {
|
|
199
|
-
type: 'base64',
|
|
200
|
-
media_type: mimeType,
|
|
201
|
-
data: base64Data,
|
|
202
|
-
},
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
else {
|
|
206
|
-
// URL 格式 - Anthropic 也支持
|
|
207
|
-
contentArray.push({
|
|
208
|
-
type: 'image',
|
|
209
|
-
source: {
|
|
210
|
-
type: 'url',
|
|
211
|
-
media_type: attachment.mimeType || 'image/png',
|
|
212
|
-
url: attachment.url,
|
|
213
|
-
},
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
else {
|
|
218
|
-
// 其他文件类型 - Anthropic 不直接支持,添加为文本引用
|
|
219
|
-
const fileInfo = `[附件: ${attachment.name}](${attachment.url})`;
|
|
220
|
-
if (contentArray.length === 0) {
|
|
221
|
-
contentArray.push({
|
|
222
|
-
type: 'text',
|
|
223
|
-
text: fileInfo,
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
else {
|
|
227
|
-
const lastItem = contentArray[contentArray.length - 1];
|
|
228
|
-
if (lastItem.type === 'text') {
|
|
229
|
-
lastItem.text += `\n${fileInfo}`;
|
|
230
|
-
}
|
|
231
|
-
else {
|
|
232
|
-
contentArray.push({
|
|
233
|
-
type: 'text',
|
|
234
|
-
text: fileInfo,
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
return {
|
|
241
|
-
role: 'user',
|
|
242
|
-
content: contentArray,
|
|
243
|
-
};
|
|
162
|
+
else {
|
|
163
|
+
return [{
|
|
164
|
+
role: 'tool',
|
|
165
|
+
tool_call_id: m.tool_call_id,
|
|
166
|
+
content: m.content.substring(0, 100) + '...(内容过长已截断)',
|
|
167
|
+
}];
|
|
244
168
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
role: m.role,
|
|
248
|
-
content: m.content,
|
|
249
|
-
};
|
|
250
|
-
}),
|
|
251
|
-
stream,
|
|
252
|
-
max_tokens: model.maxTokens || 4096,
|
|
253
|
-
},
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
else {
|
|
257
|
-
// OpenAI compatible API
|
|
258
|
-
let existAttachments = false;
|
|
259
|
-
const body = {
|
|
260
|
-
model: model.modelId,
|
|
261
|
-
messages: this.messages.map((m) => {
|
|
262
|
-
if (m.role === 'tool') {
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
263
171
|
return [{
|
|
264
172
|
role: 'tool',
|
|
265
173
|
tool_call_id: m.tool_call_id,
|
|
266
174
|
content: m.content,
|
|
267
175
|
}];
|
|
268
176
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
if (item.tool_calls.length > 0) {
|
|
315
|
-
items.push(item);
|
|
316
|
-
items = items.concat(tools);
|
|
177
|
+
}
|
|
178
|
+
// 助手调用工具的消息:必须保留 tool_calls!!(你之前丢了,导致报错)
|
|
179
|
+
if (m.role === 'assistant' && m.tool_calls) {
|
|
180
|
+
return [{
|
|
181
|
+
role: 'assistant',
|
|
182
|
+
content: m.content || null,
|
|
183
|
+
tool_calls: m.tool_calls.map(m => {
|
|
184
|
+
return {
|
|
185
|
+
id: m.id,
|
|
186
|
+
function: {
|
|
187
|
+
name: m.toolName,
|
|
188
|
+
arguments: m.input
|
|
189
|
+
},
|
|
190
|
+
type: 'function'
|
|
191
|
+
};
|
|
192
|
+
}),
|
|
193
|
+
}];
|
|
194
|
+
}
|
|
195
|
+
if (m.role === 'assistant' && m.toolCalls) {
|
|
196
|
+
let items = [];
|
|
197
|
+
for (let toolCall of m.toolCalls) {
|
|
198
|
+
const item = {
|
|
199
|
+
role: 'assistant',
|
|
200
|
+
content: toolCall.content,
|
|
201
|
+
reasoning_content: toolCall.reasoningContent,
|
|
202
|
+
tool_calls: []
|
|
203
|
+
};
|
|
204
|
+
const tools = [];
|
|
205
|
+
for (let i = 0; i < toolCall.toolCalls.length; i++) {
|
|
206
|
+
const toolCallItem = toolCall.toolCalls[i];
|
|
207
|
+
if (toolCallItem.output != null) {
|
|
208
|
+
item.tool_calls.push({
|
|
209
|
+
id: toolCallItem.id,
|
|
210
|
+
function: {
|
|
211
|
+
name: toolCallItem.toolName,
|
|
212
|
+
arguments: toolCallItem.input
|
|
213
|
+
},
|
|
214
|
+
type: 'function'
|
|
215
|
+
});
|
|
216
|
+
tools.push({
|
|
217
|
+
role: 'tool',
|
|
218
|
+
tool_call_id: toolCallItem.id,
|
|
219
|
+
content: toolCallItem.output.substring(0, 100) + '...' + '(内容过长已截断)'
|
|
220
|
+
});
|
|
317
221
|
}
|
|
318
222
|
}
|
|
319
|
-
if (
|
|
320
|
-
items.push(
|
|
321
|
-
|
|
322
|
-
content: m.content
|
|
323
|
-
});
|
|
223
|
+
if (item.tool_calls.length > 0) {
|
|
224
|
+
items.push(item);
|
|
225
|
+
items = items.concat(tools);
|
|
324
226
|
}
|
|
325
|
-
return items;
|
|
326
227
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
228
|
+
if (m.content) {
|
|
229
|
+
items.push({
|
|
230
|
+
role: 'assistant',
|
|
231
|
+
content: m.content
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
return items;
|
|
235
|
+
}
|
|
236
|
+
// 处理用户消息中的附件
|
|
237
|
+
if (m.role === 'user' && m.attachments && m.attachments.length > 0) {
|
|
238
|
+
existAttachments = true;
|
|
239
|
+
const contentArray = [];
|
|
240
|
+
// 添加附件
|
|
241
|
+
for (const attachment of m.attachments) {
|
|
242
|
+
if (attachment.type === 'image') {
|
|
243
|
+
// 图片附件
|
|
244
|
+
contentArray.push({
|
|
245
|
+
type: 'image_url',
|
|
246
|
+
image_url: {
|
|
247
|
+
url: attachment.url,
|
|
248
|
+
detail: 'high',
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
else if (attachment.type === 'audio') {
|
|
253
|
+
// 音频附件 - 使用 file 类型
|
|
254
|
+
contentArray.push({
|
|
255
|
+
type: 'input_audio',
|
|
256
|
+
input_audio: {
|
|
257
|
+
data: attachment.url, // 如果是 base64 格式
|
|
258
|
+
format: attachment.mimeType?.split('/')[1] || 'wav',
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
// 其他文件类型:video, file - 添加为文本引用
|
|
264
|
+
const fileInfo = `[附件: ${attachment.name}](${attachment.url})`;
|
|
265
|
+
if (contentArray.length === 0) {
|
|
345
266
|
contentArray.push({
|
|
346
|
-
type: '
|
|
347
|
-
|
|
348
|
-
data: attachment.url, // 如果是 base64 格式
|
|
349
|
-
format: attachment.mimeType?.split('/')[1] || 'wav',
|
|
350
|
-
},
|
|
267
|
+
type: 'text',
|
|
268
|
+
text: fileInfo,
|
|
351
269
|
});
|
|
352
270
|
}
|
|
353
271
|
else {
|
|
354
|
-
//
|
|
355
|
-
const
|
|
356
|
-
if (
|
|
272
|
+
// 如果已经有文本,将文件信息追加到文本中
|
|
273
|
+
const lastItem = contentArray[contentArray.length - 1];
|
|
274
|
+
if (lastItem.type === 'text') {
|
|
275
|
+
lastItem.text += `\n${fileInfo}`;
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
357
278
|
contentArray.push({
|
|
358
279
|
type: 'text',
|
|
359
280
|
text: fileInfo,
|
|
360
281
|
});
|
|
361
282
|
}
|
|
362
|
-
else {
|
|
363
|
-
// 如果已经有文本,将文件信息追加到文本中
|
|
364
|
-
const lastItem = contentArray[contentArray.length - 1];
|
|
365
|
-
if (lastItem.type === 'text') {
|
|
366
|
-
lastItem.text += `\n${fileInfo}`;
|
|
367
|
-
}
|
|
368
|
-
else {
|
|
369
|
-
contentArray.push({
|
|
370
|
-
type: 'text',
|
|
371
|
-
text: fileInfo,
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
283
|
}
|
|
376
284
|
}
|
|
377
|
-
// 如果有文本内容,添加文本
|
|
378
|
-
if (m.content && m.content.trim()) {
|
|
379
|
-
contentArray.push({
|
|
380
|
-
type: 'text',
|
|
381
|
-
text: m.content,
|
|
382
|
-
});
|
|
383
|
-
}
|
|
384
|
-
return [{
|
|
385
|
-
role: m.role,
|
|
386
|
-
content: contentArray,
|
|
387
|
-
}];
|
|
388
285
|
}
|
|
389
|
-
//
|
|
286
|
+
// 如果有文本内容,添加文本
|
|
287
|
+
if (m.content && m.content.trim()) {
|
|
288
|
+
contentArray.push({
|
|
289
|
+
type: 'text',
|
|
290
|
+
text: m.content,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
390
293
|
return [{
|
|
391
294
|
role: m.role,
|
|
392
|
-
content:
|
|
295
|
+
content: contentArray,
|
|
393
296
|
}];
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
throw Error('exceed max message tokens');
|
|
407
|
-
}
|
|
408
|
-
return {
|
|
409
|
-
url: `${model.baseUrl}/chat/completions`,
|
|
410
|
-
headers: {
|
|
411
|
-
'Content-Type': 'application/json',
|
|
412
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
413
|
-
},
|
|
414
|
-
body,
|
|
415
|
-
};
|
|
297
|
+
}
|
|
298
|
+
// 普通消息(无附件)
|
|
299
|
+
return [{
|
|
300
|
+
role: m.role,
|
|
301
|
+
content: m.content,
|
|
302
|
+
}];
|
|
303
|
+
}).flatMap(x => x),
|
|
304
|
+
stream,
|
|
305
|
+
enable_thinking: true,
|
|
306
|
+
};
|
|
307
|
+
if (this.tools && this.tools.length > 0) {
|
|
308
|
+
body.tools = this.tools;
|
|
416
309
|
}
|
|
310
|
+
const tokens = JSON.stringify(body).length;
|
|
311
|
+
logger.debug('tokens', tokens);
|
|
312
|
+
if (tokens > 200000 && !existAttachments && !existScreenShot) {
|
|
313
|
+
throw Error('exceed max message tokens');
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
url: `${model.baseUrl}/chat/completions`,
|
|
317
|
+
headers: {
|
|
318
|
+
'Content-Type': 'application/json',
|
|
319
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
320
|
+
},
|
|
321
|
+
body,
|
|
322
|
+
};
|
|
417
323
|
}
|
|
418
324
|
/**
|
|
419
325
|
* 执行流式请求
|
|
@@ -41,16 +41,13 @@ class MemoryManager {
|
|
|
41
41
|
let messages = this.session.getMessages();
|
|
42
42
|
if (this.childAgent) {
|
|
43
43
|
messages.splice(-1);
|
|
44
|
-
logger.debug('history', messages);
|
|
45
44
|
}
|
|
46
45
|
// 2. 消息少于摘要配置的阈值数,直接返回
|
|
47
|
-
logger.debug('message length:', messages.length);
|
|
48
46
|
if (messages.length < this.config.summaryThreshold) {
|
|
49
47
|
return messages;
|
|
50
48
|
}
|
|
51
49
|
// 3. 检查是否有摘要
|
|
52
50
|
const hasSummary = this.session.lastMessageSummary;
|
|
53
|
-
logger.debug('hasSummary', hasSummary);
|
|
54
51
|
if (!hasSummary) {
|
|
55
52
|
// 4A. 首次摘要生成
|
|
56
53
|
const summary = await this.generateSummaryAsync(messages.slice(0, -index_js_2.appConfig.messageKeep), null);
|
|
@@ -65,7 +62,6 @@ class MemoryManager {
|
|
|
65
62
|
else {
|
|
66
63
|
// 4B. 增量摘要更新
|
|
67
64
|
const newMessages = messages.filter((m) => m.createdAt > this.session.lastMessageSummaryAt);
|
|
68
|
-
logger.debug('new messages length:', newMessages.length);
|
|
69
65
|
if (newMessages.length < this.config.summaryThreshold) {
|
|
70
66
|
// 新消息少于阈值条:返回摘要 + 从摘要时刻起的所有消息
|
|
71
67
|
const fromSummary = messages.filter((m) => m.createdAt >= this.session.lastMessageSummaryAt);
|
|
@@ -120,7 +120,7 @@ exports.execTool = {
|
|
|
120
120
|
}
|
|
121
121
|
catch (e) {
|
|
122
122
|
const error = e;
|
|
123
|
-
resolve({ success: false, errorMessage: error
|
|
123
|
+
resolve({ success: false, errorMessage: (error?.stdout?.toLocaleString()?.substring(0, 100) ?? '') + (error?.stderr?.toLocaleString() ?? '') + (error?.message ?? '') });
|
|
124
124
|
return;
|
|
125
125
|
}
|
|
126
126
|
});
|
|
@@ -15,7 +15,7 @@ exports.screenshotTool = {
|
|
|
15
15
|
const base64 = imgBuffer.toString('base64');
|
|
16
16
|
return {
|
|
17
17
|
success: true,
|
|
18
|
-
output:
|
|
18
|
+
output: `data:image/jpeg;base64,${base64}`,
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
21
|
catch (error) {
|