autosnippet 2.5.0 → 2.6.0

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.
@@ -5,7 +5,7 @@
5
5
 
6
6
  import express from 'express';
7
7
  import { asyncHandler } from '../middleware/errorHandler.js';
8
- import { listSkills, loadSkill, createSkill } from '../../external/mcp/handlers/skill.js';
8
+ import { listSkills, loadSkill, createSkill, suggestSkills } from '../../external/mcp/handlers/skill.js';
9
9
  import { ValidationError } from '../../shared/errors/index.js';
10
10
 
11
11
  const router = express.Router();
@@ -25,6 +25,41 @@ router.get('/', asyncHandler(async (_req, res) => {
25
25
  res.json({ success: true, data: parsed.data });
26
26
  }));
27
27
 
28
+ /**
29
+ * GET /api/v1/skills/signal-status
30
+ * 获取 SignalCollector 后台服务状态
31
+ */
32
+ router.get('/signal-status', asyncHandler(async (_req, res) => {
33
+ const { _signalCollector } = global;
34
+ if (!_signalCollector) {
35
+ return res.json({ success: true, data: { running: false, mode: 'off', snapshot: null } });
36
+ }
37
+ res.json({
38
+ success: true,
39
+ data: {
40
+ running: true,
41
+ mode: _signalCollector.getMode(),
42
+ snapshot: _signalCollector.getSnapshot(),
43
+ },
44
+ });
45
+ }));
46
+
47
+ /**
48
+ * GET /api/v1/skills/suggest
49
+ * 基于使用模式分析,推荐创建 Skill
50
+ */
51
+ router.get('/suggest', asyncHandler(async (req, res) => {
52
+ const ctx = { container: req.app.locals?.container || null };
53
+ const raw = await suggestSkills(ctx);
54
+ const parsed = JSON.parse(raw);
55
+
56
+ if (!parsed.success) {
57
+ return res.status(500).json(parsed);
58
+ }
59
+
60
+ res.json({ success: true, data: parsed.data });
61
+ }));
62
+
28
63
  /**
29
64
  * GET /api/v1/skills/:name
30
65
  * 加载指定 Skill 的完整文档
@@ -51,13 +86,13 @@ router.get('/:name', asyncHandler(async (req, res) => {
51
86
  * Body: { name, description, content, overwrite? }
52
87
  */
53
88
  router.post('/', asyncHandler(async (req, res) => {
54
- const { name, description, content, overwrite } = req.body;
89
+ const { name, description, content, overwrite, createdBy } = req.body;
55
90
 
56
91
  if (!name || !description || !content) {
57
92
  throw new ValidationError('name, description, content are all required');
58
93
  }
59
94
 
60
- const raw = createSkill(null, { name, description, content, overwrite });
95
+ const raw = createSkill(null, { name, description, content, overwrite, createdBy: createdBy || 'manual' });
61
96
  const parsed = JSON.parse(raw);
62
97
 
63
98
  if (!parsed.success) {
@@ -193,6 +193,24 @@ export class AuditStore {
193
193
  byAction,
194
194
  };
195
195
  }
196
+
197
+ /**
198
+ * 清理过期审计日志
199
+ * @param {object} [opts]
200
+ * @param {number} [opts.maxAgeDays=90] — 保留天数,超过此天数的记录将被删除
201
+ * @returns {{ deleted: number }}
202
+ */
203
+ cleanup({ maxAgeDays = 90 } = {}) {
204
+ try {
205
+ const cutoff = Date.now() - maxAgeDays * 86400000;
206
+ const result = this.db.prepare(
207
+ 'DELETE FROM audit_logs WHERE timestamp < ?'
208
+ ).run(cutoff);
209
+ return { deleted: result.changes || 0 };
210
+ } catch {
211
+ return { deleted: 0 };
212
+ }
213
+ }
196
214
  }
197
215
 
198
216
  export default AuditStore;
@@ -52,7 +52,7 @@ import { ALL_TOOLS } from '../service/chat/tools.js';
52
52
  import { SkillHooks } from '../service/skills/SkillHooks.js';
53
53
 
54
54
  // ─── P3: Infrastructure ──────────────────────────────
55
- // EventBus / PluginManager imports removed — source files retained for future use
55
+ import { EventBus } from '../infrastructure/event/EventBus.js';
56
56
 
57
57
  /**
58
58
  * DependencyInjection 容器
@@ -201,7 +201,13 @@ export class ServiceContainer {
201
201
  return this.singletons.gateway;
202
202
  });
203
203
 
204
-
204
+ // EventBus(全局事件总线)
205
+ this.register('eventBus', () => {
206
+ if (!this.singletons.eventBus) {
207
+ this.singletons.eventBus = new EventBus({ maxListeners: 30 });
208
+ }
209
+ return this.singletons.eventBus;
210
+ });
205
211
  }
206
212
 
207
213
  /**
@@ -28,6 +28,7 @@ import { fileURLToPath } from 'node:url';
28
28
  import Logger from '../../infrastructure/logging/Logger.js';
29
29
  import { TaskPipeline } from './TaskPipeline.js';
30
30
  import { Memory } from './Memory.js';
31
+ import { ConversationStore } from './ConversationStore.js';
31
32
 
32
33
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
33
34
  const PROJECT_ROOT = path.resolve(__dirname, '../../..');
@@ -46,6 +47,10 @@ export class ChatAgent {
46
47
  #projectBriefingCache = '';
47
48
  /** @type {Memory|null} 跨对话轻量记忆 */
48
49
  #memory = null;
50
+ /** @type {ConversationStore|null} 对话持久化 */
51
+ #conversations = null;
52
+ /** @type {string|null} 当前 execute 调用的 source — 'user' | 'system' */
53
+ #currentSource = null;
49
54
 
50
55
  /**
51
56
  * @param {object} opts
@@ -62,11 +67,12 @@ export class ChatAgent {
62
67
  /** 是否有 AI Provider(只读) */
63
68
  this.hasAI = !!aiProvider;
64
69
 
65
- // 初始化跨对话记忆
70
+ // 初始化跨对话记忆 + 对话持久化
66
71
  try {
67
72
  const projectRoot = container?.singletons?._projectRoot || process.cwd();
68
73
  this.#memory = new Memory(projectRoot);
69
- } catch { /* Memory init failed, degrade silently */ }
74
+ this.#conversations = new ConversationStore(projectRoot);
75
+ } catch { /* Memory/ConversationStore init failed, degrade silently */ }
70
76
 
71
77
  // 注册内置 DAG 管线
72
78
  this.#registerBuiltinPipelines();
@@ -81,9 +87,20 @@ export class ChatAgent {
81
87
  * @param {string} prompt — 用户消息
82
88
  * @param {object} opts
83
89
  * @param {Array} opts.history — 对话历史 [{role, content}]
84
- * @returns {Promise<{reply: string, toolCalls: Array, hasContext: boolean}>}
90
+ * @param {string} [opts.conversationId] 对话 ID(启用持久化时)
91
+ * @param {'user'|'system'} [opts.source='user'] — 调用来源(影响 Memory 隔离)
92
+ * @returns {Promise<{reply: string, toolCalls: Array, hasContext: boolean, conversationId?: string}>}
85
93
  */
86
- async execute(prompt, { history = [] } = {}) {
94
+ async execute(prompt, { history = [], conversationId, source = 'user' } = {}) {
95
+ this.#currentSource = source;
96
+
97
+ // 对话持久化: 如果传了 conversationId,从 ConversationStore 加载历史
98
+ let effectiveHistory = history;
99
+ if (conversationId && this.#conversations) {
100
+ effectiveHistory = this.#conversations.load(conversationId);
101
+ this.#conversations.append(conversationId, { role: 'user', content: prompt });
102
+ }
103
+
87
104
  // 每次对话刷新项目概况(不是每轮 ReAct)
88
105
  this.#projectBriefingCache = await this.#buildProjectBriefing();
89
106
 
@@ -92,7 +109,7 @@ export class ChatAgent {
92
109
 
93
110
  // 首次 LLM 调用
94
111
  const messages = [
95
- ...history,
112
+ ...effectiveHistory,
96
113
  { role: 'user', content: prompt },
97
114
  ];
98
115
 
@@ -100,13 +117,34 @@ export class ChatAgent {
100
117
  let iterations = 0;
101
118
  let currentPrompt = prompt;
102
119
 
120
+ let consecutiveAiErrors = 0;
121
+
103
122
  while (iterations < MAX_ITERATIONS) {
104
123
  iterations++;
105
124
 
106
- const response = await this.#aiProvider.chat(currentPrompt, {
107
- history: messages.slice(0, -1), // 不含最新 user prompt
108
- systemPrompt,
109
- });
125
+ let response;
126
+ try {
127
+ response = await this.#aiProvider.chat(currentPrompt, {
128
+ history: messages.slice(0, -1), // 不含最新 user prompt
129
+ systemPrompt,
130
+ });
131
+ consecutiveAiErrors = 0;
132
+ } catch (aiErr) {
133
+ consecutiveAiErrors++;
134
+ this.#logger.warn(`[ChatAgent] AI call failed (attempt ${consecutiveAiErrors}): ${aiErr.message}`);
135
+
136
+ // 连续 2 次失败则降级返回错误提示
137
+ if (consecutiveAiErrors >= 2) {
138
+ const errorReply = `抱歉,AI 服务暂时不可用(${aiErr.message})。请稍后重试,或检查 API 配置。`;
139
+ if (conversationId && this.#conversations) {
140
+ this.#conversations.append(conversationId, { role: 'assistant', content: errorReply });
141
+ }
142
+ return { reply: errorReply, toolCalls, hasContext: toolCalls.length > 0, conversationId };
143
+ }
144
+ // 首次失败:等待后重试本轮
145
+ await new Promise(r => setTimeout(r, 2000));
146
+ continue;
147
+ }
110
148
 
111
149
  // 尝试解析 Action 块
112
150
  const action = this.#parseAction(response);
@@ -115,7 +153,15 @@ export class ChatAgent {
115
153
  // 没有 Action → 最终回答
116
154
  const reply = this.#cleanFinalAnswer(response);
117
155
  this.#extractMemory(prompt, reply);
118
- return { reply, toolCalls, hasContext: toolCalls.length > 0 };
156
+
157
+ // 持久化 assistant 回复
158
+ if (conversationId && this.#conversations) {
159
+ this.#conversations.append(conversationId, { role: 'assistant', content: reply });
160
+ // 消息过多时自动压缩
161
+ this.#autoSummarize(conversationId).catch(() => {});
162
+ }
163
+
164
+ return { reply, toolCalls, hasContext: toolCalls.length > 0, conversationId };
119
165
  }
120
166
 
121
167
  // 执行工具
@@ -124,11 +170,18 @@ export class ChatAgent {
124
170
  iteration: iterations,
125
171
  });
126
172
 
127
- const toolResult = await this.#toolRegistry.execute(
128
- action.tool,
129
- action.params,
130
- this.#getToolContext(),
131
- );
173
+ let toolResult;
174
+ try {
175
+ toolResult = await this.#toolRegistry.execute(
176
+ action.tool,
177
+ action.params,
178
+ this.#getToolContext(),
179
+ );
180
+ } catch (toolErr) {
181
+ this.#logger.warn(`[ChatAgent] Tool "${action.tool}" failed: ${toolErr.message}`);
182
+ // 将错误反馈给 LLM,让它尝试其他方法
183
+ toolResult = `Error: tool "${action.tool}" failed — ${toolErr.message}. Try a different approach or provide your answer based on available information.`;
184
+ }
132
185
 
133
186
  toolCalls.push({
134
187
  tool: action.tool,
@@ -146,22 +199,42 @@ export class ChatAgent {
146
199
  // 追加到消息历史中以保持上下文
147
200
  messages.push({ role: 'assistant', content: response });
148
201
  messages.push({ role: 'user', content: currentPrompt });
202
+
203
+ // ── Context Window 自动压缩(Cline AutoCondense 模式)──
204
+ // 每轮 ReAct 后检测消息总 token,超过预算时压缩中段消息
205
+ this.#condenseIfNeeded(messages);
149
206
  }
150
207
 
151
208
  // 达到最大迭代次数,要求 LLM 总结
152
209
  const summaryPrompt = `You have used ${iterations} tool calls. Summarize what you found and answer the user's original question: "${prompt}"`;
153
- const finalResponse = await this.#aiProvider.chat(summaryPrompt, {
154
- history: messages,
155
- systemPrompt: '直接回答用户问题,不要再调用工具。',
156
- });
210
+ let finalResponse;
211
+ try {
212
+ finalResponse = await this.#aiProvider.chat(summaryPrompt, {
213
+ history: messages,
214
+ systemPrompt: '直接回答用户问题,不要再调用工具。',
215
+ });
216
+ } catch (err) {
217
+ this.#logger.warn(`[ChatAgent] Final summary AI call failed: ${err.message}`);
218
+ // 降级:用工具调用结果拼一个简单回复
219
+ finalResponse = `根据 ${toolCalls.length} 次工具调用的结果,以下是收集到的信息:\n\n` +
220
+ toolCalls.map(tc => `• ${tc.tool}: ${typeof tc.result === 'string' ? tc.result.substring(0, 200) : JSON.stringify(tc.result).substring(0, 200)}`).join('\n') +
221
+ '\n\n(注:AI 总结服务暂时不可用,上述为原始工具输出摘要)';
222
+ }
157
223
 
158
224
  const finalReply = this.#cleanFinalAnswer(finalResponse);
159
225
  this.#extractMemory(prompt, finalReply);
160
226
 
227
+ // 持久化 assistant 回复
228
+ if (conversationId && this.#conversations) {
229
+ this.#conversations.append(conversationId, { role: 'assistant', content: finalReply });
230
+ this.#autoSummarize(conversationId).catch(() => {});
231
+ }
232
+
161
233
  return {
162
234
  reply: finalReply,
163
235
  toolCalls,
164
236
  hasContext: toolCalls.length > 0,
237
+ conversationId,
165
238
  };
166
239
  }
167
240
 
@@ -177,6 +250,40 @@ export class ChatAgent {
177
250
  return this.#toolRegistry.execute(toolName, params, this.#getToolContext());
178
251
  }
179
252
 
253
+ // ─── 对话管理 API ──────────────────────────────────────
254
+
255
+ /**
256
+ * 创建新对话(用于 Dashboard 前端)
257
+ * @param {object} [opts]
258
+ * @param {'user'|'system'} [opts.category='user']
259
+ * @param {string} [opts.title]
260
+ * @returns {string} conversationId
261
+ */
262
+ createConversation({ category = 'user', title = '' } = {}) {
263
+ if (!this.#conversations) return null;
264
+ return this.#conversations.create({ category, title });
265
+ }
266
+
267
+ /**
268
+ * 获取对话列表
269
+ * @param {object} [opts]
270
+ * @param {'user'|'system'} [opts.category]
271
+ * @param {number} [opts.limit=20]
272
+ * @returns {Array}
273
+ */
274
+ getConversations({ category, limit = 20 } = {}) {
275
+ if (!this.#conversations) return [];
276
+ return this.#conversations.list({ category, limit });
277
+ }
278
+
279
+ /**
280
+ * 获取 ConversationStore 实例(供外部使用,如 HTTP 路由)
281
+ * @returns {ConversationStore|null}
282
+ */
283
+ getConversationStore() {
284
+ return this.#conversations;
285
+ }
286
+
180
287
  /**
181
288
  * 预定义任务流
182
289
  * 将常见多步骤操作封装为一个任务名。
@@ -576,14 +683,38 @@ ${code.substring(0, 3000)}
576
683
 
577
684
  /**
578
685
  * 构建系统提示词(含工具描述 + Skills 感知)
686
+ *
687
+ * 工具注入策略(Lazy Tool Schema — 类似 Cline .clinerules 按需加载):
688
+ * - 首屏只注入工具名 + 一行描述(compact list)
689
+ * - 系统提示词中告知 LLM 可通过 get_tool_details 获取完整参数
690
+ * - 少量核心工具(search_knowledge, submit_with_check, analyze_code,
691
+ * bootstrap_knowledge, load_skill, suggest_skills)直接展开完整 schema
692
+ *
693
+ * 效果: 39 个工具的 prompt 从 ~5000 tokens 降到 ~1500 tokens
579
694
  */
580
695
  #buildSystemPrompt(toolSchemas) {
581
- const toolDescriptions = toolSchemas.map(t => {
582
- const paramsDesc = Object.entries(t.parameters.properties || {})
583
- .map(([k, v]) => ` - ${k} (${v.type}): ${v.description || ''}`)
584
- .join('\n');
585
- return `- **${t.name}**: ${t.description}\n Parameters:\n${paramsDesc || ' (none)'}`;
586
- }).join('\n\n');
696
+ // 核心工具 使用最频繁,直接展示完整 schema
697
+ const coreTools = new Set([
698
+ 'search_knowledge', 'submit_with_check', 'analyze_code',
699
+ 'bootstrap_knowledge', 'load_skill', 'suggest_skills',
700
+ 'create_skill', 'knowledge_overview', 'get_tool_details',
701
+ ]);
702
+
703
+ const compactDescriptions = [];
704
+ const detailedDescriptions = [];
705
+
706
+ for (const t of toolSchemas) {
707
+ if (coreTools.has(t.name)) {
708
+ const paramsDesc = Object.entries(t.parameters.properties || {})
709
+ .map(([k, v]) => ` - ${k} (${v.type}): ${v.description || ''}`)
710
+ .join('\n');
711
+ detailedDescriptions.push(`- **${t.name}**: ${t.description}\n Parameters:\n${paramsDesc || ' (none)'}`);
712
+ } else {
713
+ compactDescriptions.push(`- ${t.name}: ${t.description}`);
714
+ }
715
+ }
716
+
717
+ const toolDescriptions = `### 核心工具(完整参数)\n\n${detailedDescriptions.join('\n\n')}\n\n### 其他工具(调用 get_tool_details 获取参数详情)\n\n${compactDescriptions.join('\n')}`;
587
718
 
588
719
  // Skills 清单 — 让 LLM 知道有哪些领域知识可加载
589
720
  const skillList = this.#listAvailableSkills();
@@ -602,7 +733,7 @@ ${code.substring(0, 3000)}
602
733
  return `${soulSection}
603
734
  你是 AutoSnippet 项目的统一 AI 中心。项目内所有 AI 推理和分析都通过你执行。
604
735
  你拥有 ${toolSchemas.length} 个工具覆盖知识库管理全链路:搜索、提交、审核、质量评估、Guard 检查、知识图谱、冷启动等。
605
- ${this.#projectBriefingCache}${this.#memory?.toPromptSection() || ''}
736
+ ${this.#projectBriefingCache}${this.#memory?.toPromptSection({ source: this.#currentSource === 'system' ? undefined : 'user' }) || ''}
606
737
  可用工具:
607
738
 
608
739
  ${toolDescriptions}
@@ -627,7 +758,9 @@ ${skillSection}
627
758
  - 不确定做什么 → load_skill("autosnippet-intent")
628
759
  8. 你可以组合多个工具完成复杂任务(如:查重 → 提交 → 质量评分 → 知识图谱关联)。
629
760
  9. 当工具返回 _meta.confidence = "none" 时,告知用户无匹配并建议下一步,不要凭空编造。当 _meta.confidence = "low" 时,明确标注结果不确定性。
630
- 10. 优先使用组合工具(analyze_code, knowledge_overview, submit_with_check)减少调用轮次。`;
761
+ 10. 优先使用组合工具(analyze_code, knowledge_overview, submit_with_check)减少调用轮次。
762
+ 11. 当你发现用户在重复解释编码规范、操作约定或项目特有模式时,主动调用 suggest_skills 检查是否需要创建 Skill。如果有高优先级建议,向用户说明并在确认后调用 create_skill 创建。
763
+ 12. 当对话中出现值得长期记忆的信息(用户偏好、项目规范、关键决策、技术栈事实),在回复中嵌入记忆标签:\`[MEMORY:type] 内容 [/MEMORY]\`,type 可选 preference/decision/context。这些标签会被自动提取并持久化,不会显示给用户。`;
631
764
  }
632
765
 
633
766
  /**
@@ -664,13 +797,14 @@ ${skillSection}
664
797
  }
665
798
 
666
799
  /**
667
- * 清理最终回答(去除 Thought/preamble
800
+ * 清理最终回答(去除 Thought/preamble + MEMORY 标签)
668
801
  */
669
802
  #cleanFinalAnswer(response) {
670
803
  if (!response) return '';
671
- // 去除 "Final Answer:" 前缀
672
804
  return response
673
805
  .replace(/^(Final Answer|最终回答|Answer)\s*[::]\s*/i, '')
806
+ .replace(/\[MEMORY:\w+\]\s*[\s\S]*?\s*\[\/MEMORY\]/g, '')
807
+ .replace(/\n{3,}/g, '\n\n')
674
808
  .trim();
675
809
  }
676
810
 
@@ -683,6 +817,7 @@ ${skillSection}
683
817
  aiProvider: this.#aiProvider,
684
818
  projectRoot: this.#container?.singletons?._projectRoot || process.cwd(),
685
819
  logger: this.#logger,
820
+ source: this.#currentSource,
686
821
  };
687
822
  }
688
823
 
@@ -772,27 +907,92 @@ ${skillSection}
772
907
  }
773
908
 
774
909
  /**
775
- * 从用户消息中提取偏好/决策写入 Memory
776
- * 使用正则匹配,不调 AI — 零延迟
910
+ * 从对话中提取值得记忆的信息写入 Memory
911
+ *
912
+ * 双层策略:
913
+ * 1. 规则快速匹配(零延迟,覆盖明确的中英文模式)
914
+ * 2. AI 驱动提取(异步后台,从 reply 中提取 [MEMORY] 标签)
915
+ *
916
+ * source 隔离: 标记 memory 来源,避免系统分析污染用户记忆
777
917
  */
778
- #extractMemory(prompt, _reply) {
918
+ #extractMemory(prompt, reply) {
779
919
  if (!this.#memory) return;
920
+ const source = this.#currentSource || 'user';
921
+
780
922
  try {
923
+ // ── 层 1: 规则快速匹配(中文 + 英文) ──
781
924
  const prefPatterns = [
782
925
  /我们(项目|团队)?(不用|不使用|禁止|避免|偏好|习惯|规范是)/,
783
926
  /以后(都|请|要)/,
784
927
  /记住/,
928
+ /we\s+(don'?t|never|always|prefer|avoid)\s+use/i,
929
+ /remember\s+(to|that)/i,
930
+ /our\s+(convention|standard|rule)\s+is/i,
785
931
  ];
786
932
  if (prefPatterns.some(p => p.test(prompt))) {
787
933
  this.#memory.append({
788
934
  type: 'preference',
789
935
  content: prompt.substring(0, 200),
936
+ source,
790
937
  ttl: 30,
791
938
  });
792
939
  }
940
+
941
+ const decisionPatterns = [
942
+ /决定(了|用|采用|使用)/,
943
+ /(确认|同意|通过)(了|这个方案|审核)/,
944
+ /就(这样|这么)(做|定|办)/,
945
+ /let'?s\s+(go\s+with|use|adopt)/i,
946
+ /approved|confirmed|decided/i,
947
+ ];
948
+ if (decisionPatterns.some(p => p.test(prompt))) {
949
+ this.#memory.append({
950
+ type: 'decision',
951
+ content: prompt.substring(0, 200),
952
+ source,
953
+ ttl: 60,
954
+ });
955
+ }
956
+
957
+ // ── 层 2: 从 AI reply 中提取 [MEMORY] 标签 ──
958
+ // AI 可在回复中嵌入: [MEMORY:preference] 内容 [/MEMORY]
959
+ if (reply) {
960
+ const memoryTagRegex = /\[MEMORY:(\w+)\]\s*([\s\S]*?)\s*\[\/MEMORY\]/g;
961
+ let match;
962
+ while ((match = memoryTagRegex.exec(reply)) !== null) {
963
+ const type = match[1]; // preference | decision | context
964
+ const content = match[2].trim();
965
+ if (content && ['preference', 'decision', 'context'].includes(type)) {
966
+ this.#memory.append({
967
+ type,
968
+ content: content.substring(0, 200),
969
+ source,
970
+ ttl: type === 'context' ? 90 : type === 'decision' ? 60 : 30,
971
+ });
972
+ }
973
+ }
974
+ }
793
975
  } catch { /* memory write failure is non-critical */ }
794
976
  }
795
977
 
978
+ /**
979
+ * 自动压缩过长的对话(异步后台执行)
980
+ * 当对话消息数超过 12 条时触发 AI 摘要压缩
981
+ */
982
+ async #autoSummarize(conversationId) {
983
+ if (!this.#conversations || !this.#aiProvider) return;
984
+ try {
985
+ const messages = this.#conversations.load(conversationId, { tokenBudget: Infinity });
986
+ if (messages.length >= 12) {
987
+ await this.#conversations.summarize(conversationId, {
988
+ aiProvider: this.#aiProvider,
989
+ });
990
+ }
991
+ } catch {
992
+ // 摘要失败不影响主流程
993
+ }
994
+ }
995
+
796
996
  /**
797
997
  * 事件驱动入口(P2 预留接口)
798
998
  * @param {{ type: string, payload: object, source?: string }} event
@@ -800,7 +1000,7 @@ ${skillSection}
800
1000
  async executeEvent(event) {
801
1001
  const { type, payload } = event;
802
1002
  const prompt = this.#eventToPrompt(type, payload);
803
- return this.execute(prompt, { history: [] });
1003
+ return this.execute(prompt, { history: [], source: 'system' });
804
1004
  }
805
1005
 
806
1006
  #eventToPrompt(type, payload) {
@@ -816,6 +1016,54 @@ ${skillSection}
816
1016
  }
817
1017
  }
818
1018
 
1019
+ /**
1020
+ * Context Window 自动压缩(受 Cline AutoCondense 启发)
1021
+ *
1022
+ * 在 ReAct 循环中实时检测消息总 token 数。
1023
+ * 当超过 TOKEN_BUDGET 时,保留:
1024
+ * - 首条消息(可能是 system / 用户首问)
1025
+ * - 最后 4 条消息(当前推理上下文)
1026
+ * 中间消息压缩为一条摘要。
1027
+ *
1028
+ * 策略: 非阻塞、纯规则(不调 AI),避免 ReAct 循环内引入额外 AI 调用。
1029
+ */
1030
+ #condenseIfNeeded(messages, tokenBudget = 10000) {
1031
+ const estimateTokens = (text) => Math.ceil((text || '').length / 3.5);
1032
+
1033
+ let totalTokens = 0;
1034
+ for (const m of messages) totalTokens += estimateTokens(m.content);
1035
+
1036
+ if (totalTokens <= tokenBudget || messages.length <= 6) return;
1037
+
1038
+ // 保留首条 + 最后 4 条,压缩中间
1039
+ const keepTail = 4;
1040
+ const first = messages[0];
1041
+ const tail = messages.slice(-keepTail);
1042
+ const middle = messages.slice(1, -keepTail);
1043
+
1044
+ if (middle.length === 0) return;
1045
+
1046
+ // 生成摘要
1047
+ const toolCallSummary = middle
1048
+ .filter(m => m.role === 'user' && m.content.startsWith('Observation from tool'))
1049
+ .map(m => {
1050
+ const toolMatch = m.content.match(/Observation from tool "([^"]+)"/);
1051
+ return toolMatch ? toolMatch[1] : null;
1052
+ })
1053
+ .filter(Boolean);
1054
+
1055
+ const condensed = {
1056
+ role: 'system',
1057
+ content: `[上下文压缩] 省略了 ${middle.length} 条中间消息(含工具调用: ${toolCallSummary.join(', ') || '无'})。请基于最近的 observation 继续推理。`,
1058
+ };
1059
+
1060
+ // 原地修改数组
1061
+ messages.length = 0;
1062
+ messages.push(first, condensed, ...tail);
1063
+
1064
+ this.#logger.debug(`[ChatAgent] condensed ${middle.length} messages (${totalTokens} → ~${estimateTokens(first.content) + estimateTokens(condensed.content) + tail.reduce((s, m) => s + estimateTokens(m.content), 0)} tokens)`);
1065
+ }
1066
+
819
1067
  /**
820
1068
  * 截断长文本
821
1069
  */