@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,733 @@
|
|
|
1
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
2
|
+
import { getLogger } from '@pocketclaw/shared';
|
|
3
|
+
import { SessionStore } from './SessionStore';
|
|
4
|
+
import { AgentStore } from '@/services/agent/AgentStore';
|
|
5
|
+
import { toModel } from '@/services/models';
|
|
6
|
+
import { executeTool, getToolDefinitions } from '@/services/tools';
|
|
7
|
+
import { getSystemPromptAsync } from '../systemPrompt';
|
|
8
|
+
import { LLMClient, parseAruments } from '../llm/LLMClient';
|
|
9
|
+
import { modelsService, settingsService } from '@/services/dataService';
|
|
10
|
+
import { MemoryManager } from '../memory/MemoryManager';
|
|
11
|
+
import { sessionManager } from './SessionManager';
|
|
12
|
+
import { appConfig } from '@/config';
|
|
13
|
+
const logger = getLogger('Session');
|
|
14
|
+
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
15
|
+
// AgentStore 单例 - 需要通过 SessionStore 获取数据库实例
|
|
16
|
+
let agentStoreInstance = null;
|
|
17
|
+
function getAgentStore() {
|
|
18
|
+
if (!agentStoreInstance) {
|
|
19
|
+
// SessionStore 会在创建时注册到全局
|
|
20
|
+
const sessionStore = global.__sessionStore;
|
|
21
|
+
if (!sessionStore) {
|
|
22
|
+
throw new Error('SessionStore not initialized');
|
|
23
|
+
}
|
|
24
|
+
agentStoreInstance = new AgentStore(sessionStore.getDb());
|
|
25
|
+
}
|
|
26
|
+
return agentStoreInstance;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Session - session entity
|
|
30
|
+
*/
|
|
31
|
+
export class Session {
|
|
32
|
+
id;
|
|
33
|
+
userId;
|
|
34
|
+
agentId;
|
|
35
|
+
title;
|
|
36
|
+
selectModelId;
|
|
37
|
+
messages;
|
|
38
|
+
voiceState;
|
|
39
|
+
createdAt;
|
|
40
|
+
updatedAt;
|
|
41
|
+
lastMessageSummary = null;
|
|
42
|
+
lastMessageSummaryAt = null;
|
|
43
|
+
isCurrent;
|
|
44
|
+
// 用于停止生成
|
|
45
|
+
isGenerating = false;
|
|
46
|
+
unreadCount = 0;
|
|
47
|
+
currentMessageId = null;
|
|
48
|
+
abortController = null;
|
|
49
|
+
toolCalls;
|
|
50
|
+
toInsertMessages;
|
|
51
|
+
constructor(data) {
|
|
52
|
+
this.id = data.id;
|
|
53
|
+
this.userId = data.userId;
|
|
54
|
+
this.agentId = data.agentId;
|
|
55
|
+
this.title = data.title || 'New Chat';
|
|
56
|
+
this.selectModelId = data.selectModelId;
|
|
57
|
+
this.messages = [];
|
|
58
|
+
this.voiceState = data.voiceState || { isRecording: false, isPlaying: false };
|
|
59
|
+
this.lastMessageSummary = data.lastMessageSummary ?? null;
|
|
60
|
+
this.lastMessageSummaryAt = data.lastMessageSummaryAt ?? null;
|
|
61
|
+
this.unreadCount = data.unreadCount || 0;
|
|
62
|
+
this.isCurrent = data.isCurrent ?? false;
|
|
63
|
+
this.createdAt = data.createdAt || Date.now();
|
|
64
|
+
this.updatedAt = data.updatedAt || Date.now();
|
|
65
|
+
this.store = new SessionStore();
|
|
66
|
+
this.toInsertMessages = [];
|
|
67
|
+
}
|
|
68
|
+
store;
|
|
69
|
+
/**
|
|
70
|
+
* Get conversation count
|
|
71
|
+
*/
|
|
72
|
+
getConversationCount() {
|
|
73
|
+
return this.messages.filter((m) => m.role === 'user' || m.role === 'assistant').length;
|
|
74
|
+
}
|
|
75
|
+
// ========== Persistence ==========
|
|
76
|
+
/**
|
|
77
|
+
* Save session
|
|
78
|
+
*/
|
|
79
|
+
save() {
|
|
80
|
+
const store = this.store;
|
|
81
|
+
const storeData = this.toStoreData();
|
|
82
|
+
store.transaction(() => {
|
|
83
|
+
const exists = store.findSessionById(this.id);
|
|
84
|
+
if (exists) {
|
|
85
|
+
store.updateSession(storeData);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
store.insertSession(storeData);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Load messages
|
|
94
|
+
*/
|
|
95
|
+
loadMessages() {
|
|
96
|
+
const store = this.store;
|
|
97
|
+
this.messages = store.findMessagesBySessionId(this.id);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Get context length
|
|
101
|
+
*/
|
|
102
|
+
getContextLength() {
|
|
103
|
+
return this.messages.reduce((total, msg) => total + msg.content.length, 0);
|
|
104
|
+
}
|
|
105
|
+
// ========== Message Operations ==========
|
|
106
|
+
/**
|
|
107
|
+
* Add user message
|
|
108
|
+
*/
|
|
109
|
+
addUserMessage(content, userMessageId, attachments) {
|
|
110
|
+
// 将附件统一为 Attachment 对象格式
|
|
111
|
+
const normalizedAttachments = (attachments || []).map((att) => {
|
|
112
|
+
if (typeof att === 'string') {
|
|
113
|
+
// URL 字符串格式,转换为对象
|
|
114
|
+
const url = att;
|
|
115
|
+
const name = url.split('/').pop() || 'attachment';
|
|
116
|
+
const ext = name.split('.').pop()?.toLowerCase() || '';
|
|
117
|
+
const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg'];
|
|
118
|
+
const videoExts = ['mp4', 'avi', 'mov', 'mkv', 'webm'];
|
|
119
|
+
const audioExts = ['mp3', 'wav', 'ogg', 'flac', 'aac'];
|
|
120
|
+
let type = 'file';
|
|
121
|
+
if (imageExts.includes(ext))
|
|
122
|
+
type = 'image';
|
|
123
|
+
else if (videoExts.includes(ext))
|
|
124
|
+
type = 'video';
|
|
125
|
+
else if (audioExts.includes(ext))
|
|
126
|
+
type = 'audio';
|
|
127
|
+
return { type, name, url };
|
|
128
|
+
}
|
|
129
|
+
// 已经是对象格式,确保有必要字段
|
|
130
|
+
return {
|
|
131
|
+
type: att.type || 'file',
|
|
132
|
+
name: att.name || 'attachment',
|
|
133
|
+
url: att.url || att.uri || '',
|
|
134
|
+
size: att.size,
|
|
135
|
+
mimeType: att.mimeType,
|
|
136
|
+
};
|
|
137
|
+
});
|
|
138
|
+
const message = {
|
|
139
|
+
sessionId: this.id,
|
|
140
|
+
id: userMessageId,
|
|
141
|
+
role: 'user',
|
|
142
|
+
content,
|
|
143
|
+
attachments: normalizedAttachments,
|
|
144
|
+
status: 'completed',
|
|
145
|
+
createdAt: Date.now(),
|
|
146
|
+
modelName: ''
|
|
147
|
+
};
|
|
148
|
+
this.messages.push(message);
|
|
149
|
+
this.toInsertMessages.push(message);
|
|
150
|
+
return message;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Add assistant message
|
|
154
|
+
*/
|
|
155
|
+
addAssistantMessage(content, toolCalls, modelName, id) {
|
|
156
|
+
const message = {
|
|
157
|
+
sessionId: this.id,
|
|
158
|
+
id: id ? id : uuidv4(),
|
|
159
|
+
role: 'assistant',
|
|
160
|
+
content,
|
|
161
|
+
toolCalls,
|
|
162
|
+
createdAt: Date.now(),
|
|
163
|
+
modelName
|
|
164
|
+
};
|
|
165
|
+
this.messages.push(message);
|
|
166
|
+
this.toInsertMessages.push(message);
|
|
167
|
+
return message;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Save a single message to store
|
|
171
|
+
*/
|
|
172
|
+
async saveMessage() {
|
|
173
|
+
const store = this.store;
|
|
174
|
+
this.updatedAt = Date.now();
|
|
175
|
+
try {
|
|
176
|
+
store.transaction(() => {
|
|
177
|
+
for (let message of this.toInsertMessages) {
|
|
178
|
+
store.insertMessage(this.id, message);
|
|
179
|
+
}
|
|
180
|
+
this.toInsertMessages = [];
|
|
181
|
+
store.updateSession(this.toStoreData());
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
logger.error('数据保存错误:', error);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Update message content
|
|
190
|
+
*/
|
|
191
|
+
updateMessage(messageId, content, status) {
|
|
192
|
+
const message = this.messages.find(m => m.id === messageId);
|
|
193
|
+
if (!message)
|
|
194
|
+
return;
|
|
195
|
+
const store = this.store;
|
|
196
|
+
store.transaction(() => {
|
|
197
|
+
message.content = content;
|
|
198
|
+
if (status)
|
|
199
|
+
message.status = status;
|
|
200
|
+
this.updatedAt = Date.now();
|
|
201
|
+
store.updateMessage(this.id, message);
|
|
202
|
+
store.updateSession(this.toStoreData());
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Delete message
|
|
207
|
+
*/
|
|
208
|
+
deleteMessage(messageId) {
|
|
209
|
+
const index = this.messages.findIndex(m => m.id === messageId);
|
|
210
|
+
if (index === -1)
|
|
211
|
+
return false;
|
|
212
|
+
const store = this.store;
|
|
213
|
+
store.transaction(() => {
|
|
214
|
+
this.messages.splice(index, 1);
|
|
215
|
+
this.updatedAt = Date.now();
|
|
216
|
+
store.deleteMessage(messageId);
|
|
217
|
+
store.updateSession(this.toStoreData());
|
|
218
|
+
});
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Clear messages
|
|
223
|
+
*/
|
|
224
|
+
clearMessages() {
|
|
225
|
+
const store = this.store;
|
|
226
|
+
store.transaction(() => {
|
|
227
|
+
this.messages = [];
|
|
228
|
+
this.updatedAt = Date.now();
|
|
229
|
+
store.deleteMessagesBySessionId(this.id);
|
|
230
|
+
store.updateSession(this.toStoreData());
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Update message status
|
|
235
|
+
*/
|
|
236
|
+
updateMessageStatus(messageId, status) {
|
|
237
|
+
const message = this.messages.find(m => m.id === messageId);
|
|
238
|
+
if (!message)
|
|
239
|
+
return;
|
|
240
|
+
const store = this.store;
|
|
241
|
+
store.transaction(() => {
|
|
242
|
+
message.status = status;
|
|
243
|
+
this.updatedAt = Date.now();
|
|
244
|
+
store.updateMessage(this.id, message);
|
|
245
|
+
store.updateSession(this.toStoreData());
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Update message feedback
|
|
250
|
+
*/
|
|
251
|
+
updateMessageFeedback(messageId, feedback) {
|
|
252
|
+
const message = this.messages.find(m => m.id === messageId);
|
|
253
|
+
if (!message)
|
|
254
|
+
return;
|
|
255
|
+
const store = this.store;
|
|
256
|
+
store.transaction(() => {
|
|
257
|
+
message.feedback = feedback;
|
|
258
|
+
this.updatedAt = Date.now();
|
|
259
|
+
store.updateMessage(this.id, message);
|
|
260
|
+
store.updateSession(this.toStoreData());
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Get messages
|
|
265
|
+
*/
|
|
266
|
+
getMessages() {
|
|
267
|
+
return this.messages;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Get message count
|
|
271
|
+
*/
|
|
272
|
+
getMessageCount() {
|
|
273
|
+
return this.messages.length;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Get messages by page (descending order)
|
|
277
|
+
*/
|
|
278
|
+
getMessagesByPage(page = 1, pageSize = 20) {
|
|
279
|
+
const sortedMessages = [...this.messages].sort((a, b) => b.createdAt - a.createdAt);
|
|
280
|
+
const total = sortedMessages.length;
|
|
281
|
+
const totalPages = Math.ceil(total / pageSize);
|
|
282
|
+
const startIndex = (page - 1) * pageSize;
|
|
283
|
+
const endIndex = startIndex + pageSize;
|
|
284
|
+
const messages = sortedMessages.slice(startIndex, endIndex);
|
|
285
|
+
return {
|
|
286
|
+
messages,
|
|
287
|
+
total,
|
|
288
|
+
page,
|
|
289
|
+
pageSize,
|
|
290
|
+
totalPages,
|
|
291
|
+
hasMore: page < totalPages,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
// ========== Voice Operations ==========
|
|
295
|
+
getVoiceState() {
|
|
296
|
+
return this.voiceState;
|
|
297
|
+
}
|
|
298
|
+
updateVoiceState(state) {
|
|
299
|
+
this.voiceState = { ...this.voiceState, ...state };
|
|
300
|
+
this.updatedAt = Date.now();
|
|
301
|
+
this.save();
|
|
302
|
+
}
|
|
303
|
+
toggleVoiceRecording() {
|
|
304
|
+
this.voiceState.isRecording = !this.voiceState.isRecording;
|
|
305
|
+
if (this.voiceState.isRecording) {
|
|
306
|
+
this.voiceState.isPlaying = false;
|
|
307
|
+
}
|
|
308
|
+
this.updatedAt = Date.now();
|
|
309
|
+
this.save();
|
|
310
|
+
return this.voiceState.isRecording;
|
|
311
|
+
}
|
|
312
|
+
// ========== Export/Import ==========
|
|
313
|
+
getSummary() {
|
|
314
|
+
return {
|
|
315
|
+
title: this.title,
|
|
316
|
+
lastMessage: this.lastMessageSummary,
|
|
317
|
+
lastMessageAt: this.lastMessageSummaryAt,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
exportData() {
|
|
321
|
+
return {
|
|
322
|
+
session: {
|
|
323
|
+
id: this.id,
|
|
324
|
+
userId: this.userId,
|
|
325
|
+
agentId: this.agentId,
|
|
326
|
+
title: this.title,
|
|
327
|
+
selectModelId: this.selectModelId,
|
|
328
|
+
voiceState: this.voiceState,
|
|
329
|
+
createdAt: this.createdAt,
|
|
330
|
+
updatedAt: this.updatedAt,
|
|
331
|
+
unreadCount: this.unreadCount,
|
|
332
|
+
isCurrent: this.isCurrent
|
|
333
|
+
},
|
|
334
|
+
messages: this.messages,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
// 未读消息数 +1 并持久化
|
|
338
|
+
incrementUnreadCount() {
|
|
339
|
+
this.unreadCount++;
|
|
340
|
+
this.save();
|
|
341
|
+
}
|
|
342
|
+
// 清除未读消息数并持久化
|
|
343
|
+
clearUnreadCount() {
|
|
344
|
+
this.unreadCount = 0;
|
|
345
|
+
this.save();
|
|
346
|
+
}
|
|
347
|
+
toStoreData() {
|
|
348
|
+
return {
|
|
349
|
+
id: this.id,
|
|
350
|
+
userId: this.userId,
|
|
351
|
+
agentId: this.agentId,
|
|
352
|
+
title: this.title,
|
|
353
|
+
selectModelId: this.selectModelId || '',
|
|
354
|
+
voiceState: this.voiceState,
|
|
355
|
+
createdAt: this.createdAt,
|
|
356
|
+
updatedAt: this.updatedAt,
|
|
357
|
+
lastMessageSummary: this.lastMessageSummary,
|
|
358
|
+
lastMessageSummaryAt: this.lastMessageSummaryAt,
|
|
359
|
+
unreadCount: this.unreadCount,
|
|
360
|
+
isCurrent: this.isCurrent
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
getStreamDelay(streamSpeed) {
|
|
364
|
+
if (streamSpeed === 'fast') {
|
|
365
|
+
return 50;
|
|
366
|
+
}
|
|
367
|
+
if (streamSpeed === 'normal') {
|
|
368
|
+
return 100;
|
|
369
|
+
}
|
|
370
|
+
if (streamSpeed === 'slow') {
|
|
371
|
+
return 200;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Stream chat response (SSE) - 使用 ModelSelector 根据选择的模型调用
|
|
376
|
+
* 支持工具调用流程
|
|
377
|
+
*/
|
|
378
|
+
async streamChat(content, attachments = [], res, userMessageId, assistantMessageId) {
|
|
379
|
+
// Add user message
|
|
380
|
+
this.getAbortController();
|
|
381
|
+
this.isGenerating = true;
|
|
382
|
+
this.toolCalls = [];
|
|
383
|
+
const settings = await settingsService.get();
|
|
384
|
+
const streamDelay = this.getStreamDelay(settings.streamSpeed);
|
|
385
|
+
this.currentMessageId = assistantMessageId;
|
|
386
|
+
const memoryManager = new MemoryManager(this, this.abortController.signal);
|
|
387
|
+
const historyMessages = await memoryManager.getHistoryMessagesAsync();
|
|
388
|
+
// SSE 辅助方法:res 为 null 时跳过写入(本地执行模式)
|
|
389
|
+
const sendSSE = (res, data) => {
|
|
390
|
+
if (!res)
|
|
391
|
+
return;
|
|
392
|
+
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
393
|
+
res.flush?.();
|
|
394
|
+
};
|
|
395
|
+
// Send message start event
|
|
396
|
+
sendSSE(res, { type: 'message_start' });
|
|
397
|
+
// Build messages for API call (保留 tool_call_id 等必要字段)
|
|
398
|
+
let messages = historyMessages.map(m => {
|
|
399
|
+
const msg = {
|
|
400
|
+
role: m.role,
|
|
401
|
+
content: m.content,
|
|
402
|
+
attachments: m.attachments,
|
|
403
|
+
toolCalls: m.toolCalls
|
|
404
|
+
};
|
|
405
|
+
// 恢复 reasoning_content
|
|
406
|
+
if (m.reasoning_content) {
|
|
407
|
+
msg.reasoning_content = m.reasoning_content;
|
|
408
|
+
}
|
|
409
|
+
// 恢复 tool_calls(assistant 消息)
|
|
410
|
+
if (m.role === 'assistant' && m.tool_calls && m.tool_calls.length > 0) {
|
|
411
|
+
msg.tool_calls = m.tool_calls;
|
|
412
|
+
}
|
|
413
|
+
// 恢复 tool_call_id(tool 消息)
|
|
414
|
+
if (m.role === 'tool' && m.tool_call_id) {
|
|
415
|
+
msg.tool_call_id = m.tool_call_id;
|
|
416
|
+
}
|
|
417
|
+
return msg;
|
|
418
|
+
});
|
|
419
|
+
// 从 Agent 获取系统提示词
|
|
420
|
+
let systemPrompt = '';
|
|
421
|
+
let agent = null;
|
|
422
|
+
if (this.agentId) {
|
|
423
|
+
agent = getAgentStore().findById(this.agentId);
|
|
424
|
+
if (agent?.systemPrompt) {
|
|
425
|
+
systemPrompt = agent.systemPrompt;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
systemPrompt = await getSystemPromptAsync(agent, systemPrompt);
|
|
429
|
+
if (messages.some(x => x.role === 'system')) {
|
|
430
|
+
messages = messages.map(m => m.role == 'system' ? { ...m, content: systemPrompt + '\n以下为之前对话内容摘要:\n' + m.content } : m);
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
messages.unshift({ role: 'system', content: systemPrompt, attachments: [] });
|
|
434
|
+
}
|
|
435
|
+
//插入用户消息
|
|
436
|
+
this.addUserMessage(content, userMessageId, attachments);
|
|
437
|
+
messages.push(this.messages.at(-1));
|
|
438
|
+
let toolCall = null;
|
|
439
|
+
let modelNames = [];
|
|
440
|
+
//用于后续的消息滑动处理
|
|
441
|
+
const messagesLength = messages.length;
|
|
442
|
+
const keep = appConfig.messageKeep;
|
|
443
|
+
//具体调用逻辑函数
|
|
444
|
+
// 辅助函数:截断消息内容
|
|
445
|
+
const truncateMessageContent = (message) => {
|
|
446
|
+
if (message.content && message.content.length > 100) {
|
|
447
|
+
message.content = message.content.substring(0, 100) + '...(内容过长已截断)';
|
|
448
|
+
}
|
|
449
|
+
if (message.reasoning_content && message.reasoning_content.length > 100) {
|
|
450
|
+
message.reasoning_content = message.reasoning_content.substring(0, 100) + '...(内容过长已截断)';
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
// 递归处理函数(支持多轮工具调用)
|
|
454
|
+
const processModelResponse = async () => {
|
|
455
|
+
if (!this.abortController || !this.abortController.signal || this.abortController.signal.aborted) {
|
|
456
|
+
throw Error('aborted');
|
|
457
|
+
}
|
|
458
|
+
// 工具调用轮次过多时,成对删除最早的 assistant(tool_calls) + tool(result)
|
|
459
|
+
// 只保留最近 keep 轮工具调用,防止上下文过长导致模型混乱
|
|
460
|
+
// 清理旧工具调用:只保留最近 keep 轮的完整上下文
|
|
461
|
+
{
|
|
462
|
+
const MAX_TOOL_PAIRS = keep;
|
|
463
|
+
const allToolCallItems = this.toolCalls.flatMap(x => x.toolCalls);
|
|
464
|
+
// 1) 清理 this.toolCalls 中超出 keep 的 output(供前端展示用)
|
|
465
|
+
allToolCallItems.forEach((item, index) => {
|
|
466
|
+
if (index < allToolCallItems.length - MAX_TOOL_PAIRS) {
|
|
467
|
+
item.output = null;
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
// 2) 从 messages 中成对删除最早的 assistant(tool_calls) + tool(result)
|
|
471
|
+
let toolPairCount = 0;
|
|
472
|
+
for (let i = messagesLength; i < messages.length; i++) {
|
|
473
|
+
if (messages[i].role === 'assistant' && messages[i].tool_calls) {
|
|
474
|
+
toolPairCount++;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
if (toolPairCount > MAX_TOOL_PAIRS) {
|
|
478
|
+
let toRemove = toolPairCount - MAX_TOOL_PAIRS;
|
|
479
|
+
// 找到需要截断的工具调用对
|
|
480
|
+
for (let i = messagesLength; i < messages.length && toRemove > 0; i++) {
|
|
481
|
+
if (messages[i].role === 'assistant' && messages[i].tool_calls) {
|
|
482
|
+
// 截断 assistant 消息内容
|
|
483
|
+
truncateMessageContent(messages[i]);
|
|
484
|
+
while (true) {
|
|
485
|
+
// 检查并截断对应的 tool 消息
|
|
486
|
+
if (i + 1 < messages.length && messages[i + 1].role === 'tool') {
|
|
487
|
+
truncateMessageContent(messages[i + 1]);
|
|
488
|
+
}
|
|
489
|
+
else {
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
i++;
|
|
493
|
+
}
|
|
494
|
+
toRemove--;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
// 获取本地工具定义
|
|
500
|
+
const tools = getToolDefinitions();
|
|
501
|
+
const models = (await modelsService.list()).data.map(x => toModel(x));
|
|
502
|
+
// 使用 LLMClient 进行流式调用
|
|
503
|
+
if (!this.abortController || !this.abortController.signal || this.abortController.signal.aborted) {
|
|
504
|
+
throw Error('aborted');
|
|
505
|
+
}
|
|
506
|
+
const llmClient = new LLMClient(models, messages, this.abortController.signal, tools);
|
|
507
|
+
llmClient.setPreferredModel(this.selectModelId);
|
|
508
|
+
// 使用 streamChat 调用模型
|
|
509
|
+
const llmResult = await llmClient.Chat();
|
|
510
|
+
// 验证 LLM 结果
|
|
511
|
+
if (!llmResult || !llmResult.modelName) {
|
|
512
|
+
throw new Error('Invalid LLM result: missing modelName');
|
|
513
|
+
}
|
|
514
|
+
const modelName = llmResult.modelName;
|
|
515
|
+
modelNames.push(modelName);
|
|
516
|
+
// ========== 处理工具调用 ==========
|
|
517
|
+
if (llmResult.toolCalls && llmResult.toolCalls.length > 0) {
|
|
518
|
+
messages.push({
|
|
519
|
+
role: 'assistant',
|
|
520
|
+
content: llmResult.content,
|
|
521
|
+
reasoning_content: llmResult.reasoningContent,
|
|
522
|
+
tool_calls: llmResult.toolCalls
|
|
523
|
+
});
|
|
524
|
+
const content = llmResult.content || llmResult.reasoningContent;
|
|
525
|
+
if (content || toolCall == null) {
|
|
526
|
+
const toolCallId = uuidv4();
|
|
527
|
+
toolCall = {
|
|
528
|
+
id: toolCallId,
|
|
529
|
+
content: llmResult.content,
|
|
530
|
+
reasoningContent: llmResult.reasoningContent,
|
|
531
|
+
toolCalls: []
|
|
532
|
+
};
|
|
533
|
+
sendSSE(res, {
|
|
534
|
+
type: 'tool_call_start',
|
|
535
|
+
toolCall: {
|
|
536
|
+
id: toolCallId,
|
|
537
|
+
content: '',
|
|
538
|
+
reasoningContent: '',
|
|
539
|
+
toolCalls: []
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
// 分段发送内容
|
|
543
|
+
if (llmResult.content) {
|
|
544
|
+
const chunkSize = 1;
|
|
545
|
+
for (let i = 0; i < llmResult.content.length; i += chunkSize) {
|
|
546
|
+
const chunk = llmResult.content.slice(i, i + chunkSize);
|
|
547
|
+
sendSSE(res, {
|
|
548
|
+
type: 'tool_call_start_chunk',
|
|
549
|
+
id: toolCallId,
|
|
550
|
+
chunk
|
|
551
|
+
});
|
|
552
|
+
await sleep(streamDelay);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
// 分段发送内容
|
|
556
|
+
if (llmResult.reasoningContent) {
|
|
557
|
+
const chunkSize = 1;
|
|
558
|
+
for (let i = 0; i < llmResult.reasoningContent.length; i += chunkSize) {
|
|
559
|
+
const chunk = llmResult.reasoningContent.slice(i, i + chunkSize);
|
|
560
|
+
sendSSE(res, {
|
|
561
|
+
type: 'tool_call_start_reasoning_chunk',
|
|
562
|
+
id: toolCallId,
|
|
563
|
+
chunk
|
|
564
|
+
});
|
|
565
|
+
await sleep(streamDelay);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
// 执行工具调用并收集结果
|
|
570
|
+
const toolResults = [];
|
|
571
|
+
for (const pendingToolCall of llmResult.toolCalls) {
|
|
572
|
+
// 发送工具开始执行事件
|
|
573
|
+
const toolStartEvent = {
|
|
574
|
+
type: 'tool_call_execute',
|
|
575
|
+
id: pendingToolCall.id,
|
|
576
|
+
toolName: pendingToolCall.toolName,
|
|
577
|
+
actionName: pendingToolCall.actionName,
|
|
578
|
+
input: pendingToolCall.input,
|
|
579
|
+
toolCallId: toolCall.id,
|
|
580
|
+
modelName
|
|
581
|
+
};
|
|
582
|
+
sendSSE(res, toolStartEvent);
|
|
583
|
+
try {
|
|
584
|
+
const result = await executeTool(pendingToolCall.toolName, parseAruments(pendingToolCall.input), this.id, this.currentMessageId);
|
|
585
|
+
toolResults.push({
|
|
586
|
+
id: pendingToolCall.id,
|
|
587
|
+
name: pendingToolCall.toolName,
|
|
588
|
+
output: result.output || result.errorMessage,
|
|
589
|
+
success: result.success,
|
|
590
|
+
});
|
|
591
|
+
toolCall.toolCalls.push({
|
|
592
|
+
id: pendingToolCall.id,
|
|
593
|
+
toolName: pendingToolCall.toolName,
|
|
594
|
+
input: pendingToolCall.input,
|
|
595
|
+
output: result.output || result.errorMessage,
|
|
596
|
+
status: result.success ? 'success' : 'error',
|
|
597
|
+
actionName: pendingToolCall.actionName
|
|
598
|
+
});
|
|
599
|
+
const resultEvent = {
|
|
600
|
+
type: 'tool_call_result',
|
|
601
|
+
id: pendingToolCall.id,
|
|
602
|
+
toolName: pendingToolCall.toolName,
|
|
603
|
+
actionName: pendingToolCall.actionName,
|
|
604
|
+
output: JSON.stringify(result),
|
|
605
|
+
status: result.success ? 'success' : 'error',
|
|
606
|
+
toolCallId: toolCall.id,
|
|
607
|
+
modelName
|
|
608
|
+
};
|
|
609
|
+
sendSSE(res, resultEvent);
|
|
610
|
+
}
|
|
611
|
+
catch (error) {
|
|
612
|
+
logger.error(`Tool execution error: ${pendingToolCall.toolName} `, error);
|
|
613
|
+
const errorMessage = error.message || 'Execution error';
|
|
614
|
+
toolResults.push({
|
|
615
|
+
id: pendingToolCall.id,
|
|
616
|
+
name: pendingToolCall.toolName,
|
|
617
|
+
output: errorMessage,
|
|
618
|
+
success: false,
|
|
619
|
+
});
|
|
620
|
+
toolCall.toolCalls.push({
|
|
621
|
+
id: pendingToolCall.id,
|
|
622
|
+
toolName: pendingToolCall.toolName,
|
|
623
|
+
input: pendingToolCall.input,
|
|
624
|
+
status: 'error',
|
|
625
|
+
output: errorMessage,
|
|
626
|
+
actionName: pendingToolCall.actionName
|
|
627
|
+
});
|
|
628
|
+
// 发送工具执行失败事件
|
|
629
|
+
const errorEvent = {
|
|
630
|
+
type: 'tool_call_result',
|
|
631
|
+
id: pendingToolCall.id,
|
|
632
|
+
toolName: pendingToolCall.toolName,
|
|
633
|
+
output: errorMessage,
|
|
634
|
+
status: 'error',
|
|
635
|
+
toolCallId: toolCall.id,
|
|
636
|
+
actionName: pendingToolCall.actionName,
|
|
637
|
+
modelName
|
|
638
|
+
};
|
|
639
|
+
sendSSE(res, errorEvent);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
this.toolCalls.push(toolCall);
|
|
643
|
+
// 将工具结果添加到消息历史(格式化为 tool 角色的消息)
|
|
644
|
+
for (const result of toolResults) {
|
|
645
|
+
const toolResultMessage = {
|
|
646
|
+
sessionId: this.id,
|
|
647
|
+
id: `tool_result_${result.id}_${Date.now()}`,
|
|
648
|
+
role: 'tool',
|
|
649
|
+
content: JSON.stringify({ success: result.success, output: result.output }),
|
|
650
|
+
tool_call_id: result.id,
|
|
651
|
+
tool_call_name: result.name,
|
|
652
|
+
createdAt: Date.now(),
|
|
653
|
+
modelName
|
|
654
|
+
};
|
|
655
|
+
messages.push(toolResultMessage);
|
|
656
|
+
}
|
|
657
|
+
// 继续递归处理(带上工具结果继续调用模型)
|
|
658
|
+
await processModelResponse();
|
|
659
|
+
}
|
|
660
|
+
else {
|
|
661
|
+
// ========== 没有工具调用,保存消息并结束 ==========
|
|
662
|
+
if (llmResult.content || llmResult.reasoningContent) {
|
|
663
|
+
// 分段发送内容,每段约50个字符
|
|
664
|
+
const chunkSize = 1;
|
|
665
|
+
const content = llmResult.content || llmResult.reasoningContent;
|
|
666
|
+
for (let i = 0; i < content.length; i += chunkSize) {
|
|
667
|
+
const chunk = content.slice(i, i + chunkSize);
|
|
668
|
+
sendSSE(res, {
|
|
669
|
+
type: 'content',
|
|
670
|
+
data: chunk
|
|
671
|
+
});
|
|
672
|
+
await sleep(streamDelay);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
this.addAssistantMessage(llmResult.content || llmResult.reasoningContent, this.toolCalls, [...new Set(modelNames)].join(','), this.currentMessageId);
|
|
676
|
+
sendSSE(res, { type: 'complete', modelName: [...new Set(modelNames)].join(",") });
|
|
677
|
+
sendSSE(res, { type: '[DONE]' });
|
|
678
|
+
}
|
|
679
|
+
};
|
|
680
|
+
try {
|
|
681
|
+
await processModelResponse();
|
|
682
|
+
}
|
|
683
|
+
catch (error) {
|
|
684
|
+
logger.error('stream chat ', error);
|
|
685
|
+
if (this.toolCalls) {
|
|
686
|
+
this.addAssistantMessage('', this.toolCalls, [...new Set(modelNames)].join(','), this.currentMessageId);
|
|
687
|
+
}
|
|
688
|
+
if (error.message !== 'aborted') {
|
|
689
|
+
sendSSE(res, { type: 'error', error: error.message });
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
finally {
|
|
693
|
+
this.isGenerating = false;
|
|
694
|
+
this.abortController = null;
|
|
695
|
+
if (res) {
|
|
696
|
+
res.end();
|
|
697
|
+
}
|
|
698
|
+
// 非当前查看的 session,助手回复完成后增加未读计数
|
|
699
|
+
sessionManager.onMessageComplete(this.id);
|
|
700
|
+
// 只在非 abort 的情况下保存(abort 由 stopGenerating 负责保存)
|
|
701
|
+
if (this.currentMessageId !== null) {
|
|
702
|
+
this.saveMessage();
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* 停止当前正在进行的生成
|
|
708
|
+
*/
|
|
709
|
+
stopGenerating() {
|
|
710
|
+
this.isGenerating = false;
|
|
711
|
+
if (this.abortController) {
|
|
712
|
+
this.abortController.abort();
|
|
713
|
+
this.abortController = null;
|
|
714
|
+
}
|
|
715
|
+
// stopGenerating 负责保存中断的消息,streamChat 的 finally 不再重复保存
|
|
716
|
+
if (this.currentMessageId) {
|
|
717
|
+
if (this.toolCalls && this.toolCalls.length > 0) {
|
|
718
|
+
this.addAssistantMessage('', this.toolCalls, this.currentMessageId);
|
|
719
|
+
}
|
|
720
|
+
this.saveMessage();
|
|
721
|
+
this.currentMessageId = null;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* 获取 AbortController 用于中断请求
|
|
726
|
+
*/
|
|
727
|
+
getAbortController() {
|
|
728
|
+
if (!this.abortController) {
|
|
729
|
+
this.abortController = new AbortController();
|
|
730
|
+
}
|
|
731
|
+
return this.abortController;
|
|
732
|
+
}
|
|
733
|
+
}
|