@myassis/gateway 1.0.84 → 1.0.85
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/routes/agent.js
CHANGED
|
@@ -488,7 +488,9 @@ router.post('/sessions/:sessionId/reset', async (req, res) => {
|
|
|
488
488
|
try {
|
|
489
489
|
const userId = req.userId;
|
|
490
490
|
const { sessionId } = req.params;
|
|
491
|
-
|
|
491
|
+
// 终端标识:停止事件广播给其他终端时排除发起端
|
|
492
|
+
const clientId = req.body?.clientId || req.headers['x-client-id'] || undefined;
|
|
493
|
+
const stopped = (0, index_js_2.getSessionManager)(userId).stopSession(sessionId, clientId);
|
|
492
494
|
res.json({ success: true, data: { stopped } });
|
|
493
495
|
}
|
|
494
496
|
catch (error) {
|
|
@@ -255,16 +255,29 @@ class Session {
|
|
|
255
255
|
addUserMessage(content, userMessageId, attachments) {
|
|
256
256
|
const message = this.getUserMessage(content, userMessageId, attachments);
|
|
257
257
|
this.messages.push(message);
|
|
258
|
-
this.
|
|
258
|
+
this.enqueueMessageSave(message);
|
|
259
259
|
return message;
|
|
260
260
|
}
|
|
261
261
|
/**
|
|
262
262
|
* Add assistant message
|
|
263
263
|
*/
|
|
264
264
|
addAssistantMessage(content, toolCalls, modelName, id) {
|
|
265
|
+
const messageId = id ? id : (0, uuid_1.v4)();
|
|
266
|
+
// 本轮可能已经通过 persistAssistantProgress 落过盘并入过 this.messages,
|
|
267
|
+
// 这里必须复用同一条,否则 messages 里会出现两条同 id 的助手消息,
|
|
268
|
+
// 上下文重复、前端也会渲染出两个气泡。
|
|
269
|
+
const existing = this.messages.find(m => m.id === messageId && m.role === 'assistant');
|
|
270
|
+
if (existing) {
|
|
271
|
+
existing.content = content;
|
|
272
|
+
existing.toolCalls = toolCalls;
|
|
273
|
+
existing.modelName = modelName;
|
|
274
|
+
existing.updatedAt = Date.now();
|
|
275
|
+
this.enqueueMessageSave(existing);
|
|
276
|
+
return existing;
|
|
277
|
+
}
|
|
265
278
|
const message = {
|
|
266
279
|
sessionId: this.id,
|
|
267
|
-
id:
|
|
280
|
+
id: messageId,
|
|
268
281
|
role: 'assistant',
|
|
269
282
|
content,
|
|
270
283
|
toolCalls,
|
|
@@ -272,26 +285,72 @@ class Session {
|
|
|
272
285
|
modelName
|
|
273
286
|
};
|
|
274
287
|
this.messages.push(message);
|
|
275
|
-
this.
|
|
288
|
+
this.enqueueMessageSave(message);
|
|
276
289
|
return message;
|
|
277
290
|
}
|
|
278
291
|
/**
|
|
279
|
-
*
|
|
292
|
+
* 把消息加入待落库队列(按 id 去重)。
|
|
293
|
+
*
|
|
294
|
+
* 同一条助手消息在一轮里会被反复标记为待保存(每个工具轮次、正文就绪、收尾各一次),
|
|
295
|
+
* 队列里只保留一份引用即可——落库时读的是对象的最新字段。
|
|
296
|
+
*/
|
|
297
|
+
enqueueMessageSave(message) {
|
|
298
|
+
if (!this.toInsertMessages.some(m => m.id === message.id)) {
|
|
299
|
+
this.toInsertMessages.push(message);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* 冲刷待落库队列。
|
|
304
|
+
*
|
|
305
|
+
* 原先只在 streamChat 的 finally 里调用一次,意味着一轮对话(可能持续几分钟、
|
|
306
|
+
* 跑十几个工具)期间进程崩溃/被杀,用户消息和已经产出的助手内容会全部丢失。
|
|
307
|
+
* 现在改为在流程中的关键节点多次调用,每次都是幂等 UPSERT。
|
|
280
308
|
*/
|
|
281
309
|
async saveMessage() {
|
|
282
310
|
const store = this.store;
|
|
311
|
+
if (this.toInsertMessages.length === 0) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
// 取出快照后立刻清空:落库期间若有新消息入队,属于下一次冲刷的范围,
|
|
315
|
+
// 不能因为本次失败/成功而被连带清掉或重复写入。
|
|
316
|
+
const pending = this.toInsertMessages;
|
|
317
|
+
this.toInsertMessages = [];
|
|
283
318
|
this.updatedAt = Date.now();
|
|
284
319
|
try {
|
|
285
320
|
store.transaction(() => {
|
|
286
|
-
for (
|
|
287
|
-
store.
|
|
321
|
+
for (const message of pending) {
|
|
322
|
+
store.upsertMessage(this.id, message);
|
|
288
323
|
}
|
|
289
|
-
this.toInsertMessages = [];
|
|
290
324
|
store.updateSession(this.toStoreData());
|
|
291
325
|
});
|
|
292
326
|
}
|
|
293
327
|
catch (error) {
|
|
294
328
|
logger.error('数据保存错误:', error);
|
|
329
|
+
// 落库失败的消息重新排回队列头部,等下一个节点重试,避免直接丢数据
|
|
330
|
+
this.toInsertMessages = pending.concat(this.toInsertMessages);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* 会话进行中增量保存助手消息。
|
|
335
|
+
*
|
|
336
|
+
* 在每个工具轮次结束、以及正文流式输出完成后调用,让数据库里始终有一份
|
|
337
|
+
* 接近最新的助手消息。这样即使进程异常退出,用户也能看到已经完成的部分,
|
|
338
|
+
* 而不是整轮消失。
|
|
339
|
+
*
|
|
340
|
+
* @param content 当前已产出的正文
|
|
341
|
+
* @param toolCalls 当前已完成的工具调用
|
|
342
|
+
* @param modelName 参与本轮的模型名
|
|
343
|
+
*/
|
|
344
|
+
persistAssistantProgress(content, toolCalls, modelName) {
|
|
345
|
+
if (!this.currentMessageId)
|
|
346
|
+
return;
|
|
347
|
+
try {
|
|
348
|
+
this.addAssistantMessage(content, toolCalls, modelName, this.currentMessageId);
|
|
349
|
+
void this.saveMessage();
|
|
350
|
+
}
|
|
351
|
+
catch (error) {
|
|
352
|
+
// 增量保存是尽力而为的容灾手段,失败不能中断正在进行的对话
|
|
353
|
+
logger.warn('助手消息增量保存失败:', error?.message);
|
|
295
354
|
}
|
|
296
355
|
}
|
|
297
356
|
/**
|
|
@@ -739,6 +798,9 @@ class Session {
|
|
|
739
798
|
if (!childAgent) {
|
|
740
799
|
this.addUserMessage(content, userMessageId, attachments);
|
|
741
800
|
messages.push(this.messages.at(-1));
|
|
801
|
+
// 用户消息一旦确定就立即落库:后面的模型调用可能耗时数分钟,
|
|
802
|
+
// 期间进程退出不该让用户刚发的话凭空消失。
|
|
803
|
+
void this.saveMessage();
|
|
742
804
|
}
|
|
743
805
|
else {
|
|
744
806
|
messages.push(this.getUserMessage(content, userMessageId, attachments));
|
|
@@ -1081,6 +1143,11 @@ class Session {
|
|
|
1081
1143
|
};
|
|
1082
1144
|
await Promise.all(llmResult.toolCalls.map(x => startToolCall(x)));
|
|
1083
1145
|
toolCalls.push(toolCall);
|
|
1146
|
+
// 本轮工具已执行完,增量落库一次。工具轮次可能很多且每轮都耗时,
|
|
1147
|
+
// 这里保存能让崩溃后仍保留已完成的工具调用记录。
|
|
1148
|
+
if (!childAgent) {
|
|
1149
|
+
this.persistAssistantProgress(llmResult.content || llmResult.reasoningContent || '', toolCalls, [...new Set(modelNames)].join(','));
|
|
1150
|
+
}
|
|
1084
1151
|
// 计划模式:本轮是否调用了 updatePlan
|
|
1085
1152
|
const calledUpdatePlan = llmResult.toolCalls.some((tc) => tc.toolName === 'updatePlan');
|
|
1086
1153
|
roundsSincePlanUpdate = calledUpdatePlan ? 0 : roundsSincePlanUpdate + 1;
|
|
@@ -1147,6 +1214,9 @@ class Session {
|
|
|
1147
1214
|
}
|
|
1148
1215
|
if (!childAgent) {
|
|
1149
1216
|
this.addAssistantMessage(llmResult.content || llmResult.reasoningContent, toolCalls, [...new Set(modelNames)].join(','), this.currentMessageId);
|
|
1217
|
+
// 正文已完整产出,先落库再发 complete:
|
|
1218
|
+
// 客户端收到 complete 后就认为这条消息定稿了,此时库里必须已经有它。
|
|
1219
|
+
await this.saveMessage();
|
|
1150
1220
|
}
|
|
1151
1221
|
sendSSE(res, { type: 'complete', modelName: [...new Set(modelNames)].join(",") });
|
|
1152
1222
|
sendSSE(res, { type: '[DONE]' });
|
|
@@ -1161,7 +1231,9 @@ class Session {
|
|
|
1161
1231
|
logger.error('stream chat ', error);
|
|
1162
1232
|
if (!childAgent) {
|
|
1163
1233
|
if (toolCalls) {
|
|
1164
|
-
|
|
1234
|
+
// 用 '' 覆盖会把已经增量保存的正文清空,这里保留已有内容
|
|
1235
|
+
const saved = this.messages.find(m => m.id === this.currentMessageId && m.role === 'assistant');
|
|
1236
|
+
this.addAssistantMessage(saved?.content || '', toolCalls, [...new Set(modelNames)].join(','), this.currentMessageId);
|
|
1165
1237
|
}
|
|
1166
1238
|
if (error.message !== 'aborted') {
|
|
1167
1239
|
try {
|
|
@@ -1242,7 +1314,9 @@ class Session {
|
|
|
1242
1314
|
/**
|
|
1243
1315
|
* 停止当前正在进行的生成
|
|
1244
1316
|
*/
|
|
1245
|
-
stopGenerating() {
|
|
1317
|
+
stopGenerating(clientId) {
|
|
1318
|
+
const wasGenerating = this.isGenerating;
|
|
1319
|
+
const stoppedMessageId = this.currentMessageId;
|
|
1246
1320
|
this.isGenerating = false;
|
|
1247
1321
|
this.abortPrecompression();
|
|
1248
1322
|
// 停止生成后清除计划,避免 Desktop 的进度圈停留在未完成状态
|
|
@@ -1251,6 +1325,39 @@ class Session {
|
|
|
1251
1325
|
this.abortController.abort();
|
|
1252
1326
|
this.abortController = null;
|
|
1253
1327
|
}
|
|
1328
|
+
// 通知其他终端本轮已被停止。
|
|
1329
|
+
//
|
|
1330
|
+
// 中断走的是 abort 分支,streamChat 既不会发 error 也不会发 [DONE]
|
|
1331
|
+
// (见 catch 中的 error.message !== 'aborted' 判断),
|
|
1332
|
+
// 因此镜像终端收不到任何终止事件,会永远卡在「思考中」占位和 loading 态。
|
|
1333
|
+
// 这里必须显式广播一次。
|
|
1334
|
+
if (wasGenerating) {
|
|
1335
|
+
void this.notifySessionStopped(stoppedMessageId, clientId);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* 向其他终端广播「本轮生成已停止」。
|
|
1340
|
+
*
|
|
1341
|
+
* @param assistantMessageId 被停止的助手消息 id,接收端据此定位占位气泡
|
|
1342
|
+
* @param excludeClientId 发起停止的终端(它自己已在本地做过收尾,无需再处理)
|
|
1343
|
+
*/
|
|
1344
|
+
async notifySessionStopped(assistantMessageId, excludeClientId) {
|
|
1345
|
+
try {
|
|
1346
|
+
const webSocketService = await this.getWebSocketService();
|
|
1347
|
+
if (!webSocketService)
|
|
1348
|
+
return;
|
|
1349
|
+
webSocketService.sendToUser(String(this.userId), {
|
|
1350
|
+
type: 'session_stopped',
|
|
1351
|
+
payload: {
|
|
1352
|
+
sessionId: this.id,
|
|
1353
|
+
agentId: this.agentId,
|
|
1354
|
+
assistantMessageId,
|
|
1355
|
+
},
|
|
1356
|
+
}, { excludeClientId });
|
|
1357
|
+
}
|
|
1358
|
+
catch (error) {
|
|
1359
|
+
logger.error('推送会话停止事件失败:', error);
|
|
1360
|
+
}
|
|
1254
1361
|
}
|
|
1255
1362
|
/**
|
|
1256
1363
|
* 获取 AbortController 用于中断请求
|
|
@@ -111,14 +111,15 @@ class SessionManager {
|
|
|
111
111
|
/**
|
|
112
112
|
* Stop session generation
|
|
113
113
|
*/
|
|
114
|
-
stopSession(sessionId) {
|
|
114
|
+
stopSession(sessionId, clientId) {
|
|
115
115
|
const session = this.getSession(sessionId);
|
|
116
116
|
if (!session) {
|
|
117
117
|
logger.warn(`Session ${sessionId} not found for stop`);
|
|
118
118
|
return false;
|
|
119
119
|
}
|
|
120
120
|
if (session.isGenerating) {
|
|
121
|
-
|
|
121
|
+
// 透传发起端标识:停止事件广播时要排除它,它已在本地收尾
|
|
122
|
+
session.stopGenerating(clientId);
|
|
122
123
|
logger.info(`Stopped session ${sessionId}`);
|
|
123
124
|
return true;
|
|
124
125
|
}
|
|
@@ -81,17 +81,17 @@ class SessionStore {
|
|
|
81
81
|
}
|
|
82
82
|
// ========== Session Operations ==========
|
|
83
83
|
insertSession(session) {
|
|
84
|
-
this.db.prepare(`
|
|
85
|
-
INSERT INTO sessions (id, user_id, agent_id, title, select_model_id, voice_state, created_at, updated_at,last_message_summary,last_message_summary_at,unread_count,is_current,message_queue,message_queue_auto_execute, use_system_mode)
|
|
86
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?,?,?,?,?,?,?,?)
|
|
84
|
+
this.db.prepare(`
|
|
85
|
+
INSERT INTO sessions (id, user_id, agent_id, title, select_model_id, voice_state, created_at, updated_at,last_message_summary,last_message_summary_at,unread_count,is_current,message_queue,message_queue_auto_execute, use_system_mode)
|
|
86
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?,?,?,?,?,?,?,?)
|
|
87
87
|
`).run(session.id, session.userId, session.agentId || null, session.title, session.selectModelId || '', JSON.stringify(session.voiceState), session.createdAt, session.updatedAt, session.lastMessageSummary, session.lastMessageSummaryAt, session.unreadCount || 0, session.isCurrent ? 1 : 0, JSON.stringify(session.messageQueue || []), session.messageQueueAutoExecute === false ? 0 : 1, session.useSystemMode ? 1 : 0);
|
|
88
88
|
}
|
|
89
89
|
updateSession(session) {
|
|
90
|
-
this.db.prepare(`
|
|
91
|
-
UPDATE sessions
|
|
92
|
-
SET user_id = ?, agent_id = ?, title = ?, select_model_id = ?,
|
|
93
|
-
voice_state = ?, updated_at = ?,last_message_summary=?,last_message_summary_at=?,unread_count=?,is_current=?,message_queue=?,message_queue_auto_execute=?, use_system_mode=?
|
|
94
|
-
WHERE id = ?
|
|
90
|
+
this.db.prepare(`
|
|
91
|
+
UPDATE sessions
|
|
92
|
+
SET user_id = ?, agent_id = ?, title = ?, select_model_id = ?,
|
|
93
|
+
voice_state = ?, updated_at = ?,last_message_summary=?,last_message_summary_at=?,unread_count=?,is_current=?,message_queue=?,message_queue_auto_execute=?, use_system_mode=?
|
|
94
|
+
WHERE id = ?
|
|
95
95
|
`).run(session.userId, session.agentId || null, session.title, session.selectModelId || '', JSON.stringify(session.voiceState), session.updatedAt, session.lastMessageSummary, session.lastMessageSummaryAt, session.unreadCount || 0, session.isCurrent ? 1 : 0, JSON.stringify(session.messageQueue || []), session.messageQueueAutoExecute === false ? 0 : 1, session.useSystemMode ? 1 : 0, session.id);
|
|
96
96
|
}
|
|
97
97
|
// 支持部分更新的 updateSession
|
|
@@ -142,25 +142,48 @@ class SessionStore {
|
|
|
142
142
|
}
|
|
143
143
|
// ========== Message Operations ==========
|
|
144
144
|
insertMessage(sessionId, msg) {
|
|
145
|
-
this.db.prepare(`
|
|
146
|
-
INSERT INTO messages (id, session_id, role, content, attachments, tool_calls, created_at, updated_at, model_name, feedback)
|
|
147
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
145
|
+
this.db.prepare(`
|
|
146
|
+
INSERT INTO messages (id, session_id, role, content, attachments, tool_calls, created_at, updated_at, model_name, feedback)
|
|
147
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
148
|
+
`).run(msg.id, sessionId, msg.role, msg.content, JSON.stringify(msg.attachments || []), msg.toolCalls ? JSON.stringify(msg.toolCalls) : null, msg.createdAt, msg.updatedAt || msg.createdAt, msg.modelName || null, msg.feedback || null);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* 写入或更新消息(按 id 幂等)。
|
|
152
|
+
*
|
|
153
|
+
* 会话进行中需要多次落库同一条助手消息(工具轮次结束、正文就绪、收尾),
|
|
154
|
+
* 用 INSERT 会撞主键,用 UPDATE 首次又没有行,因此统一走 UPSERT。
|
|
155
|
+
*
|
|
156
|
+
* 冲突时刻意不更新 session_id / role / created_at:
|
|
157
|
+
* created_at 是消息列表的排序依据(findMessagesBySessionId ORDER BY created_at),
|
|
158
|
+
* 若每次落库都刷新它,同一轮的用户消息与助手消息顺序会被打乱。
|
|
159
|
+
*/
|
|
160
|
+
upsertMessage(sessionId, msg) {
|
|
161
|
+
this.db.prepare(`
|
|
162
|
+
INSERT INTO messages (id, session_id, role, content, attachments, tool_calls, created_at, updated_at, model_name, feedback)
|
|
163
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
164
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
165
|
+
content = excluded.content,
|
|
166
|
+
attachments = excluded.attachments,
|
|
167
|
+
tool_calls = excluded.tool_calls,
|
|
168
|
+
updated_at = excluded.updated_at,
|
|
169
|
+
model_name = excluded.model_name,
|
|
170
|
+
feedback = excluded.feedback
|
|
148
171
|
`).run(msg.id, sessionId, msg.role, msg.content, JSON.stringify(msg.attachments || []), msg.toolCalls ? JSON.stringify(msg.toolCalls) : null, msg.createdAt, msg.updatedAt || msg.createdAt, msg.modelName || null, msg.feedback || null);
|
|
149
172
|
}
|
|
150
173
|
insertMessages(sessionId, messages) {
|
|
151
|
-
const stmt = this.db.prepare(`
|
|
152
|
-
INSERT INTO messages (id, session_id, role, content, attachments, tool_calls,created_at, updated_at, model_name, feedback)
|
|
153
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
174
|
+
const stmt = this.db.prepare(`
|
|
175
|
+
INSERT INTO messages (id, session_id, role, content, attachments, tool_calls,created_at, updated_at, model_name, feedback)
|
|
176
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
154
177
|
`);
|
|
155
178
|
for (const msg of messages) {
|
|
156
179
|
stmt.run(msg.id, sessionId, msg.role, msg.content, JSON.stringify(msg.attachments || []), msg.toolCalls ? JSON.stringify(msg.toolCalls) : null, msg.createdAt, msg.updatedAt || msg.createdAt, msg.modelName || null, msg.feedback || null);
|
|
157
180
|
}
|
|
158
181
|
}
|
|
159
182
|
updateMessage(sessionId, msg) {
|
|
160
|
-
this.db.prepare(`
|
|
161
|
-
UPDATE messages
|
|
162
|
-
SET content = ?, attachments = ?, tool_calls = ?, updated_at = ?, model_name = ?, feedback = ?
|
|
163
|
-
WHERE id = ? AND session_id = ?
|
|
183
|
+
this.db.prepare(`
|
|
184
|
+
UPDATE messages
|
|
185
|
+
SET content = ?, attachments = ?, tool_calls = ?, updated_at = ?, model_name = ?, feedback = ?
|
|
186
|
+
WHERE id = ? AND session_id = ?
|
|
164
187
|
`).run(msg.content, JSON.stringify(msg.attachments || []), msg.toolCalls ? JSON.stringify(msg.toolCalls) : null, msg.updatedAt || msg.createdAt, msg.modelName || null, msg.feedback || null, msg.id, sessionId);
|
|
165
188
|
}
|
|
166
189
|
deleteMessage(messageId) {
|