@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.
Files changed (65) hide show
  1. package/README.md +194 -0
  2. package/dist/.env +6 -0
  3. package/dist/api/index.js +182 -0
  4. package/dist/config/index.js +41 -0
  5. package/dist/index.js +183 -0
  6. package/dist/middleware/auth.js +53 -0
  7. package/dist/middleware/errorHandler.js +20 -0
  8. package/dist/routes/agent.js +513 -0
  9. package/dist/routes/auth.js +172 -0
  10. package/dist/routes/chat.js +45 -0
  11. package/dist/routes/config.js +21 -0
  12. package/dist/routes/models.js +123 -0
  13. package/dist/routes/service.js +240 -0
  14. package/dist/routes/settings.js +101 -0
  15. package/dist/routes/skillHub.js +126 -0
  16. package/dist/routes/skills.js +159 -0
  17. package/dist/routes/tasks.js +149 -0
  18. package/dist/routes/upload.js +129 -0
  19. package/dist/routes/version.js +66 -0
  20. package/dist/services/HMSPushService.js +24 -0
  21. package/dist/services/LocalTaskService.js +223 -0
  22. package/dist/services/NotificationService.js +242 -0
  23. package/dist/services/ServiceManager.js +348 -0
  24. package/dist/services/TaskSchedulerService.js +195 -0
  25. package/dist/services/TaskService.js +240 -0
  26. package/dist/services/WebSocketService.js +236 -0
  27. package/dist/services/agent/Agent.js +120 -0
  28. package/dist/services/agent/AgentManager.js +265 -0
  29. package/dist/services/agent/AgentStore.js +73 -0
  30. package/dist/services/dataService.js +293 -0
  31. package/dist/services/index.js +15 -0
  32. package/dist/services/llm/LLMClient.js +724 -0
  33. package/dist/services/memory/MemoryManager.js +117 -0
  34. package/dist/services/model/ModelCapabilities.js +141 -0
  35. package/dist/services/model/index.js +4 -0
  36. package/dist/services/models.js +16 -0
  37. package/dist/services/session/MigrationManager.js +176 -0
  38. package/dist/services/session/Session.js +733 -0
  39. package/dist/services/session/SessionManager.js +255 -0
  40. package/dist/services/session/SessionStore.js +186 -0
  41. package/dist/services/session/index.js +3 -0
  42. package/dist/services/skills.js +34 -0
  43. package/dist/services/systemPrompt.js +150 -0
  44. package/dist/services/task/PushTokenStore.js +124 -0
  45. package/dist/services/task/TaskStore.js +143 -0
  46. package/dist/services/tools/calculator.js +27 -0
  47. package/dist/services/tools/edit.js +318 -0
  48. package/dist/services/tools/exec.js +119 -0
  49. package/dist/services/tools/fetch.js +155 -0
  50. package/dist/services/tools/file.js +315 -0
  51. package/dist/services/tools/index.js +48 -0
  52. package/dist/services/tools/keyboard.js +145 -0
  53. package/dist/services/tools/model.js +86 -0
  54. package/dist/services/tools/mouse.js +55 -0
  55. package/dist/services/tools/screenshot.js +19 -0
  56. package/dist/services/tools/search.js +53 -0
  57. package/dist/services/tools/skill.js +108 -0
  58. package/dist/services/tools/task.js +110 -0
  59. package/dist/services/tools/types.js +1 -0
  60. package/dist/services/tools/webFetch.js +34 -0
  61. package/dist/stores/authStore.js +178 -0
  62. package/dist/stores/index.js +6 -0
  63. package/dist/stores/memoryStore.js +191 -0
  64. package/dist/stores/persistStore.js +317 -0
  65. 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
+ }