@myassis/gateway 1.0.75 → 1.0.77
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 +14 -0
- package/dist/middleware/auth.js +1 -1
- package/dist/routes/agent.js +6 -6
- package/dist/routes/upload.js +3 -3
- package/dist/services/TaskSchedulerService.js +1 -1
- package/dist/services/TaskService.js +1 -1
- package/dist/services/agent/AgentManager.js +2 -2
- package/dist/services/llm/LLMClient.js +40 -22
- package/dist/services/memory/ContextBuilder.js +214 -0
- package/dist/services/memory/MemoryManager.js +195 -117
- package/dist/services/session/Session.js +57 -71
- package/dist/services/session/SessionManager.js +1 -1
- package/dist/services/systemPrompt.js +1 -1
- package/dist/services/tools/model.js +1 -1
- package/dist/services/tools/skill.js +1 -1
- package/package.json +2 -2
package/dist/config/index.js
CHANGED
|
@@ -79,6 +79,20 @@ exports.appConfig = {
|
|
|
79
79
|
clientUrl: process.env.CLIENT_URL || 'http://localhost:3000',
|
|
80
80
|
nodeEnv: process.env.NODE_ENV || defaultEnv,
|
|
81
81
|
messageKeep: 4,
|
|
82
|
+
/** 摘要后保留的原始历史消息条数 */
|
|
83
|
+
historyKeep: parseInt(process.env.HISTORY_KEEP || '4', 10),
|
|
84
|
+
/** 保留完整输出的最近工具调用轮数 */
|
|
85
|
+
toolPairKeep: parseInt(process.env.TOOL_PAIR_KEEP || '8', 10),
|
|
86
|
+
/** 极端裁剪时至少保留的工具调用轮数 */
|
|
87
|
+
minToolPairKeep: 2,
|
|
88
|
+
/** 单次请求上下文字符预算(LLMClient 硬上限为 200000,此处留出余量) */
|
|
89
|
+
contextMaxChars: parseInt(process.env.CONTEXT_MAX_CHARS || '180000', 10),
|
|
90
|
+
/** 超出 toolPairKeep 的旧工具调用内容截断长度 */
|
|
91
|
+
oldToolContentLimit: parseInt(process.env.OLD_TOOL_CONTENT_LIMIT || '500', 10),
|
|
92
|
+
/** 单次请求内最大工具调用轮数 */
|
|
93
|
+
maxToolRounds: parseInt(process.env.MAX_TOOL_ROUNDS || '100', 10),
|
|
94
|
+
/** 触发上下文压缩的字符阈值 */
|
|
95
|
+
summaryTriggerChars: parseInt(process.env.SUMMARY_TRIGGER_CHARS || '60000', 10),
|
|
82
96
|
appName: '我的助手'
|
|
83
97
|
};
|
|
84
98
|
exports.default = exports.appConfig;
|
package/dist/middleware/auth.js
CHANGED
|
@@ -9,7 +9,7 @@ exports.requireAuth = void 0;
|
|
|
9
9
|
* 注:Gateway 不自行验证 JWT 签名,而是比对 authStore 中由 Server 下发并本地持久化的 token。
|
|
10
10
|
* 因此无需也不应持有 JWT_SECRET,避免密钥在末端多处散落。
|
|
11
11
|
*/
|
|
12
|
-
const authStore_js_1 = require("
|
|
12
|
+
const authStore_js_1 = require("../stores/authStore.js");
|
|
13
13
|
const shared_1 = require("@myassis/shared");
|
|
14
14
|
const logger = (0, shared_1.getLogger)('middleware/auth');
|
|
15
15
|
/**
|
package/dist/routes/agent.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const express_1 = require("express");
|
|
4
|
-
const auth_js_1 = require("
|
|
5
|
-
const index_js_1 = require("
|
|
6
|
-
const index_js_2 = require("
|
|
7
|
-
const AgentManager_js_1 = require("
|
|
8
|
-
const AgentStore_js_1 = require("
|
|
4
|
+
const auth_js_1 = require("../middleware/auth.js");
|
|
5
|
+
const index_js_1 = require("../api/index.js");
|
|
6
|
+
const index_js_2 = require("../services/session/index.js");
|
|
7
|
+
const AgentManager_js_1 = require("../services/agent/AgentManager.js");
|
|
8
|
+
const AgentStore_js_1 = require("../services/agent/AgentStore.js");
|
|
9
9
|
const shared_1 = require("@myassis/shared");
|
|
10
10
|
const logger = (0, shared_1.getLogger)('AgentRoutes');
|
|
11
|
-
const SessionStore_js_1 = require("
|
|
11
|
+
const SessionStore_js_1 = require("../services/session/SessionStore.js");
|
|
12
12
|
const router = (0, express_1.Router)();
|
|
13
13
|
// 所有路由需要认证
|
|
14
14
|
router.use(auth_js_1.requireAuth, (req, _res, next) => {
|
package/dist/routes/upload.js
CHANGED
|
@@ -6,9 +6,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.deleteFile = void 0;
|
|
7
7
|
const express_1 = require("express");
|
|
8
8
|
const multer_1 = __importDefault(require("multer"));
|
|
9
|
-
const index_js_1 = require("
|
|
10
|
-
const auth_js_1 = require("
|
|
11
|
-
const index_js_2 = require("
|
|
9
|
+
const index_js_1 = require("../config/index.js");
|
|
10
|
+
const auth_js_1 = require("../middleware/auth.js");
|
|
11
|
+
const index_js_2 = require("../api/index.js");
|
|
12
12
|
const shared_1 = require("@myassis/shared");
|
|
13
13
|
const logger = (0, shared_1.getLogger)('UploadRoutes');
|
|
14
14
|
const router = (0, express_1.Router)();
|
|
@@ -11,7 +11,7 @@ const WebSocketService_js_1 = require("./WebSocketService.js");
|
|
|
11
11
|
const index_js_1 = require("./session/index.js");
|
|
12
12
|
const shared_1 = require("@myassis/shared");
|
|
13
13
|
const dataService_js_1 = require("./dataService.js");
|
|
14
|
-
const authStore_js_1 = require("
|
|
14
|
+
const authStore_js_1 = require("../stores/authStore.js");
|
|
15
15
|
const logger = (0, shared_1.getLogger)('TaskSchedulerService');
|
|
16
16
|
const EXPIRED_THRESHOLD = 60 * 60 * 1000; // 1小时
|
|
17
17
|
const CHECK_INTERVAL = 60 * 1000; // 1分钟
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.taskService = void 0;
|
|
13
|
-
const index_js_1 = require("
|
|
13
|
+
const index_js_1 = require("../stores/index.js");
|
|
14
14
|
const index_js_2 = require("../api/index.js");
|
|
15
15
|
const LocalTaskService_js_1 = require("./LocalTaskService.js");
|
|
16
16
|
const shared_1 = require("@myassis/shared");
|
|
@@ -7,9 +7,9 @@ exports.initAgentManager = exports.agentManagerMap = exports.AgentManager = void
|
|
|
7
7
|
const uuid_1 = require("uuid");
|
|
8
8
|
const Agent_js_1 = require("./Agent.js");
|
|
9
9
|
const SessionManager_js_1 = require("../session/SessionManager.js");
|
|
10
|
-
const authStore_js_1 = require("
|
|
10
|
+
const authStore_js_1 = require("../../stores/authStore.js");
|
|
11
11
|
const shared_1 = require("@myassis/shared");
|
|
12
|
-
const index_js_1 = __importDefault(require("
|
|
12
|
+
const index_js_1 = __importDefault(require("../../config/index.js"));
|
|
13
13
|
const logger = (0, shared_1.getLogger)('AgentManager');
|
|
14
14
|
/**
|
|
15
15
|
* AgentManager - Business logic layer
|
|
@@ -4,10 +4,17 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.getGlobalModelSelector = exports.parseAruments = exports.LLMClient = void 0;
|
|
7
|
-
const index_js_1 = require("
|
|
7
|
+
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
|
+
const index_js_2 = require("../../config/index.js");
|
|
10
11
|
const logger = (0, shared_1.getLogger)('LLMClient');
|
|
12
|
+
/** 历史工具输出在上下文中的最大保留长度 */
|
|
13
|
+
const HISTORY_TOOL_OUTPUT_LIMIT = index_js_2.appConfig.oldToolContentLimit;
|
|
14
|
+
/** 请求体字符硬上限,超过则视为上下文超限 */
|
|
15
|
+
const HARD_MAX_CHARS = 200000; /**
|
|
16
|
+
* LLM 调用结果类型
|
|
17
|
+
*/
|
|
11
18
|
function convertBase64ToImage(base64Img) {
|
|
12
19
|
// 去掉data:image/xxx;base64,前缀
|
|
13
20
|
const pureBase64 = base64Img.replace(/^data:image\/\w+;base64,/, '');
|
|
@@ -185,7 +192,7 @@ class LLMClient {
|
|
|
185
192
|
return [{
|
|
186
193
|
role: 'tool',
|
|
187
194
|
tool_call_id: m.tool_call_id,
|
|
188
|
-
content: m.content.substring(0,
|
|
195
|
+
content: m.content.substring(0, HISTORY_TOOL_OUTPUT_LIMIT) + '...(内容过长已截断)',
|
|
189
196
|
}];
|
|
190
197
|
}
|
|
191
198
|
}
|
|
@@ -222,28 +229,39 @@ class LLMClient {
|
|
|
222
229
|
tool_calls: []
|
|
223
230
|
};
|
|
224
231
|
const tools = [];
|
|
225
|
-
for (let i = 0; i < toolCall.toolCalls.length; i++) {
|
|
232
|
+
for (let i = 0; i < (toolCall.toolCalls || []).length; i++) {
|
|
226
233
|
const toolCallItem = toolCall.toolCalls[i];
|
|
227
|
-
if (toolCallItem
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
234
|
+
if (!toolCallItem?.id)
|
|
235
|
+
continue;
|
|
236
|
+
item.tool_calls.push({
|
|
237
|
+
id: toolCallItem.id,
|
|
238
|
+
function: {
|
|
239
|
+
name: toolCallItem.toolName,
|
|
240
|
+
arguments: toolCallItem.input
|
|
241
|
+
},
|
|
242
|
+
type: 'function'
|
|
243
|
+
});
|
|
244
|
+
// output 为空(历史裁剪/执行中断)时也必须补齐 tool 消息,
|
|
245
|
+
// 否则 assistant.tool_calls 缺少配对结果会被 API 拒绝
|
|
246
|
+
const rawOutput = toolCallItem.output == null
|
|
247
|
+
? '(历史记录已省略该结果)'
|
|
248
|
+
: String(toolCallItem.output);
|
|
249
|
+
tools.push({
|
|
250
|
+
role: 'tool',
|
|
251
|
+
tool_call_id: toolCallItem.id,
|
|
252
|
+
content: rawOutput.length > HISTORY_TOOL_OUTPUT_LIMIT
|
|
253
|
+
? rawOutput.substring(0, HISTORY_TOOL_OUTPUT_LIMIT) + '...(内容过长已截断)'
|
|
254
|
+
: rawOutput
|
|
255
|
+
});
|
|
242
256
|
}
|
|
243
257
|
if (item.tool_calls.length > 0) {
|
|
244
258
|
items.push(item);
|
|
245
259
|
items = items.concat(tools);
|
|
246
260
|
}
|
|
261
|
+
else if (item.content || item.reasoning_content) {
|
|
262
|
+
// 没有可用的工具调用,但思路内容仍有上下文价值,不应整体丢弃
|
|
263
|
+
items.push({ role: 'assistant', content: item.content || item.reasoning_content });
|
|
264
|
+
}
|
|
247
265
|
}
|
|
248
266
|
if (m.content) {
|
|
249
267
|
items.push({
|
|
@@ -358,9 +376,10 @@ class LLMClient {
|
|
|
358
376
|
if (this.tools && this.tools.length > 0) {
|
|
359
377
|
body.tools = this.tools;
|
|
360
378
|
}
|
|
361
|
-
const
|
|
362
|
-
logger.debug('
|
|
363
|
-
|
|
379
|
+
const bodyChars = JSON.stringify(body).length;
|
|
380
|
+
logger.debug('context chars', bodyChars);
|
|
381
|
+
// 附件/截图场景 base64 体积大但不可裁剪,跳过硬上限检查
|
|
382
|
+
if (bodyChars > HARD_MAX_CHARS && !existAttachments && !existScreenShot) {
|
|
364
383
|
throw Error('exceed max message tokens');
|
|
365
384
|
}
|
|
366
385
|
return {
|
|
@@ -591,7 +610,6 @@ class LLMClient {
|
|
|
591
610
|
'insufficient_quota',
|
|
592
611
|
'rate_limit_exceeded',
|
|
593
612
|
'model_not_found',
|
|
594
|
-
'context_length_exceeded',
|
|
595
613
|
'Model call failed',
|
|
596
614
|
'No API Key',
|
|
597
615
|
'InvalidEndpointOrModel',
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.nextTrimLevel = exports.buildContext = exports.estimateTokens = exports.estimateChars = exports.TrimLevel = void 0;
|
|
4
|
+
const shared_1 = require("@myassis/shared");
|
|
5
|
+
const index_js_1 = require("../../config/index.js");
|
|
6
|
+
const logger = (0, shared_1.getLogger)('ContextBuilder');
|
|
7
|
+
/**
|
|
8
|
+
* 上下文构造与分级裁剪
|
|
9
|
+
*
|
|
10
|
+
* 统一负责“发给模型的消息数组”的体积控制,避免裁剪逻辑散落在
|
|
11
|
+
* Session.streamChat 与 LLMClient.mapMessages 两处且互不可见。
|
|
12
|
+
*
|
|
13
|
+
* 关键约束:本模块只对 *投影副本* 做裁剪,绝不修改会话持久化数据。
|
|
14
|
+
*/
|
|
15
|
+
/** 中英混合场景下的粗略 token 估算系数 */
|
|
16
|
+
const CHARS_PER_TOKEN = 1.6;
|
|
17
|
+
/** 裁剪级别,逐级加重 */
|
|
18
|
+
var TrimLevel;
|
|
19
|
+
(function (TrimLevel) {
|
|
20
|
+
/** 不裁剪 */
|
|
21
|
+
TrimLevel[TrimLevel["None"] = 0] = "None";
|
|
22
|
+
/** 截断超出 toolPairKeep 的旧工具输出与参数 */
|
|
23
|
+
TrimLevel[TrimLevel["OldToolPayload"] = 1] = "OldToolPayload";
|
|
24
|
+
/** 进一步压缩保留轮数,并降级旧附件为占位符 */
|
|
25
|
+
TrimLevel[TrimLevel["Aggressive"] = 2] = "Aggressive";
|
|
26
|
+
/** 丢弃最早的完整工具调用对,只留最近 minToolPairKeep 轮 */
|
|
27
|
+
TrimLevel[TrimLevel["DropOldPairs"] = 3] = "DropOldPairs";
|
|
28
|
+
})(TrimLevel || (exports.TrimLevel = TrimLevel = {}));
|
|
29
|
+
/** 估算单条消息的字符数(含附件与工具参数) */
|
|
30
|
+
function messageChars(message) {
|
|
31
|
+
if (!message)
|
|
32
|
+
return 0;
|
|
33
|
+
let total = 0;
|
|
34
|
+
if (typeof message.content === 'string') {
|
|
35
|
+
total += message.content.length;
|
|
36
|
+
}
|
|
37
|
+
else if (Array.isArray(message.content)) {
|
|
38
|
+
total += JSON.stringify(message.content).length;
|
|
39
|
+
}
|
|
40
|
+
if (message.reasoning_content)
|
|
41
|
+
total += String(message.reasoning_content).length;
|
|
42
|
+
if (Array.isArray(message.tool_calls))
|
|
43
|
+
total += JSON.stringify(message.tool_calls).length;
|
|
44
|
+
if (Array.isArray(message.toolCalls))
|
|
45
|
+
total += JSON.stringify(message.toolCalls).length;
|
|
46
|
+
if (Array.isArray(message.attachments)) {
|
|
47
|
+
for (const att of message.attachments) {
|
|
48
|
+
total += (att?.url?.length || 0) + (att?.name?.length || 0);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return total;
|
|
52
|
+
}
|
|
53
|
+
/** 估算整个消息数组的字符数 */
|
|
54
|
+
function estimateChars(messages) {
|
|
55
|
+
return (messages || []).reduce((sum, m) => sum + messageChars(m), 0);
|
|
56
|
+
}
|
|
57
|
+
exports.estimateChars = estimateChars;
|
|
58
|
+
/** 估算 token 数(粗略) */
|
|
59
|
+
function estimateTokens(messages) {
|
|
60
|
+
return Math.ceil(estimateChars(messages) / CHARS_PER_TOKEN);
|
|
61
|
+
}
|
|
62
|
+
exports.estimateTokens = estimateTokens;
|
|
63
|
+
/** 按上限截断字符串,保留可读提示 */
|
|
64
|
+
function clip(text, limit) {
|
|
65
|
+
if (typeof text !== 'string' || text.length <= limit)
|
|
66
|
+
return text;
|
|
67
|
+
return text.slice(0, limit) + '...(内容过长已截断)';
|
|
68
|
+
}
|
|
69
|
+
/** 浅拷贝一条消息,保证裁剪不影响原对象 */
|
|
70
|
+
function cloneMessage(message) {
|
|
71
|
+
const copy = { ...message };
|
|
72
|
+
if (Array.isArray(message.tool_calls)) {
|
|
73
|
+
copy.tool_calls = message.tool_calls.map((tc) => ({ ...tc }));
|
|
74
|
+
}
|
|
75
|
+
if (Array.isArray(message.attachments)) {
|
|
76
|
+
copy.attachments = message.attachments.map((att) => ({ ...att }));
|
|
77
|
+
}
|
|
78
|
+
return copy;
|
|
79
|
+
}
|
|
80
|
+
/** 判断是否为“助手发起工具调用”的消息 */
|
|
81
|
+
function isToolCallAssistant(message) {
|
|
82
|
+
return message?.role === 'assistant'
|
|
83
|
+
&& Array.isArray(message.tool_calls)
|
|
84
|
+
&& message.tool_calls.length > 0;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* 将消息数组按“工具调用对”切分。
|
|
88
|
+
*
|
|
89
|
+
* 一个 pair = 1 条 assistant(tool_calls) + 其后连续的 tool 结果消息。
|
|
90
|
+
* 返回的 index 区间为左闭右开。
|
|
91
|
+
*/
|
|
92
|
+
function collectToolPairs(messages, from) {
|
|
93
|
+
const pairs = [];
|
|
94
|
+
for (let i = from; i < messages.length; i++) {
|
|
95
|
+
if (!isToolCallAssistant(messages[i]))
|
|
96
|
+
continue;
|
|
97
|
+
let end = i + 1;
|
|
98
|
+
while (end < messages.length && messages[end].role === 'tool') {
|
|
99
|
+
end++;
|
|
100
|
+
}
|
|
101
|
+
pairs.push({ start: i, end });
|
|
102
|
+
// 直接跳到该 pair 之后,避免遍历中改动循环变量导致漏检
|
|
103
|
+
i = end - 1;
|
|
104
|
+
}
|
|
105
|
+
return pairs;
|
|
106
|
+
}
|
|
107
|
+
/** 截断一个 pair 内的 content / reasoning_content / tool_calls.arguments */
|
|
108
|
+
function clipPair(messages, pair, limit) {
|
|
109
|
+
const head = messages[pair.start];
|
|
110
|
+
head.content = clip(head.content, limit);
|
|
111
|
+
head.reasoning_content = clip(head.reasoning_content, limit);
|
|
112
|
+
// 工具参数往往是上下文膨胀的主因(写文件 / patch 类调用)
|
|
113
|
+
if (Array.isArray(head.tool_calls)) {
|
|
114
|
+
for (const tc of head.tool_calls) {
|
|
115
|
+
if (typeof tc.input === 'string')
|
|
116
|
+
tc.input = clip(tc.input, limit);
|
|
117
|
+
if (tc.function && typeof tc.function.arguments === 'string') {
|
|
118
|
+
tc.function.arguments = clip(tc.function.arguments, limit);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
for (let i = pair.start + 1; i < pair.end; i++) {
|
|
123
|
+
messages[i].content = clip(messages[i].content, limit);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/** 将较早的图片 / 音频附件降级为文本占位符,避免 base64 永久占用上下文 */
|
|
127
|
+
function degradeAttachments(messages, keepLastN) {
|
|
128
|
+
const withAttachments = [];
|
|
129
|
+
messages.forEach((m, index) => {
|
|
130
|
+
if (Array.isArray(m.attachments) && m.attachments.length > 0) {
|
|
131
|
+
withAttachments.push(index);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
const degradeUntil = withAttachments.length - keepLastN;
|
|
135
|
+
for (let k = 0; k < degradeUntil; k++) {
|
|
136
|
+
const message = messages[withAttachments[k]];
|
|
137
|
+
const placeholders = message.attachments
|
|
138
|
+
.map((att) => `[${att.type || 'file'}: ${att.name || 'attachment'}]`)
|
|
139
|
+
.join(' ');
|
|
140
|
+
message.attachments = [];
|
|
141
|
+
const base = typeof message.content === 'string' ? message.content : '';
|
|
142
|
+
message.content = placeholders + (base ? `\n${base}` : '');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* 按预算对上下文做分级裁剪。
|
|
147
|
+
*
|
|
148
|
+
* @param messages 完整消息数组(不会被修改)
|
|
149
|
+
* @param protectFrom 该下标之前的消息视为“历史/摘要”,工具对裁剪只作用于其后
|
|
150
|
+
* @param maxChars 字符预算
|
|
151
|
+
* @param minLevel 最低裁剪级别(用于超限重试时强制加重)
|
|
152
|
+
*/
|
|
153
|
+
function buildContext(messages, protectFrom, maxChars = index_js_1.appConfig.contextMaxChars, minLevel = TrimLevel.OldToolPayload) {
|
|
154
|
+
let working = messages.map(cloneMessage);
|
|
155
|
+
let level = TrimLevel.None;
|
|
156
|
+
const overBudget = () => estimateChars(working) > maxChars;
|
|
157
|
+
const applyLevel = (target) => {
|
|
158
|
+
level = target;
|
|
159
|
+
working = messages.map(cloneMessage);
|
|
160
|
+
const pairs = collectToolPairs(working, protectFrom);
|
|
161
|
+
const keep = target >= TrimLevel.Aggressive
|
|
162
|
+
? index_js_1.appConfig.minToolPairKeep
|
|
163
|
+
: index_js_1.appConfig.toolPairKeep;
|
|
164
|
+
// 1) 截断超出 keep 轮的旧工具调用负载
|
|
165
|
+
const clipUntil = Math.max(0, pairs.length - keep);
|
|
166
|
+
for (let i = 0; i < clipUntil; i++) {
|
|
167
|
+
clipPair(working, pairs[i], index_js_1.appConfig.oldToolContentLimit);
|
|
168
|
+
}
|
|
169
|
+
// 2) 降级旧附件
|
|
170
|
+
if (target >= TrimLevel.Aggressive) {
|
|
171
|
+
degradeAttachments(working, 1);
|
|
172
|
+
}
|
|
173
|
+
// 3) 整对丢弃最早的工具调用(保持 assistant/tool 配对完整,避免协议错误)
|
|
174
|
+
if (target >= TrimLevel.DropOldPairs && pairs.length > index_js_1.appConfig.minToolPairKeep) {
|
|
175
|
+
const dropCount = pairs.length - index_js_1.appConfig.minToolPairKeep;
|
|
176
|
+
const dropIndexes = new Set();
|
|
177
|
+
for (let i = 0; i < dropCount; i++) {
|
|
178
|
+
for (let j = pairs[i].start; j < pairs[i].end; j++) {
|
|
179
|
+
dropIndexes.add(j);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const dropped = working.filter((_, index) => !dropIndexes.has(index));
|
|
183
|
+
// 用一条系统提示替代被丢弃的轮次,避免模型认为自己没做过这些事
|
|
184
|
+
dropped.splice(protectFrom, 0, {
|
|
185
|
+
role: 'system',
|
|
186
|
+
content: `(已省略 ${dropCount} 轮较早的工具调用记录以控制上下文长度)`,
|
|
187
|
+
attachments: [],
|
|
188
|
+
});
|
|
189
|
+
working = dropped;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
for (const target of [minLevel, TrimLevel.Aggressive, TrimLevel.DropOldPairs]) {
|
|
193
|
+
if (target < level)
|
|
194
|
+
continue;
|
|
195
|
+
applyLevel(target);
|
|
196
|
+
if (!overBudget())
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
const chars = estimateChars(working);
|
|
200
|
+
if (chars > maxChars) {
|
|
201
|
+
logger.warn(`上下文仍超出预算: ${chars} > ${maxChars}(已用最高裁剪级别 ${level})`);
|
|
202
|
+
}
|
|
203
|
+
return { messages: working, chars, level, trimmed: level > TrimLevel.None };
|
|
204
|
+
}
|
|
205
|
+
exports.buildContext = buildContext;
|
|
206
|
+
/** 依据当前级别推导下一个更重的裁剪级别,用于超限重试 */
|
|
207
|
+
function nextTrimLevel(level) {
|
|
208
|
+
if (level < TrimLevel.Aggressive)
|
|
209
|
+
return TrimLevel.Aggressive;
|
|
210
|
+
if (level < TrimLevel.DropOldPairs)
|
|
211
|
+
return TrimLevel.DropOldPairs;
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
exports.nextTrimLevel = nextTrimLevel;
|
|
@@ -28,11 +28,13 @@ const shared_1 = require("@myassis/shared");
|
|
|
28
28
|
const logger = (0, shared_1.getLogger)('MemoryManager');
|
|
29
29
|
const models_js_1 = require("../models.js");
|
|
30
30
|
const dataService_js_1 = require("../dataService.js");
|
|
31
|
-
const index_js_1 = require("
|
|
31
|
+
const index_js_1 = require("../../stores/index.js");
|
|
32
32
|
const LLMClient_js_1 = require("../llm/LLMClient.js");
|
|
33
|
-
const index_js_2 = require("
|
|
33
|
+
const index_js_2 = require("../../config/index.js");
|
|
34
|
+
const ContextBuilder_js_1 = require("./ContextBuilder.js");
|
|
34
35
|
const DEFAULT_CONFIG = {
|
|
35
36
|
summaryThreshold: 10,
|
|
37
|
+
summaryTriggerChars: index_js_2.appConfig.summaryTriggerChars,
|
|
36
38
|
enabled: true,
|
|
37
39
|
};
|
|
38
40
|
/** SSE 辅助方法:res 为 null 时跳过写入(本地执行模式) */
|
|
@@ -52,6 +54,7 @@ class MemoryManager {
|
|
|
52
54
|
config = DEFAULT_CONFIG;
|
|
53
55
|
childAgent;
|
|
54
56
|
res;
|
|
57
|
+
summaryModels = null;
|
|
55
58
|
constructor(session, signal, childAgent, res = null) {
|
|
56
59
|
this.session = session;
|
|
57
60
|
this.signal = signal;
|
|
@@ -68,52 +71,86 @@ class MemoryManager {
|
|
|
68
71
|
modelName,
|
|
69
72
|
};
|
|
70
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* 是否需要触发压缩:消息条数或字符体积任一超阈值
|
|
76
|
+
*/
|
|
77
|
+
shouldSummarize(messages) {
|
|
78
|
+
if (!this.config.enabled)
|
|
79
|
+
return false;
|
|
80
|
+
if (messages.length >= this.config.summaryThreshold)
|
|
81
|
+
return true;
|
|
82
|
+
// 单条工具结果可能极大,条数少也可能远超上下文预算
|
|
83
|
+
return (0, ContextBuilder_js_1.estimateChars)(messages) >= this.config.summaryTriggerChars;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* 取得“最后一条被摘要消息”的时间戳作为摘要边界。
|
|
87
|
+
*
|
|
88
|
+
* 不能用 Date.now():保留窗口内的消息 createdAt 均小于当前时间,
|
|
89
|
+
* 下一轮按时间过滤时会既不在摘要中、也不在历史中,造成上下文空洞。
|
|
90
|
+
*/
|
|
91
|
+
boundaryOf(summarized, fallback) {
|
|
92
|
+
if (!summarized || summarized.length === 0)
|
|
93
|
+
return fallback;
|
|
94
|
+
return summarized[summarized.length - 1].createdAt;
|
|
95
|
+
}
|
|
71
96
|
async getHistoryMessagesAsync() {
|
|
72
|
-
|
|
73
|
-
|
|
97
|
+
const keep = index_js_2.appConfig.historyKeep;
|
|
98
|
+
// 复制数组:getMessages() 返回的是会话内部的活引用,不可原地修改
|
|
99
|
+
let messages = [...this.session.getMessages()];
|
|
74
100
|
if (this.childAgent) {
|
|
75
|
-
|
|
101
|
+
// 子 agent 的最后一条消息由调用方单独拼接,此处排除
|
|
102
|
+
messages = messages.slice(0, -1);
|
|
103
|
+
}
|
|
104
|
+
if (!this.shouldSummarize(messages)) {
|
|
105
|
+
return messages;
|
|
76
106
|
}
|
|
77
|
-
|
|
78
|
-
if (messages.length < this.config.summaryThreshold) {
|
|
107
|
+
if (messages.length <= keep) {
|
|
79
108
|
return messages;
|
|
80
109
|
}
|
|
81
|
-
// 3. 检查是否有摘要
|
|
82
110
|
const hasSummary = this.session.lastMessageSummary;
|
|
83
111
|
if (!hasSummary) {
|
|
84
|
-
//
|
|
85
|
-
const
|
|
112
|
+
// 首次摘要生成
|
|
113
|
+
const toSummarize = messages.slice(0, -keep);
|
|
114
|
+
const summary = await this.generateSummaryAsync(toSummarize, null);
|
|
115
|
+
if (!summary) {
|
|
116
|
+
// 摘要失败/被中断:退回原始历史,不写入残缺摘要
|
|
117
|
+
return messages;
|
|
118
|
+
}
|
|
119
|
+
const boundary = this.boundaryOf(toSummarize, Date.now());
|
|
86
120
|
this.session.lastMessageSummary = summary;
|
|
87
|
-
this.session.lastMessageSummaryAt =
|
|
121
|
+
this.session.lastMessageSummaryAt = boundary;
|
|
88
122
|
this.session.save();
|
|
89
123
|
return [
|
|
90
|
-
this.toSummaryMessage(summary,
|
|
91
|
-
...messages.slice(-
|
|
124
|
+
this.toSummaryMessage(summary, boundary, ''),
|
|
125
|
+
...messages.slice(-keep),
|
|
92
126
|
];
|
|
93
127
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
...fromSummary,
|
|
103
|
-
];
|
|
104
|
-
}
|
|
105
|
-
else {
|
|
106
|
-
// 新消息超过阈值条,重新生成摘要
|
|
107
|
-
const summary = await this.generateSummaryAsync(newMessages.slice(0, -index_js_2.appConfig.messageKeep), this.session.lastMessageSummary);
|
|
108
|
-
this.session.lastMessageSummary = summary;
|
|
109
|
-
this.session.lastMessageSummaryAt = Date.now();
|
|
110
|
-
this.session.save();
|
|
111
|
-
return [
|
|
112
|
-
this.toSummaryMessage(summary, this.session.lastMessageSummaryAt, ''),
|
|
113
|
-
...messages.slice(-index_js_2.appConfig.messageKeep),
|
|
114
|
-
];
|
|
115
|
-
}
|
|
128
|
+
// 增量摘要:边界之后的消息才是“新消息”
|
|
129
|
+
const boundaryAt = this.session.lastMessageSummaryAt;
|
|
130
|
+
const newMessages = messages.filter((m) => m.createdAt > boundaryAt);
|
|
131
|
+
if (!this.shouldSummarize(newMessages) || newMessages.length <= keep) {
|
|
132
|
+
return [
|
|
133
|
+
this.toSummaryMessage(this.session.lastMessageSummary, boundaryAt, ''),
|
|
134
|
+
...newMessages,
|
|
135
|
+
];
|
|
116
136
|
}
|
|
137
|
+
// 新消息超过阈值,基于旧摘要增量重算
|
|
138
|
+
const toSummarize = newMessages.slice(0, -keep);
|
|
139
|
+
const summary = await this.generateSummaryAsync(toSummarize, this.session.lastMessageSummary);
|
|
140
|
+
if (!summary) {
|
|
141
|
+
return [
|
|
142
|
+
this.toSummaryMessage(this.session.lastMessageSummary, boundaryAt, ''),
|
|
143
|
+
...newMessages,
|
|
144
|
+
];
|
|
145
|
+
}
|
|
146
|
+
const nextBoundary = this.boundaryOf(toSummarize, boundaryAt);
|
|
147
|
+
this.session.lastMessageSummary = summary;
|
|
148
|
+
this.session.lastMessageSummaryAt = nextBoundary;
|
|
149
|
+
this.session.save();
|
|
150
|
+
return [
|
|
151
|
+
this.toSummaryMessage(summary, nextBoundary, ''),
|
|
152
|
+
...newMessages.slice(-keep),
|
|
153
|
+
];
|
|
117
154
|
}
|
|
118
155
|
/**
|
|
119
156
|
* 格式化工具调用内容,提取关键信息供摘要模型理解
|
|
@@ -182,38 +219,50 @@ class MemoryManager {
|
|
|
182
219
|
.filter((m) => m.role === 'user' || m.role === 'assistant')
|
|
183
220
|
.map((m) => this.formatMessage(m))
|
|
184
221
|
.join('\n\n');
|
|
185
|
-
return `你是一个对话摘要专家。请分析以下对话内容,生成一份结构化的摘要。
|
|
186
|
-
|
|
187
|
-
重要规则:
|
|
188
|
-
1. 如果对话涉及代码开发/项目工作,必须详细总结项目结构,包括:技术栈、目录结构、关键文件及其职责、模块间关系
|
|
189
|
-
2. 项目结构描述不限字数,务必完整清晰
|
|
190
|
-
3. 保留所有关键决策、修改内容和上下文
|
|
191
|
-
4. 保留错误信息和解决方案,避免重复犯错
|
|
192
|
-
5. 如果有上一轮摘要,合并其中的关键信息
|
|
193
|
-
|
|
194
|
-
${lastSummary ? '=== 上一轮摘要 ===\n' + lastSummary + '\n\n' : ''}=== 对话内容 ===
|
|
195
|
-
${conversation}
|
|
196
|
-
|
|
197
|
-
=== 输出格式 ===
|
|
198
|
-
请按以下格式输出(各部分字数不限,以完整准确为优先):
|
|
199
|
-
|
|
200
|
-
【摘要】简要概括对话主题和关键进展
|
|
201
|
-
|
|
202
|
-
【项目结构】(如涉及项目开发)详细描述:
|
|
203
|
-
- 技术栈与框架
|
|
204
|
-
- 目录结构与各目录职责
|
|
205
|
-
- 关键文件及其作用
|
|
206
|
-
- 模块间依赖关系
|
|
207
|
-
- 已完成和待完成的功能
|
|
208
|
-
|
|
209
|
-
【关键决策】列出重要决策及原因
|
|
210
|
-
|
|
211
|
-
【要点】列出3-8个关键要点
|
|
212
|
-
|
|
222
|
+
return `你是一个对话摘要专家。请分析以下对话内容,生成一份结构化的摘要。
|
|
223
|
+
|
|
224
|
+
重要规则:
|
|
225
|
+
1. 如果对话涉及代码开发/项目工作,必须详细总结项目结构,包括:技术栈、目录结构、关键文件及其职责、模块间关系
|
|
226
|
+
2. 项目结构描述不限字数,务必完整清晰
|
|
227
|
+
3. 保留所有关键决策、修改内容和上下文
|
|
228
|
+
4. 保留错误信息和解决方案,避免重复犯错
|
|
229
|
+
5. 如果有上一轮摘要,合并其中的关键信息
|
|
230
|
+
|
|
231
|
+
${lastSummary ? '=== 上一轮摘要 ===\n' + lastSummary + '\n\n' : ''}=== 对话内容 ===
|
|
232
|
+
${conversation}
|
|
233
|
+
|
|
234
|
+
=== 输出格式 ===
|
|
235
|
+
请按以下格式输出(各部分字数不限,以完整准确为优先):
|
|
236
|
+
|
|
237
|
+
【摘要】简要概括对话主题和关键进展
|
|
238
|
+
|
|
239
|
+
【项目结构】(如涉及项目开发)详细描述:
|
|
240
|
+
- 技术栈与框架
|
|
241
|
+
- 目录结构与各目录职责
|
|
242
|
+
- 关键文件及其作用
|
|
243
|
+
- 模块间依赖关系
|
|
244
|
+
- 已完成和待完成的功能
|
|
245
|
+
|
|
246
|
+
【关键决策】列出重要决策及原因
|
|
247
|
+
|
|
248
|
+
【要点】列出3-8个关键要点
|
|
249
|
+
|
|
213
250
|
【错误与修复】列出遇到的错误及解决方案,标注"⚠️"避免重复`;
|
|
214
251
|
}
|
|
252
|
+
/**
|
|
253
|
+
* 获取摘要用的模型列表(避免在分批循环中重复请求)
|
|
254
|
+
*/
|
|
255
|
+
async getSummaryModels() {
|
|
256
|
+
if (!this.summaryModels) {
|
|
257
|
+
const token = index_js_1.authStore.get(this.session.userId).accessToken;
|
|
258
|
+
this.summaryModels = (await dataService_js_1.modelsService.list(token)).data.map((x) => (0, models_js_1.toModel)(x));
|
|
259
|
+
}
|
|
260
|
+
return this.summaryModels;
|
|
261
|
+
}
|
|
215
262
|
/**
|
|
216
263
|
* 生成对话摘要(支持分层摘要)
|
|
264
|
+
*
|
|
265
|
+
* 返回 null 表示摘要失败或被中断,调用方应保留原始历史而非写入残缺摘要。
|
|
217
266
|
*/
|
|
218
267
|
async generateSummaryAsync(messages, lastSummary) {
|
|
219
268
|
// 非 childAgent 会话通知 Desktop 显示"上下文压缩中"
|
|
@@ -231,20 +280,23 @@ ${conversation}
|
|
|
231
280
|
return await this.hierarchicalSummary(formattedMessages, lastSummary);
|
|
232
281
|
}
|
|
233
282
|
const summaryPrompt = this.buildSummaryPrompt(messages, lastSummary);
|
|
234
|
-
const
|
|
235
|
-
const llmClient = new LLMClient_js_1.LLMClient((await dataService_js_1.modelsService.list(token)).data.map((x) => (0, models_js_1.toModel)(x)), [{ role: 'user', content: summaryPrompt }], this.signal, [], 600000);
|
|
283
|
+
const llmClient = new LLMClient_js_1.LLMClient(await this.getSummaryModels(), [{ role: 'user', content: summaryPrompt }], this.signal, [], 600000);
|
|
236
284
|
if (this.session.useSystemMode) {
|
|
237
285
|
const { getRequestToken, getServerBaseUrl } = await Promise.resolve().then(() => __importStar(require('../../api/index.js')));
|
|
238
286
|
llmClient.setSystemMode(true, this.session.selectModelId, getServerBaseUrl(), getRequestToken());
|
|
239
287
|
}
|
|
240
|
-
|
|
241
|
-
|
|
288
|
+
let content = '';
|
|
289
|
+
try {
|
|
290
|
+
const response = await llmClient.Chat();
|
|
291
|
+
content = (response.content || '').trim();
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
logger.warn(`摘要生成失败,保留原始历史: ${error?.message}`);
|
|
295
|
+
return lastSummary || null;
|
|
296
|
+
}
|
|
242
297
|
if (!content) {
|
|
243
|
-
logger.warn('摘要内容为空,保留旧摘要'
|
|
244
|
-
|
|
245
|
-
responseKeys: Object.keys(response),
|
|
246
|
-
});
|
|
247
|
-
return lastSummary || '(摘要生成失败,内容为空)';
|
|
298
|
+
logger.warn('摘要内容为空,保留旧摘要');
|
|
299
|
+
return lastSummary || null;
|
|
248
300
|
}
|
|
249
301
|
return content;
|
|
250
302
|
}
|
|
@@ -253,42 +305,68 @@ ${conversation}
|
|
|
253
305
|
*/
|
|
254
306
|
async hierarchicalSummary(formattedMessages, lastSummary) {
|
|
255
307
|
const BATCH_SIZE = 15; // 每批消息数
|
|
256
|
-
const
|
|
308
|
+
const CONCURRENCY = 3; // 子摘要并发上限
|
|
309
|
+
const batches = [];
|
|
257
310
|
for (let i = 0; i < formattedMessages.length; i += BATCH_SIZE) {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
311
|
+
batches.push(formattedMessages.slice(i, i + BATCH_SIZE));
|
|
312
|
+
}
|
|
313
|
+
const models = await this.getSummaryModels();
|
|
314
|
+
const summarizeBatch = async (batch) => {
|
|
261
315
|
const batchPrompt = `请简洁总结以下对话片段的关键信息,特别关注:项目结构、关键决策、错误及修复。\n\n${batch.join('\n\n')}`;
|
|
262
|
-
const
|
|
263
|
-
const llmClient = new LLMClient_js_1.LLMClient((await dataService_js_1.modelsService.list(token)).data.map((x) => (0, models_js_1.toModel)(x)), [{ role: 'user', content: batchPrompt }], this.signal, [], 600000);
|
|
316
|
+
const llmClient = new LLMClient_js_1.LLMClient(models, [{ role: 'user', content: batchPrompt }], this.signal, [], 600000);
|
|
264
317
|
if (this.session.useSystemMode) {
|
|
265
318
|
const { getRequestToken, getServerBaseUrl } = await Promise.resolve().then(() => __importStar(require('../../api/index.js')));
|
|
266
319
|
llmClient.setSystemMode(true, this.session.selectModelId, getServerBaseUrl(), getRequestToken());
|
|
267
320
|
}
|
|
268
321
|
const response = await llmClient.Chat();
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
322
|
+
return (response.content || '').trim();
|
|
323
|
+
};
|
|
324
|
+
// 保持批次顺序,同时限制并发
|
|
325
|
+
const subSummaries = new Array(batches.length).fill('');
|
|
326
|
+
let aborted = false;
|
|
327
|
+
for (let i = 0; i < batches.length; i += CONCURRENCY) {
|
|
328
|
+
if (this.signal.aborted) {
|
|
329
|
+
aborted = true;
|
|
330
|
+
break;
|
|
272
331
|
}
|
|
332
|
+
const slice = batches.slice(i, i + CONCURRENCY);
|
|
333
|
+
const results = await Promise.allSettled(slice.map((batch) => summarizeBatch(batch)));
|
|
334
|
+
results.forEach((result, offset) => {
|
|
335
|
+
if (result.status === 'fulfilled' && result.value) {
|
|
336
|
+
subSummaries[i + offset] = result.value;
|
|
337
|
+
}
|
|
338
|
+
else if (result.status === 'rejected') {
|
|
339
|
+
logger.warn(`子摘要生成失败: ${result.reason?.message}`);
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
// 被中断时不写入残缺摘要,交由调用方回退到原始历史
|
|
344
|
+
if (aborted || this.signal.aborted) {
|
|
345
|
+
return lastSummary || null;
|
|
273
346
|
}
|
|
274
|
-
|
|
275
|
-
if (
|
|
276
|
-
return lastSummary ||
|
|
347
|
+
const valid = subSummaries.filter((x) => !!x);
|
|
348
|
+
if (valid.length === 0) {
|
|
349
|
+
return lastSummary || null;
|
|
277
350
|
}
|
|
278
|
-
if (
|
|
279
|
-
return
|
|
351
|
+
if (valid.length === 1) {
|
|
352
|
+
return valid[0];
|
|
280
353
|
}
|
|
281
354
|
// 合并子摘要为最终摘要
|
|
282
|
-
const mergePrompt = this.buildMergePrompt(
|
|
283
|
-
const
|
|
284
|
-
const mergeLlmClient = new LLMClient_js_1.LLMClient((await dataService_js_1.modelsService.list(token)).data.map((x) => (0, models_js_1.toModel)(x)), [{ role: 'user', content: mergePrompt }], this.signal, [], 600000);
|
|
355
|
+
const mergePrompt = this.buildMergePrompt(valid, lastSummary);
|
|
356
|
+
const mergeLlmClient = new LLMClient_js_1.LLMClient(models, [{ role: 'user', content: mergePrompt }], this.signal, [], 600000);
|
|
285
357
|
if (this.session.useSystemMode) {
|
|
286
358
|
const { getRequestToken, getServerBaseUrl } = await Promise.resolve().then(() => __importStar(require('../../api/index.js')));
|
|
287
359
|
mergeLlmClient.setSystemMode(true, this.session.selectModelId, getServerBaseUrl(), getRequestToken());
|
|
288
360
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
361
|
+
try {
|
|
362
|
+
const mergeResponse = await mergeLlmClient.Chat();
|
|
363
|
+
const mergedContent = (mergeResponse.content || '').trim();
|
|
364
|
+
return mergedContent || valid.join('\n\n');
|
|
365
|
+
}
|
|
366
|
+
catch (error) {
|
|
367
|
+
logger.warn(`摘要合并失败,回退为拼接: ${error?.message}`);
|
|
368
|
+
return valid.join('\n\n');
|
|
369
|
+
}
|
|
292
370
|
}
|
|
293
371
|
/**
|
|
294
372
|
* 构建合并摘要提示词
|
|
@@ -297,31 +375,31 @@ ${conversation}
|
|
|
297
375
|
const summaries = subSummaries
|
|
298
376
|
.map((s, i) => `--- 片段 ${i + 1} ---\n${s}`)
|
|
299
377
|
.join('\n\n');
|
|
300
|
-
return `你是一个对话摘要专家。以下是多个对话片段的摘要,请合并为一份完整的结构化摘要。
|
|
301
|
-
|
|
302
|
-
重要规则:
|
|
303
|
-
1. 合并重复信息,去重项目结构描述
|
|
304
|
-
2. 如果涉及项目开发,务必完整保留项目结构信息(技术栈、目录结构、关键文件、模块关系)
|
|
305
|
-
3. 按时间顺序整理决策和进展
|
|
306
|
-
4. 保留所有错误信息及解决方案
|
|
307
|
-
|
|
308
|
-
${lastSummary ? '=== 上一轮摘要 ===\n' + lastSummary + '\n\n' : ''}=== 分片段摘要 ===
|
|
309
|
-
${summaries}
|
|
310
|
-
|
|
311
|
-
=== 输出格式 ===
|
|
312
|
-
【摘要】简要概括对话主题和关键进展
|
|
313
|
-
|
|
314
|
-
【项目结构】(如涉及项目开发)详细描述:
|
|
315
|
-
- 技术栈与框架
|
|
316
|
-
- 目录结构与各目录职责
|
|
317
|
-
- 关键文件及其作用
|
|
318
|
-
- 模块间依赖关系
|
|
319
|
-
- 已完成和待完成的功能
|
|
320
|
-
|
|
321
|
-
【关键决策】列出重要决策及原因
|
|
322
|
-
|
|
323
|
-
【要点】列出3-8个关键要点
|
|
324
|
-
|
|
378
|
+
return `你是一个对话摘要专家。以下是多个对话片段的摘要,请合并为一份完整的结构化摘要。
|
|
379
|
+
|
|
380
|
+
重要规则:
|
|
381
|
+
1. 合并重复信息,去重项目结构描述
|
|
382
|
+
2. 如果涉及项目开发,务必完整保留项目结构信息(技术栈、目录结构、关键文件、模块关系)
|
|
383
|
+
3. 按时间顺序整理决策和进展
|
|
384
|
+
4. 保留所有错误信息及解决方案
|
|
385
|
+
|
|
386
|
+
${lastSummary ? '=== 上一轮摘要 ===\n' + lastSummary + '\n\n' : ''}=== 分片段摘要 ===
|
|
387
|
+
${summaries}
|
|
388
|
+
|
|
389
|
+
=== 输出格式 ===
|
|
390
|
+
【摘要】简要概括对话主题和关键进展
|
|
391
|
+
|
|
392
|
+
【项目结构】(如涉及项目开发)详细描述:
|
|
393
|
+
- 技术栈与框架
|
|
394
|
+
- 目录结构与各目录职责
|
|
395
|
+
- 关键文件及其作用
|
|
396
|
+
- 模块间依赖关系
|
|
397
|
+
- 已完成和待完成的功能
|
|
398
|
+
|
|
399
|
+
【关键决策】列出重要决策及原因
|
|
400
|
+
|
|
401
|
+
【要点】列出3-8个关键要点
|
|
402
|
+
|
|
325
403
|
【错误与修复】列出遇到的错误及解决方案,标注"⚠️"避免重复`;
|
|
326
404
|
}
|
|
327
405
|
}
|
|
@@ -27,15 +27,16 @@ exports.Session = exports.handleApprovalResponse = exports.registerApprovalWaite
|
|
|
27
27
|
const uuid_1 = require("uuid");
|
|
28
28
|
const shared_1 = require("@myassis/shared");
|
|
29
29
|
const SessionStore_js_1 = require("./SessionStore.js");
|
|
30
|
-
const AgentStore_js_1 = require("
|
|
31
|
-
const authStore_js_1 = require("
|
|
32
|
-
const models_js_1 = require("
|
|
33
|
-
const index_js_1 = require("
|
|
30
|
+
const AgentStore_js_1 = require("../../services/agent/AgentStore.js");
|
|
31
|
+
const authStore_js_1 = require("../../stores/authStore.js");
|
|
32
|
+
const models_js_1 = require("../../services/models.js");
|
|
33
|
+
const index_js_1 = require("../../services/tools/index.js");
|
|
34
34
|
const systemPrompt_js_1 = require("../systemPrompt.js");
|
|
35
35
|
const LLMClient_js_1 = require("../llm/LLMClient.js");
|
|
36
|
-
const dataService_js_1 = require("
|
|
36
|
+
const dataService_js_1 = require("../../services/dataService.js");
|
|
37
37
|
const MemoryManager_js_1 = require("../memory/MemoryManager.js");
|
|
38
|
-
const index_js_2 = require("
|
|
38
|
+
const index_js_2 = require("../../config/index.js");
|
|
39
|
+
const ContextBuilder_js_1 = require("../memory/ContextBuilder.js");
|
|
39
40
|
const SessionManager_js_1 = require("./SessionManager.js");
|
|
40
41
|
const logger = (0, shared_1.getLogger)('Session');
|
|
41
42
|
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
@@ -570,82 +571,62 @@ class Session {
|
|
|
570
571
|
let modelNames = [];
|
|
571
572
|
//用于后续的消息滑动处理
|
|
572
573
|
const messagesLength = messages.length;
|
|
573
|
-
const keep = index_js_2.appConfig.messageKeep;
|
|
574
|
-
//具体调用逻辑函数
|
|
575
|
-
// 辅助函数:截断消息内容
|
|
576
|
-
const truncateMessageContent = (message) => {
|
|
577
|
-
if (message.content && message.content.length > 100) {
|
|
578
|
-
message.content = message.content.substring(0, 100) + '...(内容过长已截断)';
|
|
579
|
-
}
|
|
580
|
-
if (message.reasoning_content && message.reasoning_content.length > 100) {
|
|
581
|
-
message.reasoning_content = message.reasoning_content.substring(0, 100) + '...(内容过长已截断)';
|
|
582
|
-
}
|
|
583
|
-
};
|
|
584
574
|
const toolCalls = [];
|
|
585
575
|
// 获取本地工具定义
|
|
586
576
|
const tools = (0, index_js_1.getToolDefinitions)();
|
|
587
577
|
const models = this.useSystemMode
|
|
588
578
|
? (await dataService_js_1.modelsService.listSystem()).data.map(x => (0, models_js_1.toSystemModel)(x))
|
|
589
579
|
: (await dataService_js_1.modelsService.list(token)).data.map(x => (0, models_js_1.toModel)(x));
|
|
590
|
-
//
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
toolPairCount++;
|
|
612
|
-
}
|
|
580
|
+
// 工具调用轮次计数,防止无限循环
|
|
581
|
+
let toolRound = 0;
|
|
582
|
+
// 当前裁剪级别,遇到上下文超限时逐级加重
|
|
583
|
+
let trimLevel = ContextBuilder_js_1.TrimLevel.OldToolPayload;
|
|
584
|
+
/** 判断错误是否为上下文超限 */
|
|
585
|
+
const isContextOverflow = (error) => {
|
|
586
|
+
const text = String(error?.message || error);
|
|
587
|
+
return text.includes('exceed max message tokens')
|
|
588
|
+
|| text.includes('context_length_exceeded');
|
|
589
|
+
};
|
|
590
|
+
/**
|
|
591
|
+
* 调用模型:先按预算裁剪上下文,超限时逐级加重裁剪后重试。
|
|
592
|
+
*
|
|
593
|
+
* 裁剪只作用于投影副本,不会修改 messages / this.messages 中的原始数据。
|
|
594
|
+
*/
|
|
595
|
+
const callModel = async () => {
|
|
596
|
+
let lastError;
|
|
597
|
+
while (true) {
|
|
598
|
+
const built = (0, ContextBuilder_js_1.buildContext)(messages, messagesLength, index_js_2.appConfig.contextMaxChars, trimLevel);
|
|
599
|
+
if (built.trimmed) {
|
|
600
|
+
logger.debug(`上下文裁剪: level=${built.level} chars=${built.chars}`);
|
|
613
601
|
}
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
}
|
|
602
|
+
const llmClient = new LLMClient_js_1.LLMClient(models, built.messages, this.abortController.signal, tools);
|
|
603
|
+
llmClient.setPreferredModel(this.selectModelId);
|
|
604
|
+
// 系统模式:转发到 Server LLM 代理
|
|
605
|
+
if (this.useSystemMode) {
|
|
606
|
+
const { getRequestToken, getServerBaseUrl } = await Promise.resolve().then(() => __importStar(require('../../api/index.js')));
|
|
607
|
+
llmClient.setSystemMode(true, this.selectModelId, getServerBaseUrl(), getRequestToken());
|
|
608
|
+
}
|
|
609
|
+
try {
|
|
610
|
+
return await llmClient.Chat();
|
|
611
|
+
}
|
|
612
|
+
catch (error) {
|
|
613
|
+
lastError = error;
|
|
614
|
+
if (!isContextOverflow(error))
|
|
615
|
+
throw error;
|
|
616
|
+
const next = (0, ContextBuilder_js_1.nextTrimLevel)(trimLevel);
|
|
617
|
+
if (next === null)
|
|
618
|
+
throw error;
|
|
619
|
+
logger.warn(`上下文超限,加重裁剪重试: ${trimLevel} -> ${next}`);
|
|
620
|
+
trimLevel = next;
|
|
634
621
|
}
|
|
635
622
|
}
|
|
636
|
-
|
|
623
|
+
};
|
|
624
|
+
// 递归处理函数(支持多轮工具调用)
|
|
625
|
+
const processModelResponse = async () => {
|
|
637
626
|
if (!this.abortController || !this.abortController.signal || this.abortController.signal.aborted) {
|
|
638
627
|
throw Error('aborted');
|
|
639
628
|
}
|
|
640
|
-
const
|
|
641
|
-
llmClient.setPreferredModel(this.selectModelId);
|
|
642
|
-
// 系统模式:转发到 Server LLM 代理
|
|
643
|
-
if (this.useSystemMode) {
|
|
644
|
-
const { getRequestToken, getServerBaseUrl } = await Promise.resolve().then(() => __importStar(require('../../api/index.js')));
|
|
645
|
-
llmClient.setSystemMode(true, this.selectModelId, getServerBaseUrl(), getRequestToken());
|
|
646
|
-
}
|
|
647
|
-
// 使用 streamChat 调用模型
|
|
648
|
-
const llmResult = await llmClient.Chat();
|
|
629
|
+
const llmResult = await callModel();
|
|
649
630
|
// 验证 LLM 结果
|
|
650
631
|
if (!llmResult || !llmResult.modelName) {
|
|
651
632
|
throw new Error('Invalid LLM result: missing modelName');
|
|
@@ -654,6 +635,11 @@ class Session {
|
|
|
654
635
|
modelNames.push(modelName);
|
|
655
636
|
// ========== 处理工具调用 ==========
|
|
656
637
|
if (llmResult.toolCalls && llmResult.toolCalls.length > 0) {
|
|
638
|
+
toolRound++;
|
|
639
|
+
if (toolRound > index_js_2.appConfig.maxToolRounds) {
|
|
640
|
+
logger.warn(`工具调用轮次超过上限 ${index_js_2.appConfig.maxToolRounds},终止本轮`);
|
|
641
|
+
throw new Error(`工具调用轮次超过上限(${index_js_2.appConfig.maxToolRounds}),已终止以防无限循环`);
|
|
642
|
+
}
|
|
657
643
|
messages.push({
|
|
658
644
|
role: 'assistant',
|
|
659
645
|
content: llmResult.content,
|
|
@@ -929,7 +915,7 @@ class Session {
|
|
|
929
915
|
};
|
|
930
916
|
messages.push(toolResultMessage);
|
|
931
917
|
}
|
|
932
|
-
//
|
|
918
|
+
// 继续下一轮(带上工具结果继续调用模型)
|
|
933
919
|
return await processModelResponse();
|
|
934
920
|
}
|
|
935
921
|
else {
|
|
@@ -5,7 +5,7 @@ const uuid_1 = require("uuid");
|
|
|
5
5
|
const shared_1 = require("@myassis/shared");
|
|
6
6
|
const logger = (0, shared_1.getLogger)('SessionManager');
|
|
7
7
|
const SessionStore_js_1 = require("./SessionStore.js");
|
|
8
|
-
const authStore_js_1 = require("
|
|
8
|
+
const authStore_js_1 = require("../../stores/authStore.js");
|
|
9
9
|
const Session_js_1 = require("./Session.js");
|
|
10
10
|
/**
|
|
11
11
|
* SessionManager - Business logic layer
|
|
@@ -4,7 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.getSystemPromptAsync = exports.LANGUAGES = exports.CHAT_STYLE_TEMPLATES = void 0;
|
|
7
|
-
const index_js_1 = require("
|
|
7
|
+
const index_js_1 = require("../stores/index.js");
|
|
8
8
|
const os_1 = __importDefault(require("os"));
|
|
9
9
|
const dataService_js_1 = require("./dataService.js");
|
|
10
10
|
const shared_1 = require("@myassis/shared");
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.modelTool = void 0;
|
|
4
4
|
const dataService_js_1 = require("../dataService.js");
|
|
5
|
-
const index_js_1 = require("
|
|
5
|
+
const index_js_1 = require("../../stores/index.js");
|
|
6
6
|
exports.modelTool = {
|
|
7
7
|
name: 'model',
|
|
8
8
|
description: '模型管理工具',
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.skillTool = void 0;
|
|
4
4
|
const dataService_js_1 = require("../dataService.js");
|
|
5
|
-
const index_js_1 = require("
|
|
5
|
+
const index_js_1 = require("../../stores/index.js");
|
|
6
6
|
const shared_1 = require("@myassis/shared");
|
|
7
7
|
const logger = (0, shared_1.getLogger)('skill');
|
|
8
8
|
exports.skillTool = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myassis/gateway",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.77",
|
|
4
4
|
"description": "我的助手 Gateway Service - 本地 AI 网关服务,支持认证、WebSocket 实时通信和任务调度",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"@nut-tree/nut-js": "^4.2.0",
|
|
45
45
|
"axios": "^1.15.2",
|
|
46
46
|
"bcryptjs": "^2.4.3",
|
|
47
|
-
"better-sqlite3": "
|
|
47
|
+
"better-sqlite3": "12.9.0",
|
|
48
48
|
"cheerio": "^1.2.0",
|
|
49
49
|
"compression": "^1.7.4",
|
|
50
50
|
"cors": "^2.8.5",
|