@myassis/gateway 1.0.78 → 1.0.81

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.
@@ -90,7 +90,7 @@ exports.appConfig = {
90
90
  /** 超出 toolPairKeep 的旧工具调用内容截断长度 */
91
91
  oldToolContentLimit: parseInt(process.env.OLD_TOOL_CONTENT_LIMIT || '500', 10),
92
92
  /** 单次请求内最大工具调用轮数 */
93
- maxToolRounds: parseInt(process.env.MAX_TOOL_ROUNDS || '100', 10),
93
+ maxToolRounds: parseInt(process.env.MAX_TOOL_ROUNDS || '200', 10),
94
94
  /** 触发上下文压缩的字符阈值 */
95
95
  summaryTriggerChars: parseInt(process.env.SUMMARY_TRIGGER_CHARS || '60000', 10),
96
96
  appName: '我的助手'
@@ -254,7 +254,8 @@ class AgentManager {
254
254
  createdAt: s.createdAt,
255
255
  updatedAt: s.updatedAt,
256
256
  messageQueue: s.messageQueue,
257
- messageQueueAutoExecute: s.messageQueueAutoExecute
257
+ messageQueueAutoExecute: s.messageQueueAutoExecute,
258
+ plan: s.plan
258
259
  }));
259
260
  }
260
261
  /**
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.nextTrimLevel = exports.buildContext = exports.estimateTokens = exports.estimateChars = exports.TrimLevel = void 0;
4
4
  const shared_1 = require("@myassis/shared");
5
5
  const index_js_1 = require("../../config/index.js");
6
+ const ToolLedger_js_1 = require("./ToolLedger.js");
6
7
  const logger = (0, shared_1.getLogger)('ContextBuilder');
7
8
  /**
8
9
  * 上下文构造与分级裁剪
@@ -14,17 +15,35 @@ const logger = (0, shared_1.getLogger)('ContextBuilder');
14
15
  */
15
16
  /** 中英混合场景下的粗略 token 估算系数 */
16
17
  const CHARS_PER_TOKEN = 1.6;
17
- /** 裁剪级别,逐级加重 */
18
+ /** 骨架化后单条工具结果保留的长度 */
19
+ const SKELETON_OUTPUT_LIMIT = 120;
20
+ /**
21
+ * 裁剪级别,逐级加重。
22
+ *
23
+ * 设计原则:**任何级别都不会删除工具调用记录**。
24
+ * 一旦删除,模型就看不到自己已经做过什么,会重复调用同一个工具
25
+ * (重复读文件、重复写入、重复执行命令),比截断内容的代价大得多。
26
+ * 因此加重裁剪只压缩「负载」(输出正文、参数值、附件),
27
+ * 始终保留「调用事实」(工具名 + 关键参数 + 成功/失败)。
28
+ */
18
29
  var TrimLevel;
19
30
  (function (TrimLevel) {
20
31
  /** 不裁剪 */
21
32
  TrimLevel[TrimLevel["None"] = 0] = "None";
22
- /** 截断超出 toolPairKeep 的旧工具输出与参数 */
33
+ /** 截断超出 toolPairKeep 轮的旧工具输出 */
23
34
  TrimLevel[TrimLevel["OldToolPayload"] = 1] = "OldToolPayload";
24
- /** 进一步压缩保留轮数,并降级旧附件为占位符 */
35
+ /** 收紧保留轮数、截断参数值、降级旧附件为占位符 */
25
36
  TrimLevel[TrimLevel["Aggressive"] = 2] = "Aggressive";
26
- /** 丢弃最早的完整工具调用对,只留最近 minToolPairKeep 轮 */
27
- TrimLevel[TrimLevel["DropOldPairs"] = 3] = "DropOldPairs";
37
+ /** 旧工具调用退化为骨架:仅保留工具名、关键参数与结果摘要 */
38
+ TrimLevel[TrimLevel["SkeletonOldCalls"] = 3] = "SkeletonOldCalls";
39
+ /**
40
+ * 最后手段:把旧工具调用折叠进一份「已完成操作」账本。
41
+ *
42
+ * 消息条目本身被移除,但调用事实以清单形式并入首条 system 消息,
43
+ * 模型依然知道哪些操作已经做过 —— 这是「不丢事实」与「控制体积」的折中,
44
+ * 仅在骨架化后仍超预算(数百轮调用)时启用。
45
+ */
46
+ TrimLevel[TrimLevel["FoldToLedger"] = 4] = "FoldToLedger";
28
47
  })(TrimLevel || (exports.TrimLevel = TrimLevel = {}));
29
48
  /** 估算单条消息的字符数(含附件与工具参数) */
30
49
  function messageChars(message) {
@@ -164,6 +183,138 @@ function clipPair(messages, pair, limit) {
164
183
  messages[i].content = clip(messages[i].content, limit);
165
184
  }
166
185
  }
186
+ /** 从工具参数中提取用于标识「这次调用做了什么」的关键字段 */
187
+ const IDENTITY_KEYS = [
188
+ 'path', 'file', 'filePath', 'filename', 'dir', 'directory',
189
+ 'command', 'cmd', 'query', 'pattern', 'url', 'name', 'key', 'id',
190
+ 'action', 'method', 'sessionId', 'target',
191
+ ];
192
+ /** 单个关键参数值的最大长度 */
193
+ const IDENTITY_VALUE_LIMIT = 200;
194
+ /**
195
+ * 将工具参数压成「身份摘要」:只留能标识调用对象的关键字段。
196
+ *
197
+ * 例如 write_file 的 4000 字 content 被丢掉,但 path 保留,
198
+ * 模型据此知道「src/a.ts 已经写过了」,不会重复写入。
199
+ */
200
+ function skeletonArguments(args) {
201
+ let parsed;
202
+ if (typeof args === 'string') {
203
+ try {
204
+ parsed = JSON.parse(args);
205
+ }
206
+ catch {
207
+ return JSON.stringify({ _omitted: true });
208
+ }
209
+ }
210
+ else if (args && typeof args === 'object') {
211
+ parsed = args;
212
+ }
213
+ else {
214
+ return '{}';
215
+ }
216
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
217
+ return JSON.stringify({ _omitted: true });
218
+ }
219
+ const out = {};
220
+ for (const key of Object.keys(parsed)) {
221
+ if (!IDENTITY_KEYS.includes(key))
222
+ continue;
223
+ const value = parsed[key];
224
+ if (value == null)
225
+ continue;
226
+ if (typeof value === 'object')
227
+ continue;
228
+ const text = String(value);
229
+ out[key] = text.length > IDENTITY_VALUE_LIMIT
230
+ ? text.slice(0, IDENTITY_VALUE_LIMIT) + '...'
231
+ : text;
232
+ }
233
+ // 标记参数已省略,避免模型误以为这是完整参数而照抄
234
+ out._omitted = true;
235
+ return JSON.stringify(out);
236
+ }
237
+ /** 从工具结果中提炼一行摘要,保留成功/失败信号 */
238
+ function skeletonOutput(content) {
239
+ const text = typeof content === 'string' ? content : JSON.stringify(content ?? '');
240
+ if (!text)
241
+ return '(已执行,结果略)';
242
+ let success = null;
243
+ let body = text;
244
+ try {
245
+ const parsed = JSON.parse(text);
246
+ if (parsed && typeof parsed === 'object') {
247
+ if (typeof parsed.success === 'boolean')
248
+ success = parsed.success;
249
+ if (parsed.output != null) {
250
+ body = typeof parsed.output === 'string' ? parsed.output : JSON.stringify(parsed.output);
251
+ }
252
+ }
253
+ }
254
+ catch {
255
+ // 非 JSON 结果,直接用原文
256
+ }
257
+ const head = body.replace(/\s+/g, ' ').slice(0, SKELETON_OUTPUT_LIMIT);
258
+ const flag = success === null ? '' : success ? '[成功] ' : '[失败] ';
259
+ return `${flag}${head}${body.length > SKELETON_OUTPUT_LIMIT ? '…' : ''}(完整结果已省略,勿重复调用)`;
260
+ }
261
+ /**
262
+ * 将一个工具调用对退化为骨架。
263
+ *
264
+ * 保留 assistant.tool_calls 与配对的 tool 消息(协议要求且模型需要看到调用事实),
265
+ * 只把参数压成身份摘要、把输出压成一行结论。
266
+ */
267
+ function skeletonizePair(messages, pair) {
268
+ const head = messages[pair.start];
269
+ head.content = clip(head.content, SKELETON_OUTPUT_LIMIT);
270
+ // 旧轮次的思维链对当前决策价值最低,直接丢弃
271
+ delete head.reasoning_content;
272
+ if (Array.isArray(head.tool_calls)) {
273
+ for (const tc of head.tool_calls) {
274
+ tc.input = skeletonArguments(tc.input);
275
+ if (tc.function) {
276
+ tc.function.arguments = skeletonArguments(tc.function.arguments);
277
+ }
278
+ }
279
+ }
280
+ for (let i = pair.start + 1; i < pair.end; i++) {
281
+ messages[i].content = skeletonOutput(messages[i].content);
282
+ }
283
+ }
284
+ /**
285
+ * 把最早的工具调用对折叠进账本。
286
+ *
287
+ * 与已废弃的「直接丢弃」不同:调用事实不会消失,而是转成
288
+ * 首条 system 消息中的「已完成操作」清单。
289
+ */
290
+ function foldPairsToLedger(messages, pairs, foldCount, protectFrom) {
291
+ if (foldCount <= 0)
292
+ return messages;
293
+ const foldIndexes = new Set();
294
+ const folded = [];
295
+ for (let i = 0; i < foldCount; i++) {
296
+ for (let j = pairs[i].start; j < pairs[i].end; j++) {
297
+ foldIndexes.add(j);
298
+ folded.push(messages[j]);
299
+ }
300
+ }
301
+ const ledger = (0, ToolLedger_js_1.renderRequestLedger)(folded);
302
+ const kept = messages.filter((_, index) => !foldIndexes.has(index));
303
+ if (!ledger)
304
+ return kept;
305
+ // 并入首条 system 消息:部分厂商只允许 system 位于首位
306
+ if (kept[0]?.role === 'system' && typeof kept[0].content === 'string') {
307
+ kept[0] = { ...kept[0], content: `${kept[0].content}\n\n${ledger}` };
308
+ }
309
+ else {
310
+ kept.splice(Math.min(protectFrom, kept.length), 0, {
311
+ role: 'assistant',
312
+ content: ledger,
313
+ attachments: [],
314
+ });
315
+ }
316
+ return kept;
317
+ }
167
318
  /** 将较早的图片 / 音频附件降级为文本占位符,避免 base64 永久占用上下文 */
168
319
  function degradeAttachments(messages, keepLastN) {
169
320
  const withAttachments = [];
@@ -202,39 +353,31 @@ function buildContext(messages, protectFrom, maxChars = index_js_1.appConfig.con
202
353
  const keep = target >= TrimLevel.Aggressive
203
354
  ? index_js_1.appConfig.minToolPairKeep
204
355
  : index_js_1.appConfig.toolPairKeep;
205
- // 1) 截断超出 keep 轮的旧工具调用负载
356
+ // 1) 最重级别:把最早的工具调用退化为骨架(保留调用事实,丢弃负载)
357
+ if (target >= TrimLevel.SkeletonOldCalls) {
358
+ const skeletonUntil = Math.max(0, pairs.length - index_js_1.appConfig.minToolPairKeep);
359
+ for (let i = 0; i < skeletonUntil; i++) {
360
+ skeletonizePair(working, pairs[i]);
361
+ }
362
+ }
363
+ // 2) 截断超出 keep 轮的旧工具负载(骨架化过的 pair 已经很小,重复处理无害)
206
364
  const clipUntil = Math.max(0, pairs.length - keep);
365
+ const argLimit = target >= TrimLevel.Aggressive
366
+ ? Math.max(200, Math.floor(index_js_1.appConfig.oldToolContentLimit / 2))
367
+ : index_js_1.appConfig.oldToolContentLimit;
207
368
  for (let i = 0; i < clipUntil; i++) {
208
- clipPair(working, pairs[i], index_js_1.appConfig.oldToolContentLimit);
369
+ clipPair(working, pairs[i], argLimit);
209
370
  }
210
- // 2) 降级旧附件
371
+ // 3) 降级旧附件
211
372
  if (target >= TrimLevel.Aggressive) {
212
373
  degradeAttachments(working, 1);
213
374
  }
214
- // 3) 整对丢弃最早的工具调用(保持 assistant/tool 配对完整,避免协议错误)
215
- if (target >= TrimLevel.DropOldPairs && pairs.length > index_js_1.appConfig.minToolPairKeep) {
216
- const dropCount = pairs.length - index_js_1.appConfig.minToolPairKeep;
217
- const dropIndexes = new Set();
218
- for (let i = 0; i < dropCount; i++) {
219
- for (let j = pairs[i].start; j < pairs[i].end; j++) {
220
- dropIndexes.add(j);
221
- }
222
- }
223
- const dropped = working.filter((_, index) => !dropIndexes.has(index));
224
- // 用提示说明被丢弃的轮次,避免模型认为自己没做过这些事。
225
- // 注意:不能在对话中间插入 system 消息,部分厂商只允许 system 位于首位,
226
- // 因此把提示并入首条 system 消息(没有则退化为 assistant 提示)。
227
- const notice = `(已省略 ${dropCount} 轮较早的工具调用记录以控制上下文长度)`;
228
- if (dropped[0]?.role === 'system' && typeof dropped[0].content === 'string') {
229
- dropped[0] = { ...dropped[0], content: `${dropped[0].content}\n${notice}` };
230
- }
231
- else {
232
- dropped.splice(protectFrom, 0, { role: 'assistant', content: notice, attachments: [] });
233
- }
234
- working = dropped;
375
+ // 4) 最后手段:折叠为账本(调用事实保留在 system 清单中)
376
+ if (target >= TrimLevel.FoldToLedger && pairs.length > index_js_1.appConfig.minToolPairKeep) {
377
+ working = foldPairsToLedger(working, pairs, pairs.length - index_js_1.appConfig.minToolPairKeep, protectFrom);
235
378
  }
236
379
  };
237
- for (const target of [minLevel, TrimLevel.Aggressive, TrimLevel.DropOldPairs]) {
380
+ for (const target of [minLevel, TrimLevel.Aggressive, TrimLevel.SkeletonOldCalls, TrimLevel.FoldToLedger]) {
238
381
  if (target < level)
239
382
  continue;
240
383
  applyLevel(target);
@@ -252,8 +395,10 @@ exports.buildContext = buildContext;
252
395
  function nextTrimLevel(level) {
253
396
  if (level < TrimLevel.Aggressive)
254
397
  return TrimLevel.Aggressive;
255
- if (level < TrimLevel.DropOldPairs)
256
- return TrimLevel.DropOldPairs;
398
+ if (level < TrimLevel.SkeletonOldCalls)
399
+ return TrimLevel.SkeletonOldCalls;
400
+ if (level < TrimLevel.FoldToLedger)
401
+ return TrimLevel.FoldToLedger;
257
402
  return null;
258
403
  }
259
404
  exports.nextTrimLevel = nextTrimLevel;
@@ -32,6 +32,7 @@ const index_js_1 = require("../../stores/index.js");
32
32
  const LLMClient_js_1 = require("../llm/LLMClient.js");
33
33
  const index_js_2 = require("../../config/index.js");
34
34
  const ContextBuilder_js_1 = require("./ContextBuilder.js");
35
+ const ToolLedger_js_1 = require("./ToolLedger.js");
35
36
  const DEFAULT_CONFIG = {
36
37
  summaryThreshold: 10,
37
38
  summaryTriggerChars: index_js_2.appConfig.summaryTriggerChars,
@@ -61,10 +62,19 @@ class MemoryManager {
61
62
  this.childAgent = childAgent;
62
63
  this.res = res;
63
64
  }
64
- toSummaryMessage(summary, createdAt, modelName) {
65
+ /**
66
+ * 构造注入上下文的摘要消息。
67
+ *
68
+ * @param omitted 被摘要(即不再出现在消息数组中)的原始消息。
69
+ * 这些消息的工具调用会被提取为「已完成操作」账本附在摘要之后,
70
+ * 防止模型因为看不到调用记录而重复执行同一个工具。
71
+ */
72
+ toSummaryMessage(summary, createdAt, modelName, omitted = []) {
73
+ const ledger = (0, ToolLedger_js_1.renderLedger)(omitted);
74
+ const content = `对话内容摘要:\n${summary}` + (ledger ? `\n\n${ledger}` : '');
65
75
  return {
66
76
  sessionId: this.session.id,
67
- content: `对话内容摘要:\n${summary}`,
77
+ content,
68
78
  role: 'system',
69
79
  id: `summary_${createdAt}`,
70
80
  createdAt,
@@ -121,16 +131,18 @@ class MemoryManager {
121
131
  this.session.lastMessageSummaryAt = boundary;
122
132
  this.session.save();
123
133
  return [
124
- this.toSummaryMessage(summary, boundary, ''),
134
+ this.toSummaryMessage(summary, boundary, '', toSummarize),
125
135
  ...messages.slice(-keep),
126
136
  ];
127
137
  }
128
138
  // 增量摘要:边界之后的消息才是“新消息”
129
139
  const boundaryAt = this.session.lastMessageSummaryAt;
130
140
  const newMessages = messages.filter((m) => m.createdAt > boundaryAt);
141
+ // 边界之前的消息已被摘要取代,其工具调用需通过账本保留
142
+ const omitted = messages.filter((m) => m.createdAt <= boundaryAt);
131
143
  if (!this.shouldSummarize(newMessages) || newMessages.length <= keep) {
132
144
  return [
133
- this.toSummaryMessage(this.session.lastMessageSummary, boundaryAt, ''),
145
+ this.toSummaryMessage(this.session.lastMessageSummary, boundaryAt, '', omitted),
134
146
  ...newMessages,
135
147
  ];
136
148
  }
@@ -139,7 +151,7 @@ class MemoryManager {
139
151
  const summary = await this.generateSummaryAsync(toSummarize, this.session.lastMessageSummary);
140
152
  if (!summary) {
141
153
  return [
142
- this.toSummaryMessage(this.session.lastMessageSummary, boundaryAt, ''),
154
+ this.toSummaryMessage(this.session.lastMessageSummary, boundaryAt, '', omitted),
143
155
  ...newMessages,
144
156
  ];
145
157
  }
@@ -148,7 +160,7 @@ class MemoryManager {
148
160
  this.session.lastMessageSummaryAt = nextBoundary;
149
161
  this.session.save();
150
162
  return [
151
- this.toSummaryMessage(summary, nextBoundary, ''),
163
+ this.toSummaryMessage(summary, nextBoundary, '', [...omitted, ...toSummarize]),
152
164
  ...newMessages.slice(-keep),
153
165
  ];
154
166
  }
@@ -0,0 +1,181 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderRequestLedger = exports.renderLedger = exports.renderOperations = exports.extractOperations = exports.extractOperationsFromRequest = void 0;
4
+ /**
5
+ * 工具调用账本
6
+ *
7
+ * 当很早的工具调用连「骨架」都无法保留在消息数组中时(例如数百轮调用),
8
+ * 本模块把这些调用压成一份确定性的「已完成操作清单」,随摘要一起注入 system 消息。
9
+ *
10
+ * 为什么不交给 LLM 生成:
11
+ * 摘要由模型改写,操作清单一旦被模型漏写或改写,模型就会重复调用同一个工具。
12
+ * 因此账本从消息历史中**确定性提取**,每次实时重算,不依赖模型输出、也无需落库。
13
+ */
14
+ /** 从参数中提取调用目标的关键字段,顺序即优先级 */
15
+ const TARGET_KEYS = [
16
+ 'path', 'filePath', 'file', 'filename', 'dir', 'directory',
17
+ 'command', 'cmd', 'query', 'pattern', 'url', 'name', 'target', 'key',
18
+ ];
19
+ /** 账本最多渲染的条目数,超出只保留计数 */
20
+ const MAX_LEDGER_ENTRIES = 60;
21
+ /** 单个目标字符串的最大长度 */
22
+ const MAX_TARGET_LEN = 120;
23
+ /** 从工具参数中提取一个可读的调用目标 */
24
+ function extractTarget(input) {
25
+ if (input == null)
26
+ return '';
27
+ let parsed = input;
28
+ if (typeof input === 'string') {
29
+ try {
30
+ parsed = JSON.parse(input);
31
+ }
32
+ catch {
33
+ return input.length > MAX_TARGET_LEN ? input.slice(0, MAX_TARGET_LEN) + '…' : input;
34
+ }
35
+ }
36
+ if (!parsed || typeof parsed !== 'object')
37
+ return '';
38
+ for (const key of TARGET_KEYS) {
39
+ const value = parsed[key];
40
+ if (value == null || typeof value === 'object')
41
+ continue;
42
+ const text = String(value).trim();
43
+ if (!text)
44
+ continue;
45
+ return text.length > MAX_TARGET_LEN ? text.slice(0, MAX_TARGET_LEN) + '…' : text;
46
+ }
47
+ return '';
48
+ }
49
+ /**
50
+ * 从「请求态」消息数组中提取操作清单。
51
+ *
52
+ * 与 extractOperations 的区别在于数据形状:请求态消息用的是
53
+ * OpenAI 协议格式(assistant.tool_calls + 配对的 tool 消息),
54
+ * 而非会话持久化的 toolCalls 嵌套结构。
55
+ */
56
+ function extractOperationsFromRequest(messages) {
57
+ const map = new Map();
58
+ // 先建立 tool_call_id -> 是否成功 的索引
59
+ const outcome = new Map();
60
+ for (const message of messages || []) {
61
+ if (message?.role !== 'tool' || !message.tool_call_id)
62
+ continue;
63
+ const text = typeof message.content === 'string' ? message.content : '';
64
+ let success = null;
65
+ try {
66
+ const parsed = JSON.parse(text);
67
+ if (parsed && typeof parsed.success === 'boolean')
68
+ success = parsed.success;
69
+ }
70
+ catch {
71
+ if (text.includes('[失败]'))
72
+ success = false;
73
+ else if (text.includes('[成功]'))
74
+ success = true;
75
+ }
76
+ outcome.set(message.tool_call_id, success);
77
+ }
78
+ for (const message of messages || []) {
79
+ if (message?.role !== 'assistant' || !Array.isArray(message.tool_calls))
80
+ continue;
81
+ for (const tc of message.tool_calls) {
82
+ const name = tc?.toolName || tc?.function?.name;
83
+ if (!name)
84
+ continue;
85
+ const target = extractTarget(tc.input ?? tc.function?.arguments);
86
+ const key = `${name}\u0000${target}`;
87
+ const success = tc.id ? (outcome.get(tc.id) ?? null) : null;
88
+ const existing = map.get(key);
89
+ if (existing) {
90
+ existing.count++;
91
+ if (success !== null)
92
+ existing.lastSuccess = success;
93
+ }
94
+ else {
95
+ map.set(key, { tool: name, target, count: 1, lastSuccess: success });
96
+ }
97
+ }
98
+ }
99
+ return [...map.values()];
100
+ }
101
+ exports.extractOperationsFromRequest = extractOperationsFromRequest;
102
+ /** 判断一次工具调用是否成功 */
103
+ function isSuccess(item) {
104
+ if (item?.status === 'error')
105
+ return false;
106
+ if (item?.status === 'success')
107
+ return true;
108
+ return null;
109
+ }
110
+ /**
111
+ * 从消息中提取去重后的操作清单。
112
+ *
113
+ * 同一 工具+目标 的多次调用合并为一条并累加次数,
114
+ * 这样「读了 30 次同一个文件」不会占据 30 行。
115
+ */
116
+ function extractOperations(messages) {
117
+ const map = new Map();
118
+ for (const message of messages || []) {
119
+ for (const group of (message.toolCalls || [])) {
120
+ for (const item of (group?.toolCalls || [])) {
121
+ if (!item?.toolName)
122
+ continue;
123
+ const tool = item.actionName ? `${item.toolName}.${item.actionName}` : item.toolName;
124
+ const target = extractTarget(item.input);
125
+ const key = `${tool}\u0000${target}`;
126
+ const existing = map.get(key);
127
+ if (existing) {
128
+ existing.count++;
129
+ const success = isSuccess(item);
130
+ if (success !== null)
131
+ existing.lastSuccess = success;
132
+ }
133
+ else {
134
+ map.set(key, { tool, target, count: 1, lastSuccess: isSuccess(item) });
135
+ }
136
+ }
137
+ }
138
+ }
139
+ return [...map.values()];
140
+ }
141
+ exports.extractOperations = extractOperations;
142
+ /**
143
+ * 将操作清单渲染为文本。
144
+ *
145
+ * 返回空串表示没有需要记录的操作。
146
+ */
147
+ function renderOperations(operations) {
148
+ if (operations.length === 0)
149
+ return '';
150
+ // 失败的操作最有价值(避免模型以为没做过而重试),优先展示;其次按调用次数
151
+ const sorted = operations.sort((a, b) => {
152
+ const aFailed = a.lastSuccess === false ? 1 : 0;
153
+ const bFailed = b.lastSuccess === false ? 1 : 0;
154
+ if (aFailed !== bFailed)
155
+ return bFailed - aFailed;
156
+ return b.count - a.count;
157
+ });
158
+ const shown = sorted.slice(0, MAX_LEDGER_ENTRIES);
159
+ const lines = shown.map((op) => {
160
+ const times = op.count > 1 ? ` ×${op.count}` : '';
161
+ const flag = op.lastSuccess === false ? ' [失败]' : '';
162
+ const target = op.target ? ` ${op.target}` : '';
163
+ return `- ${op.tool}${target}${times}${flag}`;
164
+ });
165
+ const rest = sorted.length - shown.length;
166
+ if (rest > 0) {
167
+ lines.push(`- …另有 ${rest} 项操作未列出`);
168
+ }
169
+ return `【已完成操作】以下工具调用**已经执行过**,除非需要确认最新状态,不要重复调用:\n${lines.join('\n')}`;
170
+ }
171
+ exports.renderOperations = renderOperations;
172
+ /** 从会话消息渲染账本 */
173
+ function renderLedger(messages) {
174
+ return renderOperations(extractOperations(messages));
175
+ }
176
+ exports.renderLedger = renderLedger;
177
+ /** 从请求态消息渲染账本 */
178
+ function renderRequestLedger(messages) {
179
+ return renderOperations(extractOperationsFromRequest(messages));
180
+ }
181
+ exports.renderRequestLedger = renderRequestLedger;
@@ -106,6 +106,9 @@ class Session {
106
106
  lastMessageSummary = null;
107
107
  lastMessageSummaryAt = null;
108
108
  isCurrent;
109
+ // 计划模式:当前需求拆分出的执行计划
110
+ plan = [];
111
+ planUpdatedAt = null;
109
112
  // 用于停止生成
110
113
  isGenerating = false;
111
114
  unreadCount = 0;
@@ -437,6 +440,59 @@ class Session {
437
440
  this.unreadCount++;
438
441
  this.save();
439
442
  }
443
+ // ========== 计划模式 ==========
444
+ /**
445
+ * 更新当前会话的执行计划(全量替换)
446
+ */
447
+ updatePlan(steps) {
448
+ this.plan = steps.map(item => ({ step: item.step, status: item.status }));
449
+ this.planUpdatedAt = Date.now();
450
+ return this.plan;
451
+ }
452
+ /**
453
+ * 清除当前会话的执行计划
454
+ */
455
+ clearPlan() {
456
+ this.plan = [];
457
+ this.planUpdatedAt = null;
458
+ }
459
+ /**
460
+ * 计划是否已全部完成
461
+ */
462
+ isPlanCompleted() {
463
+ return this.plan.length > 0 && this.plan.every(item => item.status === 'completed');
464
+ }
465
+ /**
466
+ * 构造计划进度提醒。
467
+ *
468
+ * 模型倾向于只在任务开头和结尾调用 updatePlan,导致界面进度长期停在第一项。
469
+ * 因此当会话已有计划、且连续 roundsSincePlanUpdate 轮工具调用都没有同步进度时,
470
+ * 返回一条提醒文案注入上下文;无需提醒时返回 null。
471
+ */
472
+ buildPlanReminder(roundsSincePlanUpdate) {
473
+ // 没有计划或计划已完成,无需提醒
474
+ if (this.plan.length === 0 || this.isPlanCompleted())
475
+ return null;
476
+ // 允许模型连续做 1 轮工具调用后再要求同步,避免过于频繁地打断
477
+ if (roundsSincePlanUpdate < 2)
478
+ return null;
479
+ const current = this.plan.find(item => item.status === 'in_progress');
480
+ const nextPending = this.plan.find(item => item.status === 'pending');
481
+ const completed = this.plan.filter(item => item.status === 'completed').length;
482
+ const lines = [
483
+ `【计划进度提醒】当前计划共 ${this.plan.length} 步,已完成 ${completed} 步,`
484
+ + `已连续 ${roundsSincePlanUpdate} 轮工具调用未同步计划状态。`,
485
+ ];
486
+ if (current) {
487
+ lines.push(`进行中的步骤是「${current.step}」:如果它已经做完,请立即调用 updatePlan 把它置为 completed`
488
+ + `${nextPending ? `,并把「${nextPending.step}」置为 in_progress` : ''}。`);
489
+ }
490
+ else if (nextPending) {
491
+ lines.push(`当前没有进行中的步骤,请立即调用 updatePlan 把「${nextPending.step}」置为 in_progress。`);
492
+ }
493
+ lines.push('调用时必须传入完整步骤列表,且同一时刻最多一个步骤为 in_progress。');
494
+ return lines.join('\n');
495
+ }
440
496
  // 清除未读消息数并持久化
441
497
  clearUnreadCount() {
442
498
  this.unreadCount = 0;
@@ -489,6 +545,10 @@ class Session {
489
545
  this.stopGenerating();
490
546
  }
491
547
  }
548
+ // 上一轮计划已全部完成,新一轮需求开始前清空,避免残留在界面上
549
+ if (this.isPlanCompleted()) {
550
+ this.clearPlan();
551
+ }
492
552
  // Add user message
493
553
  this.getAbortController();
494
554
  this.isGenerating = true;
@@ -579,6 +639,8 @@ class Session {
579
639
  : (await dataService_js_1.modelsService.list(token)).data.map(x => (0, models_js_1.toModel)(x));
580
640
  // 工具调用轮次计数,防止无限循环
581
641
  let toolRound = 0;
642
+ // 计划模式:距上次调用 updatePlan 已经过的工具轮次,用于在中间过程强制提醒模型同步进度
643
+ let roundsSincePlanUpdate = 0;
582
644
  // 当前裁剪级别,遇到上下文超限时逐级加重
583
645
  let trimLevel = ContextBuilder_js_1.TrimLevel.OldToolPayload;
584
646
  /** 判断错误是否为上下文超限 */
@@ -901,6 +963,9 @@ class Session {
901
963
  };
902
964
  await Promise.all(llmResult.toolCalls.map(x => startToolCall(x)));
903
965
  toolCalls.push(toolCall);
966
+ // 计划模式:本轮是否调用了 updatePlan
967
+ const calledUpdatePlan = llmResult.toolCalls.some((tc) => tc.toolName === 'updatePlan');
968
+ roundsSincePlanUpdate = calledUpdatePlan ? 0 : roundsSincePlanUpdate + 1;
904
969
  // 将工具结果添加到消息历史(格式化为 tool 角色的消息)
905
970
  for (const result of toolResults) {
906
971
  const toolResultMessage = {
@@ -915,6 +980,17 @@ class Session {
915
980
  };
916
981
  messages.push(toolResultMessage);
917
982
  }
983
+ // 计划模式:模型常常只在开头和结尾调用 updatePlan,中间过程不同步进度。
984
+ // 这里在已有计划且连续多轮未同步时,主动注入一条提醒,迫使模型推进计划状态。
985
+ const reminder = this.buildPlanReminder(roundsSincePlanUpdate);
986
+ if (reminder) {
987
+ messages.push({
988
+ role: 'assistant',
989
+ content: reminder,
990
+ attachments: []
991
+ });
992
+ roundsSincePlanUpdate = 0;
993
+ }
918
994
  // 继续下一轮(带上工具结果继续调用模型)
919
995
  return await processModelResponse();
920
996
  }
@@ -990,6 +1066,8 @@ class Session {
990
1066
  */
991
1067
  stopGenerating() {
992
1068
  this.isGenerating = false;
1069
+ // 停止生成后清除计划,避免 Desktop 的进度圈停留在未完成状态
1070
+ this.clearPlan();
993
1071
  if (this.abortController) {
994
1072
  this.abortController.abort();
995
1073
  this.abortController = null;
@@ -79,26 +79,26 @@ function getChatStyle(settings) {
79
79
  function getIndentityInfoAsync(settings, agent) {
80
80
  const style = getChatStyle(settings);
81
81
  const agentName = agent?.name || settings.agentName || settings.assistantName;
82
- return `【身份】
83
- - 你是「${agentName}」,${settings.userName}的智能助手。${style}
82
+ return `【身份】
83
+ - 你是「${agentName}」,${settings.userName}的智能助手。${style}
84
84
  - 自称"我",称用户为"您"或「${settings.userName}」`;
85
85
  }
86
86
  function getInstalledSkillsPrompt(skills) {
87
87
  // 替换技能列表(模板中应该有占位符,如 {{skills}})
88
88
  const skillsList = skills.map((skill, index) => `skillId: "${skill.id}". **${skill.name}** - ${skill.description || '无描述'}`).join('\n');
89
89
  const skillSystemPrompt = `【技能系统】当你出现无法解决用户需求时,需要通过技能系统来帮助完成。 技能有两种,一种是通过skill工具可以获取到的技能,一种是通过clawhub安装的技能。`;
90
- const skillToolPrompt = `【skill工具技能】
91
- 【已安装skill技能】
92
- ${skillsList}
93
- 【强制调用流程】
94
- 1.必须先调用技能管理工具获取详情
95
- 2.如果技能的scriptsUrl不为空,并且当前目录中的skills目录下不存在对应的技能,需要先下载,然后解压后才可以使用,下载目录统一下载到当前目录的skills目录下的该技能目录。
90
+ const skillToolPrompt = `【skill工具技能】
91
+ 【已安装skill技能】
92
+ ${skillsList}
93
+ 【强制调用流程】
94
+ 1.必须先调用技能管理工具获取详情
95
+ 2.如果技能的scriptsUrl不为空,并且当前目录中的skills目录下不存在对应的技能,需要先下载,然后解压后才可以使用,下载目录统一下载到当前目录的skills目录下的该技能目录。
96
96
  3.下载直接使用命令行工具,不要使用fetch工具。运行完脚本记得把脚本及其解压内容删除。`;
97
- const clawhubSkillPrompt = `
98
- 【clawhub技能】
99
- 1.通过clawhub安装的技能,首先通过命令行工具查看clawhub命令是否安装
100
- 2.如果没有安装需要先安装clawhub
101
- 3.如果已经安装可以使用clawhub search命令搜索相关工具
97
+ const clawhubSkillPrompt = `
98
+ 【clawhub技能】
99
+ 1.通过clawhub安装的技能,首先通过命令行工具查看clawhub命令是否安装
100
+ 2.如果没有安装需要先安装clawhub
101
+ 3.如果已经安装可以使用clawhub search命令搜索相关工具
102
102
  4.安装后根据技能中的SKILL.md文档描述进行使用`;
103
103
  return `\n${skillSystemPrompt}\n${skillToolPrompt}\n${clawhubSkillPrompt}\n`;
104
104
  }
@@ -116,46 +116,55 @@ async function getSystemPromptAsync(agent, customPrompt) {
116
116
  const skillPrompts = getInstalledSkillsPrompt(skills);
117
117
  const chineseLocalName = exports.LANGUAGES.filter(x => x.code === settings.language)[0]?.chineseName ?? '简体中文';
118
118
  const date = new Date();
119
- let prompt = `${getIndentityInfoAsync(settings, agent)}。
120
- 【当前运行环境】:${osInfo}。
121
- 【当前时间】:${date.toLocaleString(settings.language)},weekday:${date.getDay()}。
122
- 【语言】你的回复语言严格遵循用户设备系统语言:${chineseLocalName},无论用户使用任何语言提问,你都只使用【${chineseLocalName}】进行完整回复,不要混用其他语言。
123
- 【尽力原则】充分分析用户需求,尽力满足,不要偷懒,不要得过且过,要诚实,不能胡编乱造。
124
- 【工具调用】
125
- 1.逐步思考,分步骤调用工具,工具调用前尽量返回content,明确当前工具调用的目的。
126
- 2.工具调用的参数全部采用JSON语法,调用前先验证JSON语法正确后,再回复,切记未验证JSON语法就直接回复。
127
- 【命令执行】执行命令时一定要根据当前系统来执行命令
128
- 【文件操作】
129
- 1.文件编辑后一定要验证编辑是否成功,文件内容与预期是否一致。
130
- 2.文件工具目前未提供批量替换操作,如果需要批量修改,请自行创建脚本来完成。
131
- 3.大文件写入如果无法识别,请先写入主体内容,再使用edit工具进行修改来操作
132
- 【特殊语法】mermaid折线图需要使用xychart-beta,完整示例如下:
133
- xychart-beta
134
- title "2025年上半年产品销量对比"
135
- x-axis ["1月", "2月", "3月", "4月", "5月", "6月"]
136
- y-axis "销量 (件)" 0 --> 80
137
- line "智能手机" [45, 52, 38, 65, 58, 72]
138
- line "笔记本电脑" [30, 28, 35, 42, 38, 45]
139
- line "平板电脑" [20, 25, 30, 28, 35, 40]
140
- 【解决问题思路】
141
- 1.首先分析用户需求,根据需求罗列出实施计划。如果对需求不够明确,需要把计划罗列完后,让用户确认后再执行。
142
- 2.按照计划分步执行
143
- 3.所有步骤完成后,验证结果,比如编写程序,则需要执行类型检查
144
- 4.验证完全通过,总结完成情况,告诉用户
145
- 【代码规范】
146
- ## 1 面向对象代码设计
147
- 1)根据功能先添加接口
148
- 2)对应接口添加一个基类
149
- 3)实现类默认继承这个基类
150
- ## 2 添加新功能
151
- 1)服务端根据DDD架构,进行分层设计:控制器、服务、领域服务
152
- 2)客户端将一个页面分为3部分,页面组件、页面样式、页面执行的脚本,页面执行的脚本中,数据服务与业务逻辑分开处理,数据服务又分为本地数据和远程数据
153
- ## 3 修改已经存在的代码文件
154
- 1)修改Bug,找到Bug后精确替换,不要调整架构
155
- 2)修改业务功能,删除旧的业务功能,重新添加新的,不要在旧的上面做修改,直接删除旧功能,然后添加新功能
156
- 【代码提交】务必类型检查通过
157
- 【任务完成过程中】不能独白
158
- 【会话标题】任务开始前,先修改会话标题,标题必须简洁明了,能概括任务内容
119
+ let prompt = `${getIndentityInfoAsync(settings, agent)}。
120
+ 【当前运行环境】:${osInfo}。
121
+ 【当前时间】:${date.toLocaleString(settings.language)},weekday:${date.getDay()}。
122
+ 【语言】你的回复语言严格遵循用户设备系统语言:${chineseLocalName},无论用户使用任何语言提问,你都只使用【${chineseLocalName}】进行完整回复,不要混用其他语言。
123
+ 【尽力原则】充分分析用户需求,尽力满足,不要偷懒,不要得过且过,要诚实,不能胡编乱造。
124
+ 【工具调用】
125
+ 1.逐步思考,分步骤调用工具,工具调用前尽量返回content,明确当前工具调用的目的。
126
+ 2.工具调用的参数全部采用JSON语法,调用前先验证JSON语法正确后,再回复,切记未验证JSON语法就直接回复。
127
+ 【命令执行】执行命令时一定要根据当前系统来执行命令
128
+ 【文件操作】
129
+ 1.文件编辑后一定要验证编辑是否成功,文件内容与预期是否一致。
130
+ 2.文件工具目前未提供批量替换操作,如果需要批量修改,请自行创建脚本来完成。
131
+ 3.大文件写入如果无法识别,请先写入主体内容,再使用edit工具进行修改来操作
132
+ 【特殊语法】mermaid折线图需要使用xychart-beta,完整示例如下:
133
+ xychart-beta
134
+ title "2025年上半年产品销量对比"
135
+ x-axis ["1月", "2月", "3月", "4月", "5月", "6月"]
136
+ y-axis "销量 (件)" 0 --> 80
137
+ line "智能手机" [45, 52, 38, 65, 58, 72]
138
+ line "笔记本电脑" [30, 28, 35, 42, 38, 45]
139
+ line "平板电脑" [20, 25, 30, 28, 35, 40]
140
+ 【解决问题思路】
141
+ 1.首先分析用户需求,根据需求罗列出实施计划。如果对需求不够明确,需要把计划罗列完后,让用户确认后再执行。
142
+ 2.按照计划分步执行
143
+ 3.所有步骤完成后,验证结果,比如编写程序,则需要执行类型检查
144
+ 4.验证完全通过,总结完成情况,告诉用户
145
+ 【计划模式】
146
+ 1.当用户需求需要拆分成两步以上才能完成时,必须先调用 updatePlan 工具给出完整的计划步骤(每步 status 初始为 pending,第一步直接置为 in_progress)。
147
+ 2.**每完成一个步骤就必须立刻调用一次 updatePlan**:把刚完成的步骤置为 completed,把下一个要做的步骤置为 in_progress。禁止只在开头和结尾各调用一次、中间全程不同步——这会让用户界面的进度一直停在第一项。
148
+ 3.判断标准:只要你准备开始做「下一个步骤」的事情,就说明上一个步骤已完成,此时必须先调用 updatePlan 再继续。
149
+ 4.每次调用 updatePlan 都要传入完整步骤列表,不要只传变化的部分;同一时刻最多一个步骤为 in_progress。
150
+ 5.计划步骤描述要简洁(建议不超过 30 字),可执行、可验证,不要写"分析需求""总结"之类的空步骤。
151
+ 6.如果收到【计划进度提醒】,说明你已多轮未同步进度,必须立即调用 updatePlan 修正状态。
152
+ 7.任务结束前必须把所有步骤都置为 completed,否则用户界面的计划进度圈不会消失。
153
+ 8.一句话就能答完的简单需求不要调用 updatePlan。
154
+ 【代码规范】
155
+ ## 1 面向对象代码设计
156
+ 1)根据功能先添加接口
157
+ 2)对应接口添加一个基类
158
+ 3)实现类默认继承这个基类
159
+ ## 2 添加新功能
160
+ 1)服务端根据DDD架构,进行分层设计:控制器、服务、领域服务
161
+ 2)客户端将一个页面分为3部分,页面组件、页面样式、页面执行的脚本,页面执行的脚本中,数据服务与业务逻辑分开处理,数据服务又分为本地数据和远程数据
162
+ ## 3 修改已经存在的代码文件
163
+ 1)修改Bug,找到Bug后精确替换,不要调整架构
164
+ 2)修改业务功能,删除旧的业务功能,重新添加新的,不要在旧的上面做修改,直接删除旧功能,然后添加新功能
165
+ 【代码提交】务必类型检查通过
166
+ 【任务完成过程中】不能独白
167
+ 【会话标题】任务开始前,先修改会话标题,标题必须简洁明了,能概括任务内容
159
168
  ${skillPrompts}`;
160
169
  // 如果有自定义提示词,追加
161
170
  if (customPrompt) {
@@ -37,10 +37,11 @@ const edit_js_1 = require("./edit.js");
37
37
  const webFetch_js_1 = require("./webFetch.js");
38
38
  const sessionsSpawn_js_1 = require("./sessionsSpawn.js");
39
39
  const setSessionTitle_js_1 = require("./setSessionTitle.js");
40
+ const plan_js_1 = require("./plan.js");
40
41
  exports.tools = [
41
42
  search_js_1.searchTool, calculator_js_1.calculatorTool, screenshot_js_1.screenshotTool, keyboard_js_1.keyboardTool, mouse_js_1.mouseTool,
42
43
  skill_js_1.skillTool, exec_js_1.execTool, task_js_1.taskTool, model_js_1.modelTool, fetch_js_1.fetchTool,
43
- edit_js_1.editTool, webFetch_js_1.webFetchTool, file_js_1.fileTool, sessionsSpawn_js_1.sessionsSpawnTool, setSessionTitle_js_1.setSessionTitleTool
44
+ edit_js_1.editTool, webFetch_js_1.webFetchTool, file_js_1.fileTool, sessionsSpawn_js_1.sessionsSpawnTool, setSessionTitle_js_1.setSessionTitleTool, plan_js_1.updatePlanTool
44
45
  ];
45
46
  function getToolByName(name) {
46
47
  return exports.tools.find(tool => tool.name === name);
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.updatePlanTool = void 0;
4
+ const SessionManager_js_1 = require("../session/SessionManager.js");
5
+ const WebSocketService_js_1 = require("../WebSocketService.js");
6
+ const shared_1 = require("@myassis/shared");
7
+ const logger = (0, shared_1.getLogger)('PlanTool');
8
+ const VALID_STATUS = ['pending', 'in_progress', 'completed'];
9
+ /**
10
+ * 规范化计划步骤,过滤非法数据
11
+ */
12
+ function normalizeSteps(steps) {
13
+ if (!Array.isArray(steps) || steps.length === 0)
14
+ return null;
15
+ const normalized = [];
16
+ for (const item of steps) {
17
+ if (!item)
18
+ continue;
19
+ const step = typeof item === 'string' ? item : item.step;
20
+ if (typeof step !== 'string' || step.trim().length === 0)
21
+ continue;
22
+ const status = VALID_STATUS.includes(item?.status) ? item.status : 'pending';
23
+ normalized.push({ step: step.trim(), status });
24
+ }
25
+ return normalized.length > 0 ? normalized : null;
26
+ }
27
+ exports.updatePlanTool = {
28
+ name: 'updatePlan',
29
+ description: `维护当前需求的多步执行计划(计划模式)。
30
+ 当用户需求需要拆分成两步以上才能完成时,必须先调用本工具给出完整的计划步骤。
31
+ 调用后 Desktop 会话底部会显示计划进度圈,鼠标悬浮可查看每个步骤的状态。
32
+ 规则:
33
+ 1. 每完成一个步骤都必须立刻再调用一次本工具:把已完成步骤置为 completed,把下一个步骤置为 in_progress。
34
+ 禁止只在任务开头和结尾各调用一次,中间不同步会导致用户看到的进度一直停在第一项。
35
+ 2. 每次调用都要传入完整的步骤列表(含已完成步骤),不要只传增量。
36
+ 3. 同一时刻最多只有一个步骤处于 in_progress。
37
+ 4. 全部步骤都变为 completed 后,进度圈才会消失,所以任务结束时务必把最后一个步骤标记为 completed。`,
38
+ parameters: {
39
+ type: 'object',
40
+ properties: {
41
+ steps: {
42
+ type: 'array',
43
+ description: '完整的计划步骤列表,按执行顺序排列',
44
+ items: {
45
+ type: 'object',
46
+ description: '单个计划步骤',
47
+ properties: {
48
+ step: { type: 'string', description: '步骤描述,简洁明确,建议不超过 30 字' },
49
+ status: {
50
+ type: 'string',
51
+ description: '步骤状态:pending 待完成、in_progress 进行中、completed 已完成',
52
+ enum: ['pending', 'in_progress', 'completed'],
53
+ default: 'pending',
54
+ },
55
+ },
56
+ required: ['step', 'status'],
57
+ },
58
+ },
59
+ explanation: {
60
+ type: 'string',
61
+ description: '可选,本次计划变更的简要说明',
62
+ },
63
+ },
64
+ required: ['steps'],
65
+ },
66
+ handler: async (args, sessionId, _messageId, userId) => {
67
+ try {
68
+ const steps = normalizeSteps(args?.steps);
69
+ if (!steps) {
70
+ return { success: false, errorMessage: '计划步骤不能为空,steps 需要是 [{ step, status }] 数组' };
71
+ }
72
+ if (steps.filter(s => s.status === 'in_progress').length > 1) {
73
+ return { success: false, errorMessage: '同一时刻最多只能有一个步骤处于 in_progress' };
74
+ }
75
+ if (!sessionId) {
76
+ return { success: false, errorMessage: '无当前会话,无法更新计划' };
77
+ }
78
+ if (!userId) {
79
+ return { success: false, errorMessage: '未登录' };
80
+ }
81
+ const session = (0, SessionManager_js_1.getSessionManager)(String(userId)).getSession(sessionId);
82
+ if (!session) {
83
+ return { success: false, errorMessage: '会话不存在' };
84
+ }
85
+ session.updatePlan(steps);
86
+ // 推送计划进度到 Desktop,驱动底部进度圈刷新
87
+ WebSocketService_js_1.webSocketService.sendToUser(String(userId), {
88
+ type: 'plan_updated',
89
+ payload: {
90
+ sessionId,
91
+ agentId: session.agentId,
92
+ steps: session.plan,
93
+ updatedAt: session.planUpdatedAt,
94
+ },
95
+ });
96
+ const completed = steps.filter(s => s.status === 'completed').length;
97
+ return {
98
+ success: true,
99
+ output: JSON.stringify({
100
+ total: steps.length,
101
+ completed,
102
+ steps,
103
+ }),
104
+ };
105
+ }
106
+ catch (error) {
107
+ logger.error('更新计划失败:', error);
108
+ return { success: false, errorMessage: `更新计划失败: ${error?.message}` };
109
+ }
110
+ },
111
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myassis/gateway",
3
- "version": "1.0.78",
3
+ "version": "1.0.81",
4
4
  "description": "我的助手 Gateway Service - 本地 AI 网关服务,支持认证、WebSocket 实时通信和任务调度",
5
5
  "main": "dist/index.js",
6
6
  "bin": {