@myassis/gateway 1.0.81 → 1.0.82
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/config/index.js
CHANGED
|
@@ -91,6 +91,10 @@ exports.appConfig = {
|
|
|
91
91
|
oldToolContentLimit: parseInt(process.env.OLD_TOOL_CONTENT_LIMIT || '500', 10),
|
|
92
92
|
/** 单次请求内最大工具调用轮数 */
|
|
93
93
|
maxToolRounds: parseInt(process.env.MAX_TOOL_ROUNDS || '200', 10),
|
|
94
|
+
/** 是否启用后台预压缩:回复结束后预先生成摘要,降低下一轮首字延迟 */
|
|
95
|
+
enablePrecompression: process.env.ENABLE_PRECOMPRESSION !== 'false',
|
|
96
|
+
/** 回复结束后延迟多久启动预压缩(毫秒),留出时间给用户连续追问 */
|
|
97
|
+
precompressionDelay: parseInt(process.env.PRECOMPRESSION_DELAY || '3000', 10),
|
|
94
98
|
/** 触发上下文压缩的字符阈值 */
|
|
95
99
|
summaryTriggerChars: parseInt(process.env.SUMMARY_TRIGGER_CHARS || '60000', 10),
|
|
96
100
|
appName: '我的助手'
|
|
@@ -38,6 +38,15 @@ const DEFAULT_CONFIG = {
|
|
|
38
38
|
summaryTriggerChars: index_js_2.appConfig.summaryTriggerChars,
|
|
39
39
|
enabled: true,
|
|
40
40
|
};
|
|
41
|
+
/**
|
|
42
|
+
* 会话级压缩任务表:sessionId -> 进行中的压缩任务。
|
|
43
|
+
*
|
|
44
|
+
* 前台请求与后台预压缩共用这张表,确保同一会话同一时刻只有一次摘要生成:
|
|
45
|
+
* 否则两边会各自调用一次摘要模型(浪费配额),并竞争写入 lastMessageSummary。
|
|
46
|
+
*/
|
|
47
|
+
const compressionTasks = new Map();
|
|
48
|
+
/** 后台预压缩的中断控制器,用于会话删除时取消 */
|
|
49
|
+
const backgroundAborts = new Map();
|
|
41
50
|
/** SSE 辅助方法:res 为 null 时跳过写入(本地执行模式) */
|
|
42
51
|
const sendSSE = (res, data) => {
|
|
43
52
|
if (!res)
|
|
@@ -103,7 +112,10 @@ class MemoryManager {
|
|
|
103
112
|
return fallback;
|
|
104
113
|
return summarized[summarized.length - 1].createdAt;
|
|
105
114
|
}
|
|
106
|
-
|
|
115
|
+
/**
|
|
116
|
+
* 制定压缩计划:只做判断与切分,不触发任何模型调用。
|
|
117
|
+
*/
|
|
118
|
+
plan() {
|
|
107
119
|
const keep = index_js_2.appConfig.historyKeep;
|
|
108
120
|
// 复制数组:getMessages() 返回的是会话内部的活引用,不可原地修改
|
|
109
121
|
let messages = [...this.session.getMessages()];
|
|
@@ -111,29 +123,25 @@ class MemoryManager {
|
|
|
111
123
|
// 子 agent 的最后一条消息由调用方单独拼接,此处排除
|
|
112
124
|
messages = messages.slice(0, -1);
|
|
113
125
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
126
|
+
const empty = {
|
|
127
|
+
action: 'none', toSummarize: [], ledger: [], retained: messages,
|
|
128
|
+
newBoundaryAt: 0, prevBoundaryAt: null, prevLedger: [], prevRetained: messages,
|
|
129
|
+
};
|
|
130
|
+
if (!this.shouldSummarize(messages) || messages.length <= keep) {
|
|
131
|
+
return empty;
|
|
119
132
|
}
|
|
120
|
-
|
|
121
|
-
if (!hasSummary) {
|
|
122
|
-
// 首次摘要生成
|
|
133
|
+
if (!this.session.lastMessageSummary) {
|
|
123
134
|
const toSummarize = messages.slice(0, -keep);
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
this.toSummaryMessage(summary, boundary, '', toSummarize),
|
|
135
|
-
...messages.slice(-keep),
|
|
136
|
-
];
|
|
135
|
+
return {
|
|
136
|
+
action: 'first',
|
|
137
|
+
toSummarize,
|
|
138
|
+
ledger: toSummarize,
|
|
139
|
+
retained: messages.slice(-keep),
|
|
140
|
+
newBoundaryAt: this.boundaryOf(toSummarize, Date.now()),
|
|
141
|
+
prevBoundaryAt: null,
|
|
142
|
+
prevLedger: [],
|
|
143
|
+
prevRetained: messages,
|
|
144
|
+
};
|
|
137
145
|
}
|
|
138
146
|
// 增量摘要:边界之后的消息才是“新消息”
|
|
139
147
|
const boundaryAt = this.session.lastMessageSummaryAt;
|
|
@@ -141,29 +149,163 @@ class MemoryManager {
|
|
|
141
149
|
// 边界之前的消息已被摘要取代,其工具调用需通过账本保留
|
|
142
150
|
const omitted = messages.filter((m) => m.createdAt <= boundaryAt);
|
|
143
151
|
if (!this.shouldSummarize(newMessages) || newMessages.length <= keep) {
|
|
144
|
-
return
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
152
|
+
return {
|
|
153
|
+
action: 'reuse',
|
|
154
|
+
toSummarize: [], ledger: [], retained: [],
|
|
155
|
+
newBoundaryAt: boundaryAt,
|
|
156
|
+
prevBoundaryAt: boundaryAt,
|
|
157
|
+
prevLedger: omitted,
|
|
158
|
+
prevRetained: newMessages,
|
|
159
|
+
};
|
|
148
160
|
}
|
|
149
|
-
// 新消息超过阈值,基于旧摘要增量重算
|
|
150
161
|
const toSummarize = newMessages.slice(0, -keep);
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
162
|
+
return {
|
|
163
|
+
action: 'incremental',
|
|
164
|
+
toSummarize,
|
|
165
|
+
ledger: [...omitted, ...toSummarize],
|
|
166
|
+
retained: newMessages.slice(-keep),
|
|
167
|
+
newBoundaryAt: this.boundaryOf(toSummarize, boundaryAt),
|
|
168
|
+
prevBoundaryAt: boundaryAt,
|
|
169
|
+
prevLedger: omitted,
|
|
170
|
+
prevRetained: newMessages,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* 执行压缩计划并写入会话。
|
|
175
|
+
*
|
|
176
|
+
* @returns 摘要文本;null 表示失败/中断(调用方应沿用旧摘要或原始历史)
|
|
177
|
+
*/
|
|
178
|
+
async runPlan(plan) {
|
|
179
|
+
if (plan.action !== 'first' && plan.action !== 'incremental')
|
|
180
|
+
return null;
|
|
181
|
+
const summary = await this.generateSummaryAsync(plan.toSummarize, plan.action === 'incremental' ? this.session.lastMessageSummary : null);
|
|
182
|
+
if (!summary)
|
|
183
|
+
return null;
|
|
184
|
+
// 并发保护:若边界已被其他压缩任务推进,说明有更新的摘要,放弃本次结果
|
|
185
|
+
if (plan.prevBoundaryAt !== null
|
|
186
|
+
&& this.session.lastMessageSummaryAt !== plan.prevBoundaryAt) {
|
|
187
|
+
logger.info('摘要边界已被其他任务推进,丢弃本次压缩结果');
|
|
188
|
+
return null;
|
|
157
189
|
}
|
|
158
|
-
const nextBoundary = this.boundaryOf(toSummarize, boundaryAt);
|
|
159
190
|
this.session.lastMessageSummary = summary;
|
|
160
|
-
this.session.lastMessageSummaryAt =
|
|
191
|
+
this.session.lastMessageSummaryAt = plan.newBoundaryAt;
|
|
161
192
|
this.session.save();
|
|
193
|
+
return summary;
|
|
194
|
+
}
|
|
195
|
+
async getHistoryMessagesAsync() {
|
|
196
|
+
// 若后台预压缩正在进行,等它完成即可直接复用结果,避免重复调用摘要模型
|
|
197
|
+
const pending = compressionTasks.get(this.session.id);
|
|
198
|
+
if (pending) {
|
|
199
|
+
logger.debug('等待进行中的压缩任务完成');
|
|
200
|
+
try {
|
|
201
|
+
await pending;
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
// 后台任务失败不影响前台,重新按当前状态决策
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const plan = this.plan();
|
|
208
|
+
if (plan.action === 'none') {
|
|
209
|
+
return plan.retained;
|
|
210
|
+
}
|
|
211
|
+
if (plan.action === 'reuse') {
|
|
212
|
+
return [
|
|
213
|
+
this.toSummaryMessage(this.session.lastMessageSummary, plan.prevBoundaryAt, '', plan.prevLedger),
|
|
214
|
+
...plan.prevRetained,
|
|
215
|
+
];
|
|
216
|
+
}
|
|
217
|
+
// 前台压缩同样登记到任务表,阻止后台预压缩重复执行
|
|
218
|
+
const task = this.runPlan(plan);
|
|
219
|
+
compressionTasks.set(this.session.id, task.then(() => undefined, () => undefined));
|
|
220
|
+
let summary = null;
|
|
221
|
+
try {
|
|
222
|
+
summary = await task;
|
|
223
|
+
}
|
|
224
|
+
finally {
|
|
225
|
+
compressionTasks.delete(this.session.id);
|
|
226
|
+
}
|
|
227
|
+
if (!summary) {
|
|
228
|
+
// 摘要失败/被中断:沿用旧摘要或退回原始历史,不写入残缺摘要
|
|
229
|
+
if (plan.action === 'incremental') {
|
|
230
|
+
return [
|
|
231
|
+
this.toSummaryMessage(this.session.lastMessageSummary, plan.prevBoundaryAt, '', plan.prevLedger),
|
|
232
|
+
...plan.prevRetained,
|
|
233
|
+
];
|
|
234
|
+
}
|
|
235
|
+
return plan.prevRetained;
|
|
236
|
+
}
|
|
162
237
|
return [
|
|
163
|
-
this.toSummaryMessage(summary,
|
|
164
|
-
...
|
|
238
|
+
this.toSummaryMessage(summary, plan.newBoundaryAt, '', plan.ledger),
|
|
239
|
+
...plan.retained,
|
|
165
240
|
];
|
|
166
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* 是否值得后台预压缩:仅当确实需要生成新摘要时才启动。
|
|
244
|
+
*/
|
|
245
|
+
needsPrecompression() {
|
|
246
|
+
const action = this.plan().action;
|
|
247
|
+
return action === 'first' || action === 'incremental';
|
|
248
|
+
}
|
|
249
|
+
// ========== 后台预压缩 ==========
|
|
250
|
+
/**
|
|
251
|
+
* 在上一轮回复结束后于后台预先生成摘要,让下一轮请求直接命中结果。
|
|
252
|
+
*
|
|
253
|
+
* 设计要点:
|
|
254
|
+
* - 与前台共用 compressionTasks 表,同一会话不会重复调用摘要模型;
|
|
255
|
+
* - 失败只记日志,绝不抛出:预压缩是纯优化,失败时前台会自行压缩;
|
|
256
|
+
* - 使用独立 AbortController,不受上一轮请求的 signal 影响
|
|
257
|
+
* (否则请求结束时 signal 被 abort,后台任务会立刻死掉)。
|
|
258
|
+
*/
|
|
259
|
+
static schedulePrecompression(session) {
|
|
260
|
+
if (!index_js_2.appConfig.enablePrecompression)
|
|
261
|
+
return;
|
|
262
|
+
if (compressionTasks.has(session.id))
|
|
263
|
+
return;
|
|
264
|
+
// 正在生成时不预压缩:消息还会继续追加,此刻的摘要边界会立即过期
|
|
265
|
+
if (session.isGenerating)
|
|
266
|
+
return;
|
|
267
|
+
const controller = new AbortController();
|
|
268
|
+
const manager = new MemoryManager(session, controller.signal, true, null);
|
|
269
|
+
if (!manager.needsPrecompression())
|
|
270
|
+
return;
|
|
271
|
+
logger.info(`启动后台预压缩: session=${session.id}`);
|
|
272
|
+
backgroundAborts.set(session.id, controller);
|
|
273
|
+
const task = (async () => {
|
|
274
|
+
const startedAt = Date.now();
|
|
275
|
+
try {
|
|
276
|
+
const plan = manager.plan();
|
|
277
|
+
if (plan.action !== 'first' && plan.action !== 'incremental')
|
|
278
|
+
return;
|
|
279
|
+
const summary = await manager.runPlan(plan);
|
|
280
|
+
if (summary) {
|
|
281
|
+
logger.info(`后台预压缩完成: session=${session.id} 耗时=${Date.now() - startedAt}ms`);
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
logger.info(`后台预压缩未产生新摘要: session=${session.id}`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
// 预压缩失败不影响任何用户可见行为
|
|
289
|
+
logger.warn(`后台预压缩失败: session=${session.id} ${error?.message}`);
|
|
290
|
+
}
|
|
291
|
+
finally {
|
|
292
|
+
compressionTasks.delete(session.id);
|
|
293
|
+
backgroundAborts.delete(session.id);
|
|
294
|
+
}
|
|
295
|
+
})();
|
|
296
|
+
compressionTasks.set(session.id, task);
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* 取消会话的后台预压缩(会话删除或用户重新发起请求时调用)
|
|
300
|
+
*/
|
|
301
|
+
static cancelPrecompression(sessionId) {
|
|
302
|
+
const controller = backgroundAborts.get(sessionId);
|
|
303
|
+
if (controller) {
|
|
304
|
+
controller.abort();
|
|
305
|
+
backgroundAborts.delete(sessionId);
|
|
306
|
+
logger.debug(`已取消后台预压缩: session=${sessionId}`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
167
309
|
/**
|
|
168
310
|
* 格式化工具调用内容,提取关键信息供摘要模型理解
|
|
169
311
|
*/
|
|
@@ -114,6 +114,8 @@ class Session {
|
|
|
114
114
|
unreadCount = 0;
|
|
115
115
|
currentMessageId = null;
|
|
116
116
|
abortController = null;
|
|
117
|
+
/** 后台预压缩的延迟定时器 */
|
|
118
|
+
precompressionTimer = null;
|
|
117
119
|
toInsertMessages;
|
|
118
120
|
constructor(data) {
|
|
119
121
|
this.id = data.id;
|
|
@@ -549,6 +551,8 @@ class Session {
|
|
|
549
551
|
if (this.isPlanCompleted()) {
|
|
550
552
|
this.clearPlan();
|
|
551
553
|
}
|
|
554
|
+
// 新请求到来:放弃排队中的预压缩,避免与前台压缩重复调用摘要模型
|
|
555
|
+
this.abortPrecompression();
|
|
552
556
|
// Add user message
|
|
553
557
|
this.getAbortController();
|
|
554
558
|
this.isGenerating = true;
|
|
@@ -1058,14 +1062,55 @@ class Session {
|
|
|
1058
1062
|
if (this.currentMessageId !== null) {
|
|
1059
1063
|
this.saveMessage();
|
|
1060
1064
|
}
|
|
1065
|
+
// 后台预压缩:本轮已结束,提前生成摘要让下一轮直接命中,降低首字延迟。
|
|
1066
|
+
// 延迟启动是为了给用户的连续追问让路(追问会取消它)。
|
|
1067
|
+
this.schedulePrecompression();
|
|
1061
1068
|
}
|
|
1062
1069
|
}
|
|
1063
1070
|
}
|
|
1071
|
+
/**
|
|
1072
|
+
* 延迟调度后台预压缩。
|
|
1073
|
+
*
|
|
1074
|
+
* 不 await:预压缩是纯优化,绝不能阻塞本轮请求的收尾。
|
|
1075
|
+
*/
|
|
1076
|
+
schedulePrecompression() {
|
|
1077
|
+
if (!index_js_2.appConfig.enablePrecompression)
|
|
1078
|
+
return;
|
|
1079
|
+
this.cancelPrecompressionTimer();
|
|
1080
|
+
this.precompressionTimer = setTimeout(() => {
|
|
1081
|
+
this.precompressionTimer = null;
|
|
1082
|
+
try {
|
|
1083
|
+
MemoryManager_js_1.MemoryManager.schedulePrecompression(this);
|
|
1084
|
+
}
|
|
1085
|
+
catch (error) {
|
|
1086
|
+
logger.warn('调度后台预压缩失败:', error?.message);
|
|
1087
|
+
}
|
|
1088
|
+
}, index_js_2.appConfig.precompressionDelay);
|
|
1089
|
+
// 不阻止进程退出
|
|
1090
|
+
this.precompressionTimer.unref?.();
|
|
1091
|
+
}
|
|
1092
|
+
/** 取消尚未触发的预压缩定时器 */
|
|
1093
|
+
cancelPrecompressionTimer() {
|
|
1094
|
+
if (this.precompressionTimer) {
|
|
1095
|
+
clearTimeout(this.precompressionTimer);
|
|
1096
|
+
this.precompressionTimer = null;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* 用户发起新请求或会话销毁时,放弃排队中/进行中的预压缩。
|
|
1101
|
+
*
|
|
1102
|
+
* 前台会自行压缩,且新消息会使预压缩的边界立即过期。
|
|
1103
|
+
*/
|
|
1104
|
+
abortPrecompression() {
|
|
1105
|
+
this.cancelPrecompressionTimer();
|
|
1106
|
+
MemoryManager_js_1.MemoryManager.cancelPrecompression(this.id);
|
|
1107
|
+
}
|
|
1064
1108
|
/**
|
|
1065
1109
|
* 停止当前正在进行的生成
|
|
1066
1110
|
*/
|
|
1067
1111
|
stopGenerating() {
|
|
1068
1112
|
this.isGenerating = false;
|
|
1113
|
+
this.abortPrecompression();
|
|
1069
1114
|
// 停止生成后清除计划,避免 Desktop 的进度圈停留在未完成状态
|
|
1070
1115
|
this.clearPlan();
|
|
1071
1116
|
if (this.abortController) {
|
|
@@ -149,6 +149,8 @@ class SessionManager {
|
|
|
149
149
|
const session = this.sessions.get(sessionId);
|
|
150
150
|
if (!session)
|
|
151
151
|
return false;
|
|
152
|
+
// 先取消后台预压缩:否则任务完成后会向已删除的会话写入摘要
|
|
153
|
+
session.abortPrecompression();
|
|
152
154
|
SessionStore_js_1.sessionStore.transaction(() => {
|
|
153
155
|
SessionStore_js_1.sessionStore.deleteMessagesBySessionId(sessionId);
|
|
154
156
|
SessionStore_js_1.sessionStore.deleteSession(sessionId);
|