@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.
@@ -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;
@@ -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("@/stores/authStore.js");
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
  /**
@@ -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("@/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");
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("@/services/session/SessionStore.js");
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) => {
@@ -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("@/config/index.js");
10
- const auth_js_1 = require("@/middleware/auth.js");
11
- const index_js_2 = require("@/api/index.js");
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("@/stores/authStore.js");
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("@/stores/index.js");
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("@/stores/authStore.js");
10
+ const authStore_js_1 = require("../../stores/authStore.js");
11
11
  const shared_1 = require("@myassis/shared");
12
- const index_js_1 = __importDefault(require("@/config/index.js"));
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("@/stores/index.js");
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, 100) + '...(内容过长已截断)',
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.output != null) {
228
- item.tool_calls.push({
229
- id: toolCallItem.id,
230
- function: {
231
- name: toolCallItem.toolName,
232
- arguments: toolCallItem.input
233
- },
234
- type: 'function'
235
- });
236
- tools.push({
237
- role: 'tool',
238
- tool_call_id: toolCallItem.id,
239
- content: toolCallItem.output.substring(0, 100) + '...' + '(内容过长已截断)'
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 tokens = JSON.stringify(body).length;
362
- logger.debug('tokens', tokens);
363
- if (tokens > 200000 && !existAttachments && !existScreenShot) {
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("@/stores/index.js");
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("@/config/index.js");
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
- // 1. 获取所有消息
73
- let messages = this.session.getMessages();
97
+ const keep = index_js_2.appConfig.historyKeep;
98
+ // 复制数组:getMessages() 返回的是会话内部的活引用,不可原地修改
99
+ let messages = [...this.session.getMessages()];
74
100
  if (this.childAgent) {
75
- messages.splice(-1);
101
+ // 子 agent 的最后一条消息由调用方单独拼接,此处排除
102
+ messages = messages.slice(0, -1);
103
+ }
104
+ if (!this.shouldSummarize(messages)) {
105
+ return messages;
76
106
  }
77
- // 2. 消息少于摘要配置的阈值数,直接返回
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
- // 4A. 首次摘要生成
85
- const summary = await this.generateSummaryAsync(messages.slice(0, -index_js_2.appConfig.messageKeep), null);
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 = Date.now();
121
+ this.session.lastMessageSummaryAt = boundary;
88
122
  this.session.save();
89
123
  return [
90
- this.toSummaryMessage(summary, this.session.lastMessageSummaryAt, ''),
91
- ...messages.slice(-index_js_2.appConfig.messageKeep),
124
+ this.toSummaryMessage(summary, boundary, ''),
125
+ ...messages.slice(-keep),
92
126
  ];
93
127
  }
94
- else {
95
- // 4B. 增量摘要更新
96
- const newMessages = messages.filter((m) => m.createdAt > this.session.lastMessageSummaryAt);
97
- if (newMessages.length < this.config.summaryThreshold) {
98
- // 新消息少于阈值条:返回摘要 + 从摘要时刻起的所有消息
99
- const fromSummary = messages.filter((m) => m.createdAt >= this.session.lastMessageSummaryAt);
100
- return [
101
- this.toSummaryMessage(this.session.lastMessageSummary, this.session.lastMessageSummaryAt, ''),
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 token = index_js_1.authStore.get(this.session.userId).accessToken;
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
- const response = await llmClient.Chat();
241
- const content = (response.content || '').trim();
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
- lastSummary,
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 subSummaries = [];
308
+ const CONCURRENCY = 3; // 子摘要并发上限
309
+ const batches = [];
257
310
  for (let i = 0; i < formattedMessages.length; i += BATCH_SIZE) {
258
- if (this.signal.aborted)
259
- break;
260
- const batch = formattedMessages.slice(i, i + BATCH_SIZE);
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 token = index_js_1.authStore.get(this.session.userId).accessToken;
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
- const subContent = (response.content || '').trim();
270
- if (subContent) {
271
- subSummaries.push(subContent);
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 (subSummaries.length === 0) {
276
- return lastSummary || '(摘要生成失败)';
347
+ const valid = subSummaries.filter((x) => !!x);
348
+ if (valid.length === 0) {
349
+ return lastSummary || null;
277
350
  }
278
- if (subSummaries.length === 1) {
279
- return subSummaries[0];
351
+ if (valid.length === 1) {
352
+ return valid[0];
280
353
  }
281
354
  // 合并子摘要为最终摘要
282
- const mergePrompt = this.buildMergePrompt(subSummaries, lastSummary);
283
- const token = index_js_1.authStore.get(this.session.userId).accessToken;
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
- const mergeResponse = await mergeLlmClient.Chat();
290
- const mergedContent = (mergeResponse.content || '').trim();
291
- return mergedContent || subSummaries.join('\n\n');
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("@/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");
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("@/services/dataService.js");
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("@/config/index.js");
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
- const processModelResponse = async () => {
592
- if (!this.abortController || !this.abortController.signal || this.abortController.signal.aborted) {
593
- throw Error('aborted');
594
- }
595
- // 工具调用轮次过多时,成对删除最早的 assistant(tool_calls) + tool(result)
596
- // 只保留最近 keep 轮工具调用,防止上下文过长导致模型混乱
597
- // 清理旧工具调用:只保留最近 keep 轮的完整上下文
598
- {
599
- const MAX_TOOL_PAIRS = keep;
600
- const allToolCallItems = toolCalls.flatMap(x => x.toolCalls);
601
- // 1) 清理 this.toolCalls 中超出 keep 的 output(供前端展示用)
602
- allToolCallItems.forEach((item, index) => {
603
- if (index < allToolCallItems.length - MAX_TOOL_PAIRS) {
604
- item.output = null;
605
- }
606
- });
607
- // 2) 从 messages 中成对删除最早的 assistant(tool_calls) + tool(result)
608
- let toolPairCount = 0;
609
- for (let i = messagesLength; i < messages.length; i++) {
610
- if (messages[i].role === 'assistant' && messages[i].tool_calls) {
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
- if (toolPairCount > MAX_TOOL_PAIRS) {
615
- let toRemove = toolPairCount - MAX_TOOL_PAIRS;
616
- // 找到需要截断的工具调用对
617
- for (let i = messagesLength; i < messages.length && toRemove > 0; i++) {
618
- if (messages[i].role === 'assistant' && messages[i].tool_calls) {
619
- // 截断 assistant 消息内容
620
- truncateMessageContent(messages[i]);
621
- while (true) {
622
- // 检查并截断对应的 tool 消息
623
- if (i + 1 < messages.length && messages[i + 1].role === 'tool') {
624
- truncateMessageContent(messages[i + 1]);
625
- }
626
- else {
627
- break;
628
- }
629
- i++;
630
- }
631
- toRemove--;
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
- // 使用 LLMClient 进行流式调用
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 llmClient = new LLMClient_js_1.LLMClient(models, messages, this.abortController.signal, tools);
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("@/stores/authStore.js");
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("@/stores/index.js");
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("@/stores/index.js");
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("@/stores/index.js");
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.75",
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": "^11.10.0",
47
+ "better-sqlite3": "12.9.0",
48
48
  "cheerio": "^1.2.0",
49
49
  "compression": "^1.7.4",
50
50
  "cors": "^2.8.5",