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.
Files changed (29) hide show
  1. package/bin/cli.js +35 -0
  2. package/dashboard/dist/assets/{icons-B5rs8uNb.js → icons-rnn04CvH.js} +100 -85
  3. package/dashboard/dist/assets/index-BBKa3Dgi.js +195 -0
  4. package/dashboard/dist/assets/index-DLsECfzW.css +1 -0
  5. package/dashboard/dist/assets/{react-markdown-Bp8u1wRC.js → react-markdown-CWxUbOf4.js} +1 -1
  6. package/dashboard/dist/assets/{syntax-highlighter-C6bvFtpx.js → syntax-highlighter-CJ2drQQb.js} +1 -1
  7. package/dashboard/dist/assets/{vendor-Cky7Jynh.js → vendor-f83ah6cm.js} +13 -13
  8. package/dashboard/dist/index.html +6 -6
  9. package/lib/cli/SetupService.js +30 -4
  10. package/lib/core/gateway/Gateway.js +19 -4
  11. package/lib/external/ai/AiProvider.js +94 -10
  12. package/lib/external/mcp/McpServer.js +2 -1
  13. package/lib/external/mcp/handlers/skill.js +76 -18
  14. package/lib/external/mcp/tools.js +21 -0
  15. package/lib/http/HttpServer.js +4 -0
  16. package/lib/http/routes/search.js +5 -3
  17. package/lib/http/routes/skills.js +108 -0
  18. package/lib/infrastructure/audit/AuditStore.js +18 -0
  19. package/lib/injection/ServiceContainer.js +8 -2
  20. package/lib/service/chat/ChatAgent.js +281 -33
  21. package/lib/service/chat/ConversationStore.js +377 -0
  22. package/lib/service/chat/Memory.js +40 -10
  23. package/lib/service/chat/tools.js +104 -7
  24. package/lib/service/skills/EventAggregator.js +187 -0
  25. package/lib/service/skills/SignalCollector.js +524 -0
  26. package/lib/service/skills/SkillAdvisor.js +323 -0
  27. package/package.json +1 -1
  28. package/dashboard/dist/assets/index-0YzLw2ga.css +0 -1
  29. package/dashboard/dist/assets/index-B9py3ybr.js +0 -154
@@ -0,0 +1,377 @@
1
+ /**
2
+ * ConversationStore — 对话持久化 + 上下文窗口管理
3
+ *
4
+ * 设计:
5
+ * - 每个对话一个 JSONL 文件: .autosnippet/conversations/{id}.jsonl
6
+ * - 索引文件: .autosnippet/conversations/index.json
7
+ * - 按 category 隔离: 'user'(Dashboard) / 'system'(SignalCollector)
8
+ * - Token 预算: 超限时自动生成摘要压缩旧轮次
9
+ * - 静默降级: 持久化失败不影响核心功能
10
+ *
11
+ * Token 计算策略:
12
+ * 采用字符数近似估算 (1 token ≈ 3.5 字符中文 / ≈ 4 字符英文)
13
+ * 简单高效,无需额外依赖
14
+ *
15
+ * 文件结构:
16
+ * .autosnippet/conversations/
17
+ * index.json — 对话元数据索引
18
+ * {id}.jsonl — 每行一条消息 {role, content, ts}
19
+ */
20
+
21
+ import fs from 'node:fs';
22
+ import path from 'node:path';
23
+ import crypto from 'node:crypto';
24
+ import Logger from '../../infrastructure/logging/Logger.js';
25
+
26
+ const DEFAULT_TOKEN_BUDGET = 12000; // ~12K tokens 留给历史, 其余给系统提示词和当前消息
27
+ const CHARS_PER_TOKEN = 3.5; // 近似: 中文 ~3.5 / 英文 ~4 / 取偏保守值
28
+ const MAX_CONVERSATIONS = 100; // 索引最多保留 100 个对话
29
+ const SUMMARY_TARGET_TOKENS = 500; // 压缩后的摘要目标 token 数
30
+
31
+ export class ConversationStore {
32
+ #dir;
33
+ #indexPath;
34
+ #logger;
35
+
36
+ /**
37
+ * @param {string} projectRoot — 用户项目根目录
38
+ */
39
+ constructor(projectRoot) {
40
+ this.#dir = path.join(projectRoot, '.autosnippet', 'conversations');
41
+ this.#indexPath = path.join(this.#dir, 'index.json');
42
+ this.#logger = Logger.getInstance();
43
+ }
44
+
45
+ // ═══════════════════════════════════════════════════════
46
+ // 公共 API
47
+ // ═══════════════════════════════════════════════════════
48
+
49
+ /**
50
+ * 创建新对话
51
+ * @param {object} opts
52
+ * @param {'user'|'system'} opts.category — 对话类别
53
+ * @param {string} [opts.title] — 对话标题
54
+ * @returns {string} conversationId
55
+ */
56
+ create({ category = 'user', title = '' } = {}) {
57
+ const id = crypto.randomUUID();
58
+ const entry = {
59
+ id,
60
+ category,
61
+ title,
62
+ createdAt: new Date().toISOString(),
63
+ updatedAt: new Date().toISOString(),
64
+ messageCount: 0,
65
+ hasSummary: false,
66
+ };
67
+
68
+ const index = this.#loadIndex();
69
+ index.unshift(entry);
70
+
71
+ // 限制索引大小
72
+ if (index.length > MAX_CONVERSATIONS) {
73
+ const removed = index.splice(MAX_CONVERSATIONS);
74
+ // 清理旧对话文件
75
+ for (const old of removed) {
76
+ this.#deleteConversationFile(old.id);
77
+ }
78
+ }
79
+
80
+ this.#saveIndex(index);
81
+ return id;
82
+ }
83
+
84
+ /**
85
+ * 追加消息到对话
86
+ * @param {string} conversationId
87
+ * @param {{ role: string, content: string }} message
88
+ */
89
+ append(conversationId, message) {
90
+ try {
91
+ fs.mkdirSync(this.#dir, { recursive: true });
92
+ const filePath = this.#conversationPath(conversationId);
93
+ const line = JSON.stringify({
94
+ role: message.role,
95
+ content: message.content,
96
+ ts: new Date().toISOString(),
97
+ });
98
+ fs.appendFileSync(filePath, line + '\n', 'utf-8');
99
+
100
+ // 更新索引
101
+ const index = this.#loadIndex();
102
+ const entry = index.find(e => e.id === conversationId);
103
+ if (entry) {
104
+ entry.updatedAt = new Date().toISOString();
105
+ entry.messageCount = (entry.messageCount || 0) + 1;
106
+ // 用首条用户消息作为标题
107
+ if (!entry.title && message.role === 'user') {
108
+ entry.title = message.content.substring(0, 60);
109
+ }
110
+ this.#saveIndex(index);
111
+ }
112
+ } catch (err) {
113
+ this.#logger.warn(`[ConversationStore] append failed: ${err.message}`);
114
+ }
115
+ }
116
+
117
+ /**
118
+ * 加载对话历史(带 token 预算控制)
119
+ *
120
+ * 如果历史超出 tokenBudget:
121
+ * - 保留开头的摘要(如有)
122
+ * - 截断中间的旧消息
123
+ * - 保留最新的消息
124
+ *
125
+ * @param {string} conversationId
126
+ * @param {object} [opts]
127
+ * @param {number} [opts.tokenBudget] — token 预算
128
+ * @returns {{ role: string, content: string }[]}
129
+ */
130
+ load(conversationId, { tokenBudget = DEFAULT_TOKEN_BUDGET } = {}) {
131
+ try {
132
+ const filePath = this.#conversationPath(conversationId);
133
+ if (!fs.existsSync(filePath)) return [];
134
+
135
+ const raw = fs.readFileSync(filePath, 'utf-8').trim();
136
+ if (!raw) return [];
137
+
138
+ const messages = raw.split('\n')
139
+ .filter(Boolean)
140
+ .map(line => {
141
+ try {
142
+ const parsed = JSON.parse(line);
143
+ return { role: parsed.role, content: parsed.content };
144
+ } catch { return null; }
145
+ })
146
+ .filter(Boolean);
147
+
148
+ return this.#fitWithinBudget(messages, tokenBudget);
149
+ } catch {
150
+ return [];
151
+ }
152
+ }
153
+
154
+ /**
155
+ * 对话列表
156
+ * @param {object} [opts]
157
+ * @param {'user'|'system'} [opts.category] — 按类别过滤
158
+ * @param {number} [opts.limit=20]
159
+ * @returns {Array}
160
+ */
161
+ list({ category, limit = 20 } = {}) {
162
+ const index = this.#loadIndex();
163
+ let results = index;
164
+ if (category) {
165
+ results = results.filter(e => e.category === category);
166
+ }
167
+ return results.slice(0, limit);
168
+ }
169
+
170
+ /**
171
+ * 删除对话
172
+ * @param {string} conversationId
173
+ */
174
+ delete(conversationId) {
175
+ this.#deleteConversationFile(conversationId);
176
+ const index = this.#loadIndex();
177
+ const filtered = index.filter(e => e.id !== conversationId);
178
+ this.#saveIndex(filtered);
179
+ }
180
+
181
+ /**
182
+ * 为对话生成压缩摘要(需要 AI)
183
+ * 将旧消息替换为一条 system 摘要消息
184
+ *
185
+ * @param {string} conversationId
186
+ * @param {object} opts
187
+ * @param {object} opts.aiProvider — AI Provider 实例
188
+ * @returns {Promise<boolean>} 是否成功压缩
189
+ */
190
+ async summarize(conversationId, { aiProvider }) {
191
+ if (!aiProvider) return false;
192
+
193
+ try {
194
+ const filePath = this.#conversationPath(conversationId);
195
+ if (!fs.existsSync(filePath)) return false;
196
+
197
+ const raw = fs.readFileSync(filePath, 'utf-8').trim();
198
+ if (!raw) return false;
199
+
200
+ const messages = raw.split('\n')
201
+ .filter(Boolean)
202
+ .map(line => { try { return JSON.parse(line); } catch { return null; } })
203
+ .filter(Boolean);
204
+
205
+ if (messages.length < 6) return false; // 太短不需要压缩
206
+
207
+ // 保留最近 4 条消息,压缩其余
208
+ const toSummarize = messages.slice(0, -4);
209
+ const toKeep = messages.slice(-4);
210
+
211
+ const summaryPrompt = `请用 2-3 句话总结以下对话的要点(保留关键决策、用户偏好、操作结果):\n\n${
212
+ toSummarize.map(m => `[${m.role}] ${m.content}`).join('\n').substring(0, 4000)
213
+ }`;
214
+
215
+ const summary = await aiProvider.chat(summaryPrompt, {
216
+ temperature: 0.3,
217
+ maxTokens: 300,
218
+ });
219
+
220
+ if (!summary) return false;
221
+
222
+ // 重写对话文件: 摘要 + 最近消息
223
+ const newMessages = [
224
+ { role: 'system', content: `[对话摘要] ${summary.trim()}`, ts: new Date().toISOString() },
225
+ ...toKeep,
226
+ ];
227
+
228
+ fs.writeFileSync(
229
+ filePath,
230
+ newMessages.map(m => JSON.stringify(m)).join('\n') + '\n',
231
+ 'utf-8',
232
+ );
233
+
234
+ // 更新索引
235
+ const index = this.#loadIndex();
236
+ const entry = index.find(e => e.id === conversationId);
237
+ if (entry) {
238
+ entry.hasSummary = true;
239
+ entry.messageCount = newMessages.length;
240
+ this.#saveIndex(index);
241
+ }
242
+
243
+ this.#logger.info(`[ConversationStore] summarized conversation ${conversationId}: ${messages.length} → ${newMessages.length} messages`);
244
+ return true;
245
+ } catch (err) {
246
+ this.#logger.warn(`[ConversationStore] summarize failed: ${err.message}`);
247
+ return false;
248
+ }
249
+ }
250
+
251
+ /**
252
+ * 清理过期对话
253
+ * @param {object} [opts]
254
+ * @param {number} [opts.maxAgeDays=30] — 超过此天数的对话将被删除
255
+ * @param {'user'|'system'} [opts.category] — 只清理特定类别
256
+ * @returns {{ deleted: number }}
257
+ */
258
+ cleanup({ maxAgeDays = 30, category } = {}) {
259
+ const index = this.#loadIndex();
260
+ const cutoff = Date.now() - maxAgeDays * 86400000;
261
+ let deleted = 0;
262
+
263
+ const kept = index.filter(entry => {
264
+ if (category && entry.category !== category) return true;
265
+ const updatedAt = new Date(entry.updatedAt).getTime();
266
+ if (updatedAt < cutoff) {
267
+ this.#deleteConversationFile(entry.id);
268
+ deleted++;
269
+ return false;
270
+ }
271
+ return true;
272
+ });
273
+
274
+ if (deleted > 0) {
275
+ this.#saveIndex(kept);
276
+ this.#logger.info(`[ConversationStore] cleaned up ${deleted} old conversations`);
277
+ }
278
+
279
+ return { deleted };
280
+ }
281
+
282
+ /**
283
+ * 估算 token 数
284
+ * @param {string} text
285
+ * @returns {number}
286
+ */
287
+ estimateTokens(text) {
288
+ if (!text) return 0;
289
+ return Math.ceil(text.length / CHARS_PER_TOKEN);
290
+ }
291
+
292
+ // ═══════════════════════════════════════════════════════
293
+ // 内部方法
294
+ // ═══════════════════════════════════════════════════════
295
+
296
+ /**
297
+ * 将消息列表裁剪到 token 预算内
298
+ * 策略: 保留首条摘要(如有) + 最新消息,丢弃中间旧消息
299
+ */
300
+ #fitWithinBudget(messages, tokenBudget) {
301
+ if (messages.length === 0) return [];
302
+
303
+ // 计算总 token
304
+ let totalTokens = 0;
305
+ const tokenCounts = messages.map(m => {
306
+ const tokens = this.estimateTokens(m.content);
307
+ totalTokens += tokens;
308
+ return tokens;
309
+ });
310
+
311
+ if (totalTokens <= tokenBudget) return messages;
312
+
313
+ // 超预算 — 保留首条(摘要) + 从末尾往前取
314
+ const result = [];
315
+ let used = 0;
316
+
317
+ // 如果首条是 system 摘要,优先保留
318
+ if (messages[0].role === 'system' && messages[0].content.startsWith('[对话摘要]')) {
319
+ result.push(messages[0]);
320
+ used += tokenCounts[0];
321
+ }
322
+
323
+ // 从末尾往前填充
324
+ const tail = [];
325
+ for (let i = messages.length - 1; i >= (result.length > 0 ? 1 : 0); i--) {
326
+ if (used + tokenCounts[i] > tokenBudget) break;
327
+ tail.unshift(messages[i]);
328
+ used += tokenCounts[i];
329
+ }
330
+
331
+ // 如果丢弃了消息,插入提示
332
+ const keptFromStart = result.length;
333
+ const keptFromEnd = tail.length;
334
+ const dropped = messages.length - keptFromStart - keptFromEnd;
335
+
336
+ if (dropped > 0) {
337
+ result.push({
338
+ role: 'system',
339
+ content: `[上下文截断] 省略了 ${dropped} 条较早的消息以适应上下文窗口。`,
340
+ });
341
+ }
342
+
343
+ result.push(...tail);
344
+ return result;
345
+ }
346
+
347
+ #conversationPath(id) {
348
+ return path.join(this.#dir, `${id}.jsonl`);
349
+ }
350
+
351
+ #deleteConversationFile(id) {
352
+ try {
353
+ const filePath = this.#conversationPath(id);
354
+ if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
355
+ } catch { /* ignore */ }
356
+ }
357
+
358
+ #loadIndex() {
359
+ try {
360
+ if (fs.existsSync(this.#indexPath)) {
361
+ return JSON.parse(fs.readFileSync(this.#indexPath, 'utf-8'));
362
+ }
363
+ } catch { /* corrupt — reset */ }
364
+ return [];
365
+ }
366
+
367
+ #saveIndex(index) {
368
+ try {
369
+ fs.mkdirSync(this.#dir, { recursive: true });
370
+ fs.writeFileSync(this.#indexPath, JSON.stringify(index, null, 2), 'utf-8');
371
+ } catch (err) {
372
+ this.#logger.warn(`[ConversationStore] index save failed: ${err.message}`);
373
+ }
374
+ }
375
+ }
376
+
377
+ export default ConversationStore;
@@ -1,17 +1,24 @@
1
1
  /**
2
- * Memory — 跨对话轻量记忆
2
+ * Memory — 跨对话轻量记忆(带 source 隔离 + 去重)
3
3
  *
4
4
  * 设计:
5
5
  * - JSONL 文件存储,每行一条记忆
6
6
  * - 支持 TTL 自动过期
7
7
  * - 上限 maxEntries,超出时截断旧条目
8
8
  * - 读写均做静默降级(Memory 是增强,不是核心路径)
9
+ * - source 标签: 'user'(用户对话) / 'system'(SignalCollector 等后台)
10
+ * - 去重: 相同 type+content 的记忆不重复写入
9
11
  *
10
12
  * 记忆类型:
11
13
  * - preference: 用户偏好("我们不用 singleton"、"以后用 DI")
12
14
  * - decision: 关键决策("Network 模块审核通过")
13
15
  * - context: 项目上下文("主语言是 Swift,使用 SPM")
14
16
  *
17
+ * 隔离规则:
18
+ * toPromptSection({ source }) 可按 source 过滤
19
+ * 用户对话只看 source=user 的记忆
20
+ * 系统分析可看全部记忆
21
+ *
15
22
  * 文件路径: .autosnippet/memory.jsonl
16
23
  */
17
24
 
@@ -35,9 +42,11 @@ export class Memory {
35
42
  /**
36
43
  * 读取最近 N 条记忆,过滤过期项
37
44
  * @param {number} [limit=20]
38
- * @returns {{ ts: string, type: string, content: string, ttl?: number }[]}
45
+ * @param {object} [opts]
46
+ * @param {'user'|'system'} [opts.source] — 按 source 过滤
47
+ * @returns {{ ts: string, type: string, content: string, source?: string, ttl?: number }[]}
39
48
  */
40
- load(limit = 20) {
49
+ load(limit = 20, { source } = {}) {
41
50
  try {
42
51
  if (!fs.existsSync(this.#filePath)) return [];
43
52
  const raw = fs.readFileSync(this.#filePath, 'utf-8').trim();
@@ -48,6 +57,7 @@ export class Memory {
48
57
  .map(l => { try { return JSON.parse(l); } catch { return null; } })
49
58
  .filter(Boolean)
50
59
  .filter(m => !m.ttl || (now - new Date(m.ts).getTime()) < m.ttl * 86400000)
60
+ .filter(m => !source || (m.source || 'user') === source)
51
61
  .slice(-limit);
52
62
  } catch {
53
63
  return [];
@@ -55,14 +65,27 @@ export class Memory {
55
65
  }
56
66
 
57
67
  /**
58
- * 追加一条记忆
59
- * @param {{ type: string, content: string, ttl?: number }} entry
68
+ * 追加一条记忆(自动去重)
69
+ * @param {{ type: string, content: string, source?: string, ttl?: number }} entry
60
70
  */
61
71
  append(entry) {
62
72
  try {
73
+ // 去重: 检查是否已有相同 type+content 的记忆
74
+ const existing = this.load(this.#maxEntries);
75
+ const normalizedContent = (entry.content || '').trim().substring(0, 200);
76
+ const isDuplicate = existing.some(
77
+ m => m.type === entry.type && m.content === normalizedContent,
78
+ );
79
+ if (isDuplicate) return;
80
+
63
81
  const dir = path.dirname(this.#filePath);
64
82
  fs.mkdirSync(dir, { recursive: true });
65
- const line = JSON.stringify({ ts: new Date().toISOString(), ...entry });
83
+ const line = JSON.stringify({
84
+ ts: new Date().toISOString(),
85
+ source: entry.source || 'user',
86
+ ...entry,
87
+ content: normalizedContent,
88
+ });
66
89
  fs.appendFileSync(this.#filePath, line + '\n', 'utf-8');
67
90
  this.#compact();
68
91
  } catch { /* write failure non-critical */ }
@@ -70,10 +93,15 @@ export class Memory {
70
93
 
71
94
  /**
72
95
  * 生成供系统提示词的记忆摘要
96
+ *
97
+ * @param {object} [opts]
98
+ * @param {'user'|'system'} [opts.source] — 只包含指定 source 的记忆
99
+ * - 用户对话建议传 'user' 避免系统分析记忆污染
100
+ * - 后台分析可传 undefined 获取全部
73
101
  * @returns {string}
74
102
  */
75
- toPromptSection() {
76
- const memories = this.load();
103
+ toPromptSection({ source } = {}) {
104
+ const memories = this.load(20, { source });
77
105
  if (memories.length === 0) return '';
78
106
  const lines = memories.map(m => `- [${m.type}] ${m.content}`).join('\n');
79
107
  return `\n## 历史记忆\n以下是之前对话中积累的项目偏好和决策,请参考:\n${lines}\n`;
@@ -81,9 +109,11 @@ export class Memory {
81
109
 
82
110
  /**
83
111
  * 当前记忆条数
112
+ * @param {object} [opts]
113
+ * @param {'user'|'system'} [opts.source]
84
114
  */
85
- get size() {
86
- return this.load(this.#maxEntries).length;
115
+ size({ source } = {}) {
116
+ return this.load(this.#maxEntries, { source }).length;
87
117
  }
88
118
 
89
119
  /**
@@ -49,14 +49,16 @@
49
49
  * │ 30. rebuild_index 向量索引重建 │
50
50
  * │ 31. query_audit_log 审计日志查询 │
51
51
  * └─────────────────────────────────────────────────────┘
52
- * ┌─── Skills & Bootstrap (2) ─────────────────────────┐
52
+ * ┌─── Skills & Bootstrap (4) ─────────────────────────┐
53
53
  * │ 32. load_skill 加载 Agent Skill 文档 │
54
- * │ 33. bootstrap_knowledge 冷启动知识库初始化
54
+ * │ 33. create_skill 创建项目级 Skill
55
+ * │ 34. suggest_skills 推荐创建 Skill │
56
+ * │ 35. bootstrap_knowledge 冷启动知识库初始化 │
55
57
  * └─────────────────────────────────────────────────────┘
56
58
  * ┌─── 组合工具 (3) ───────────────────────────────────┐
57
- * │ 34. analyze_code Guard + Recipe 搜索 │
58
- * │ 35. knowledge_overview 全局知识库概览 │
59
- * │ 36. submit_with_check 查重 + 提交 │
59
+ * │ 36. analyze_code Guard + Recipe 搜索 │
60
+ * │ 37. knowledge_overview 全局知识库概览 │
61
+ * │ 38. submit_with_check 查重 + 提交 │
60
62
  * └─────────────────────────────────────────────────────┘
61
63
  */
62
64
 
@@ -1167,7 +1169,52 @@ const loadSkill = {
1167
1169
  };
1168
1170
 
1169
1171
  // ────────────────────────────────────────────────────────────
1170
- // 33. bootstrap_knowledge冷启动知识库初始化
1172
+ // 33. create_skill创建项目级 Skill
1173
+ // ────────────────────────────────────────────────────────────
1174
+ const createSkillTool = {
1175
+ name: 'create_skill',
1176
+ description: '创建项目级 Skill 文档,写入 .autosnippet/skills/<name>/SKILL.md。Skill 是 Agent 的领域知识增强文档。创建后自动更新编辑器索引。',
1177
+ parameters: {
1178
+ type: 'object',
1179
+ properties: {
1180
+ name: { type: 'string', description: 'Skill 名称(kebab-case,如 my-auth-guide),3-64 字符' },
1181
+ description: { type: 'string', description: 'Skill 一句话描述(写入 frontmatter)' },
1182
+ content: { type: 'string', description: 'Skill 正文内容(Markdown 格式,不含 frontmatter)' },
1183
+ overwrite: { type: 'boolean', description: '如果同名 Skill 已存在,是否覆盖(默认 false)' },
1184
+ },
1185
+ required: ['name', 'description', 'content'],
1186
+ },
1187
+ handler: async (params, ctx) => {
1188
+ const { createSkill } = await import('../../external/mcp/handlers/skill.js');
1189
+ // 根据 ChatAgent 的 source 推断 createdBy
1190
+ const createdBy = ctx?.source === 'system' ? 'system-ai' : 'user-ai';
1191
+ const raw = createSkill(null, { ...params, createdBy });
1192
+ try { return JSON.parse(raw); } catch { return { success: false, error: raw }; }
1193
+ },
1194
+ };
1195
+
1196
+ // ────────────────────────────────────────────────────────────
1197
+ // 34. suggest_skills — 基于使用模式推荐 Skill 创建
1198
+ // ────────────────────────────────────────────────────────────
1199
+ const suggestSkills = {
1200
+ name: 'suggest_skills',
1201
+ description: '基于项目使用模式分析,推荐创建 Skill。分析 Guard 违规频率、Memory 偏好积累、Recipe 分布缺口、候选积压率。返回推荐列表(含 name/description/rationale/priority),可据此直接调用 create_skill 创建。',
1202
+ parameters: {
1203
+ type: 'object',
1204
+ properties: {},
1205
+ required: [],
1206
+ },
1207
+ handler: async (_params, ctx) => {
1208
+ const { SkillAdvisor } = await import('../../service/skills/SkillAdvisor.js');
1209
+ const database = ctx?.container?.get?.('database') || null;
1210
+ const projectRoot = ctx?.projectRoot || process.cwd();
1211
+ const advisor = new SkillAdvisor(projectRoot, { database });
1212
+ return advisor.suggest();
1213
+ },
1214
+ };
1215
+
1216
+ // ────────────────────────────────────────────────────────────
1217
+ // 34. bootstrap_knowledge — 冷启动知识库初始化
1171
1218
  // ────────────────────────────────────────────────────────────
1172
1219
  const bootstrapKnowledgeTool = {
1173
1220
  name: 'bootstrap_knowledge',
@@ -1396,6 +1443,52 @@ const submitWithCheck = {
1396
1443
  },
1397
1444
  };
1398
1445
 
1446
+ // ═══════════════════════════════════════════════════════
1447
+ // 元工具: Lazy Tool Schema 按需加载
1448
+ // ═══════════════════════════════════════════════════════
1449
+
1450
+ /**
1451
+ * get_tool_details — 查询工具的完整参数 schema
1452
+ *
1453
+ * 与 Cline .clinerules 按需加载类似:
1454
+ * System Prompt 只包含工具名+一行描述,LLM 需要调用某个工具前
1455
+ * 先通过此元工具获取完整参数定义,避免 prompt 过长浪费 token。
1456
+ */
1457
+ const getToolDetails = {
1458
+ name: 'get_tool_details',
1459
+ description: '查询指定工具的完整参数 Schema。在调用不熟悉的工具之前,先用此工具获取参数详情。',
1460
+ parameters: {
1461
+ type: 'object',
1462
+ properties: {
1463
+ toolName: {
1464
+ type: 'string',
1465
+ description: '要查询的工具名称(snake_case)',
1466
+ },
1467
+ },
1468
+ required: ['toolName'],
1469
+ },
1470
+ handler: async ({ toolName }, context) => {
1471
+ const registry = context.container?.get('toolRegistry');
1472
+ if (!registry) return { error: 'ToolRegistry not available' };
1473
+
1474
+ const schemas = registry.getToolSchemas();
1475
+ const found = schemas.find(t => t.name === toolName);
1476
+ if (!found) {
1477
+ const allNames = schemas.map(t => t.name);
1478
+ return {
1479
+ error: `Tool "${toolName}" not found`,
1480
+ availableTools: allNames,
1481
+ };
1482
+ }
1483
+
1484
+ return {
1485
+ name: found.name,
1486
+ description: found.description,
1487
+ parameters: found.parameters,
1488
+ };
1489
+ },
1490
+ };
1491
+
1399
1492
  export const ALL_TOOLS = [
1400
1493
  // 查询类 (8)
1401
1494
  searchRecipes,
@@ -1436,13 +1529,17 @@ export const ALL_TOOLS = [
1436
1529
  graphImpactAnalysis,
1437
1530
  rebuildIndex,
1438
1531
  queryAuditLog,
1439
- // Skills & Bootstrap (2)
1532
+ // Skills & Bootstrap (4)
1440
1533
  loadSkill,
1534
+ createSkillTool,
1535
+ suggestSkills,
1441
1536
  bootstrapKnowledgeTool,
1442
1537
  // 组合工具 (3) — 减少 ReAct 轮次
1443
1538
  analyzeCode,
1444
1539
  knowledgeOverview,
1445
1540
  submitWithCheck,
1541
+ // 元工具 (1) — Lazy Tool Schema 按需加载
1542
+ getToolDetails,
1446
1543
  ];
1447
1544
 
1448
1545
  export default ALL_TOOLS;