autosnippet 2.4.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.
- package/bin/cli.js +35 -0
- package/dashboard/dist/assets/{icons-B5rs8uNb.js → icons-rnn04CvH.js} +100 -85
- package/dashboard/dist/assets/index-BBKa3Dgi.js +195 -0
- package/dashboard/dist/assets/index-DLsECfzW.css +1 -0
- package/dashboard/dist/assets/{react-markdown-Bp8u1wRC.js → react-markdown-CWxUbOf4.js} +1 -1
- package/dashboard/dist/assets/{syntax-highlighter-C6bvFtpx.js → syntax-highlighter-CJ2drQQb.js} +1 -1
- package/dashboard/dist/assets/{vendor-Cky7Jynh.js → vendor-f83ah6cm.js} +13 -13
- package/dashboard/dist/index.html +6 -6
- package/lib/cli/SetupService.js +30 -4
- package/lib/core/gateway/Gateway.js +19 -4
- package/lib/external/ai/AiProvider.js +94 -10
- package/lib/external/mcp/McpServer.js +2 -1
- package/lib/external/mcp/handlers/skill.js +76 -18
- package/lib/external/mcp/tools.js +21 -0
- package/lib/http/HttpServer.js +4 -0
- package/lib/http/routes/search.js +5 -3
- package/lib/http/routes/skills.js +108 -0
- package/lib/infrastructure/audit/AuditStore.js +18 -0
- package/lib/injection/ServiceContainer.js +8 -2
- package/lib/service/chat/ChatAgent.js +281 -33
- package/lib/service/chat/ConversationStore.js +377 -0
- package/lib/service/chat/Memory.js +40 -10
- package/lib/service/chat/tools.js +104 -7
- package/lib/service/skills/EventAggregator.js +187 -0
- package/lib/service/skills/SignalCollector.js +524 -0
- package/lib/service/skills/SkillAdvisor.js +323 -0
- package/package.json +1 -1
- package/dashboard/dist/assets/index-0YzLw2ga.css +0 -1
- package/dashboard/dist/assets/index-B9py3ybr.js +0 -154
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
* @
|
|
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
|
-
...
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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
|
-
*
|
|
776
|
-
*
|
|
910
|
+
* 从对话中提取值得记忆的信息写入 Memory
|
|
911
|
+
*
|
|
912
|
+
* 双层策略:
|
|
913
|
+
* 1. 规则快速匹配(零延迟,覆盖明确的中英文模式)
|
|
914
|
+
* 2. AI 驱动提取(异步后台,从 reply 中提取 [MEMORY] 标签)
|
|
915
|
+
*
|
|
916
|
+
* source 隔离: 标记 memory 来源,避免系统分析污染用户记忆
|
|
777
917
|
*/
|
|
778
|
-
#extractMemory(prompt,
|
|
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
|
*/
|