autosnippet 2.5.0 → 2.7.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 (72) hide show
  1. package/bin/cli.js +35 -0
  2. package/dashboard/dist/assets/{icons-Dtm0E6DS.js → icons-Cq4-iQhP.js} +152 -87
  3. package/dashboard/dist/assets/index-DBxH7pVn.css +1 -0
  4. package/dashboard/dist/assets/index-Dw2F6qAS.js +197 -0
  5. package/dashboard/dist/assets/{react-markdown-CWxUbOf4.js → react-markdown-BA6FB2NP.js} +1 -1
  6. package/dashboard/dist/assets/{syntax-highlighter-CJ2drQQb.js → syntax-highlighter-CVLHn9O5.js} +1 -1
  7. package/dashboard/dist/assets/{vendor-f83ah6cm.js → vendor-BotF760a.js} +61 -61
  8. package/dashboard/dist/index.html +6 -6
  9. package/lib/bootstrap.js +1 -1
  10. package/lib/cli/SetupService.js +33 -8
  11. package/lib/cli/UpgradeService.js +139 -2
  12. package/lib/core/ast/ProjectGraph.js +599 -0
  13. package/lib/core/gateway/Gateway.js +19 -4
  14. package/lib/core/gateway/GatewayActionRegistry.js +2 -2
  15. package/lib/domain/recipe/Recipe.js +3 -0
  16. package/lib/external/ai/AiProvider.js +117 -10
  17. package/lib/external/ai/providers/ClaudeProvider.js +197 -0
  18. package/lib/external/ai/providers/GoogleGeminiProvider.js +235 -1
  19. package/lib/external/ai/providers/OpenAiProvider.js +131 -0
  20. package/lib/external/mcp/McpServer.js +2 -1
  21. package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-context.js +216 -0
  22. package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +468 -0
  23. package/lib/external/mcp/handlers/bootstrap/pipeline/tier-scheduler.js +162 -0
  24. package/lib/external/mcp/handlers/bootstrap/skills.js +225 -0
  25. package/lib/external/mcp/handlers/bootstrap.js +151 -1634
  26. package/lib/external/mcp/handlers/browse.js +1 -1
  27. package/lib/external/mcp/handlers/candidate.js +1 -33
  28. package/lib/external/mcp/handlers/skill.js +126 -31
  29. package/lib/external/mcp/tools.js +25 -3
  30. package/lib/http/middleware/requestLogger.js +23 -4
  31. package/lib/http/routes/ai.js +3 -1
  32. package/lib/http/routes/auth.js +3 -2
  33. package/lib/http/routes/candidates.js +49 -25
  34. package/lib/http/routes/commands.js +0 -8
  35. package/lib/http/routes/guardRules.js +1 -16
  36. package/lib/http/routes/recipes.js +4 -17
  37. package/lib/http/routes/search.js +16 -22
  38. package/lib/http/routes/skills.js +40 -3
  39. package/lib/http/routes/snippets.js +0 -33
  40. package/lib/http/routes/spm.js +37 -63
  41. package/lib/http/utils/routeHelpers.js +31 -0
  42. package/lib/infrastructure/audit/AuditStore.js +18 -0
  43. package/lib/infrastructure/config/Paths.js +9 -0
  44. package/lib/infrastructure/logging/Logger.js +86 -3
  45. package/lib/infrastructure/realtime/RealtimeService.js +2 -5
  46. package/lib/infrastructure/vector/JsonVectorAdapter.js +24 -1
  47. package/lib/injection/ServiceContainer.js +62 -3
  48. package/lib/service/bootstrap/BootstrapTaskManager.js +400 -0
  49. package/lib/service/candidate/CandidateFileWriter.js +68 -27
  50. package/lib/service/candidate/CandidateService.js +156 -10
  51. package/lib/service/chat/AnalystAgent.js +216 -0
  52. package/lib/service/chat/CandidateGuardrail.js +134 -0
  53. package/lib/service/chat/ChatAgent.js +1272 -155
  54. package/lib/service/chat/ContextWindow.js +730 -0
  55. package/lib/service/chat/ConversationStore.js +377 -0
  56. package/lib/service/chat/HandoffProtocol.js +180 -0
  57. package/lib/service/chat/Memory.js +40 -10
  58. package/lib/service/chat/ProducerAgent.js +240 -0
  59. package/lib/service/chat/ToolRegistry.js +149 -5
  60. package/lib/service/chat/tools.js +1493 -60
  61. package/lib/service/recipe/RecipeFileWriter.js +12 -1
  62. package/lib/service/skills/EventAggregator.js +187 -0
  63. package/lib/service/skills/SignalCollector.js +549 -0
  64. package/lib/service/skills/SkillAdvisor.js +324 -0
  65. package/lib/service/skills/SkillHooks.js +13 -5
  66. package/lib/service/spm/SpmService.js +2 -2
  67. package/package.json +1 -1
  68. package/templates/copilot-instructions.md +20 -3
  69. package/templates/cursor-rules/autosnippet-conventions.mdc +21 -4
  70. package/templates/cursor-rules/autosnippet-skills.mdc +45 -0
  71. package/dashboard/dist/assets/index-B7VpZOCz.css +0 -1
  72. package/dashboard/dist/assets/index-D87IZTmZ.js +0 -187
@@ -15,7 +15,7 @@ export async function listByKind(ctx, kind, args) {
15
15
  const items = (result?.data || result?.items || []).map(r => ({
16
16
  id: r.id, title: r.title || r.name, description: r.description,
17
17
  trigger: r.trigger || '', status: r.status, language: r.language, category: r.category,
18
- knowledgeType: r.knowledgeType || r.knowledge_type, kind: r.kind,
18
+ knowledgeType: r.knowledgeType, kind: r.kind,
19
19
  complexity: r.complexity, scope: r.scope, tags: r.tags || [],
20
20
  quality: r.quality || null, statistics: r.statistics || null,
21
21
  }));
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * MCP Handlers — 候选提交 & 校验 & AI 补全
3
3
  * validateCandidate, checkDuplicate, submitSingle, submitBatch, submitDrafts, enrichCandidates
4
- * + 辅助: buildReasoning, buildCandidateMetadata, _createCandidateItem
4
+ * + 辅助: buildReasoning, _createCandidateItem
5
5
  */
6
6
 
7
7
  import fs from 'node:fs';
@@ -28,38 +28,6 @@ export function buildReasoning(obj) {
28
28
  };
29
29
  }
30
30
 
31
- export function buildCandidateMetadata(obj) {
32
- const m = {};
33
- // 标识 & 描述
34
- if (obj.title) m.title = obj.title;
35
- if (obj.description) m.description = obj.description;
36
- // 中英文摘要(summary / summary_cn / summary_en)
37
- if (obj.summary_cn || obj.summary) m.summary = obj.summary_cn || obj.summary;
38
- if (obj.summary_en) m.summary_en = obj.summary_en;
39
- if (obj.trigger) m.trigger = obj.trigger;
40
- // 中英文使用指南(usageGuide / usageGuide_cn / usageGuide_en)
41
- if (obj.usageGuide_cn || obj.usageGuide) m.usageGuide = obj.usageGuide_cn || obj.usageGuide;
42
- if (obj.usageGuide_en) m.usageGuide_en = obj.usageGuide_en;
43
- // 分类
44
- if (obj.knowledgeType) m.knowledgeType = obj.knowledgeType;
45
- if (obj.complexity) m.complexity = obj.complexity;
46
- if (obj.scope) m.scope = obj.scope;
47
- if (obj.tags) m.tags = obj.tags;
48
- // 结构化内容
49
- if (obj.rationale) m.rationale = obj.rationale;
50
- if (obj.steps) m.steps = obj.steps;
51
- if (obj.codeChanges) m.codeChanges = obj.codeChanges;
52
- if (obj.verification) m.verification = obj.verification;
53
- if (obj.headers) m.headers = obj.headers;
54
- // 约束 & 关系
55
- if (obj.constraints) m.constraints = obj.constraints;
56
- if (obj.relations) m.relations = obj.relations;
57
- // 质量 & 来源
58
- if (obj.quality) m.quality = obj.quality;
59
- if (obj.sourceFile) m.sourceFile = obj.sourceFile;
60
- return m;
61
- }
62
-
63
31
  /**
64
32
  * 统一创建候选的内部方法 — 委托到 CandidateService.createFromToolParams()
65
33
  * 保留此函数作为 MCP handler 层的快捷入口,保持向后兼容。
@@ -13,34 +13,55 @@
13
13
  import fs from 'node:fs';
14
14
  import path from 'node:path';
15
15
  import { fileURLToPath } from 'node:url';
16
+ import { getProjectSkillsPath } from '../../../infrastructure/config/Paths.js';
16
17
 
17
18
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
- const PROJECT_ROOT = path.resolve(__dirname, '../../../..');
19
- const SKILLS_DIR = path.resolve(PROJECT_ROOT, 'skills');
20
- const PROJECT_SKILLS_DIR = path.resolve(PROJECT_ROOT, '.autosnippet', 'skills');
19
+ const SKILLS_DIR = path.resolve(__dirname, '../../../../skills');
21
20
 
22
21
  /**
23
- * Skill 名称 → 摘要描述映射(用于 list_skills 返回)
22
+ * 获取用户项目根目录(运行时动态解析)
23
+ */
24
+ function _getProjectRoot() {
25
+ return process.env.ASD_PROJECT_DIR || process.cwd();
26
+ }
27
+
28
+ /**
29
+ * 获取项目级 Skills 目录(运行时动态解析)
30
+ * 路径: {projectRoot}/AutoSnippet/skills/ — 跟随项目走
31
+ */
32
+ function _getProjectSkillsDir() {
33
+ return getProjectSkillsPath(_getProjectRoot());
34
+ }
35
+
36
+ /**
37
+ * 解析 SKILL.md frontmatter 全部元数据
24
38
  *
25
- * SKILL.md frontmatter description 提取。
26
- * 如果解析失败,返回 Skill 名称本身。
39
+ * 返回 { description, createdBy, createdAt },缺失字段为 null。
40
+ * 同时兼容旧格式(无 createdBy 的 SKILL.md)。
27
41
  */
28
- function _parseSkillSummary(skillName, baseDir = SKILLS_DIR) {
42
+ function _parseSkillMeta(skillName, baseDir = SKILLS_DIR) {
29
43
  try {
30
44
  const content = fs.readFileSync(
31
45
  path.join(baseDir, skillName, 'SKILL.md'), 'utf8',
32
46
  );
33
- // 提取 frontmatter 的 description 字段
34
- const descMatch = content.match(/^description:\s*(.+?)(?:\n|$)/m);
35
- if (descMatch) {
36
- // 截断到第一句或 120 字符
37
- const desc = descMatch[1].trim();
38
- const firstSentence = desc.split(/\.\s/)[0];
39
- return firstSentence.length < desc.length ? `${firstSentence}.` : desc.substring(0, 120);
47
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
48
+ const meta = { description: skillName, createdBy: null, createdAt: null };
49
+ if (fmMatch) {
50
+ const fm = fmMatch[1];
51
+ const descMatch = fm.match(/^description:\s*(.+?)$/m);
52
+ if (descMatch) {
53
+ const desc = descMatch[1].trim();
54
+ const firstSentence = desc.split(/\.\s/)[0];
55
+ meta.description = firstSentence.length < desc.length ? `${firstSentence}.` : desc.substring(0, 120);
56
+ }
57
+ const cbMatch = fm.match(/^createdBy:\s*(.+?)$/m);
58
+ if (cbMatch) meta.createdBy = cbMatch[1].trim();
59
+ const caMatch = fm.match(/^createdAt:\s*(.+?)$/m);
60
+ if (caMatch) meta.createdAt = caMatch[1].trim();
40
61
  }
41
- return skillName;
62
+ return meta;
42
63
  } catch {
43
- return skillName;
64
+ return { description: skillName, createdBy: null, createdAt: null };
44
65
  }
45
66
  }
46
67
 
@@ -80,26 +101,39 @@ export function listSkills() {
80
101
  const builtinDirs = fs.readdirSync(SKILLS_DIR, { withFileTypes: true })
81
102
  .filter(d => d.isDirectory()).map(d => d.name);
82
103
  for (const name of builtinDirs) {
83
- skillMap.set(name, { name, source: 'builtin', summary: _parseSkillSummary(name, SKILLS_DIR), useCase: SKILL_USE_CASES[name] || null });
104
+ const meta = _parseSkillMeta(name, SKILLS_DIR);
105
+ skillMap.set(name, { name, source: 'builtin', summary: meta.description, createdBy: null, createdAt: null, useCase: SKILL_USE_CASES[name] || null });
84
106
  }
85
107
 
86
108
  // 项目级 Skills(覆盖同名内置)
87
109
  try {
88
- const projectDirs = fs.readdirSync(PROJECT_SKILLS_DIR, { withFileTypes: true })
110
+ const projectSkillsDir = _getProjectSkillsDir();
111
+ const projectDirs = fs.readdirSync(projectSkillsDir, { withFileTypes: true })
89
112
  .filter(d => d.isDirectory()).map(d => d.name);
90
113
  for (const name of projectDirs) {
91
- skillMap.set(name, { name, source: 'project', summary: _parseSkillSummary(name, PROJECT_SKILLS_DIR), useCase: SKILL_USE_CASES[name] || null });
114
+ const meta = _parseSkillMeta(name, projectSkillsDir);
115
+ skillMap.set(name, { name, source: 'project', summary: meta.description, createdBy: meta.createdBy, createdAt: meta.createdAt, useCase: SKILL_USE_CASES[name] || null });
92
116
  }
93
117
  } catch { /* no project skills */ }
94
118
 
95
119
  const skills = [...skillMap.values()].sort((a, b) => a.name.localeCompare(b.name));
96
120
 
121
+ // _meta:附带 SignalCollector 推荐计数(如果后台服务可用)
122
+ let suggestionCount = 0;
123
+ try {
124
+ if (global._signalCollector) {
125
+ const snapshot = global._signalCollector.getSnapshot();
126
+ suggestionCount = snapshot?.lastResult?.newSuggestions || 0;
127
+ }
128
+ } catch { /* silent */ }
129
+
97
130
  return JSON.stringify({
98
131
  success: true,
99
132
  data: {
100
133
  skills,
101
134
  total: skills.length,
102
135
  hint: '根据当前任务选择合适的 Skill 加载(load_skill)。不确定时先加载 autosnippet-intent 做意图路由。',
136
+ _meta: { signalSuggestions: suggestionCount },
103
137
  },
104
138
  });
105
139
  } catch (err) {
@@ -132,7 +166,8 @@ export function loadSkill(_ctx, args) {
132
166
  }
133
167
 
134
168
  // 项目级 Skills 优先
135
- const projectSkillPath = path.join(PROJECT_SKILLS_DIR, skillName, 'SKILL.md');
169
+ const projectSkillsDir = _getProjectSkillsDir();
170
+ const projectSkillPath = path.join(projectSkillsDir, skillName, 'SKILL.md');
136
171
  const builtinSkillPath = path.join(SKILLS_DIR, skillName, 'SKILL.md');
137
172
  const skillPath = fs.existsSync(projectSkillPath) ? projectSkillPath : builtinSkillPath;
138
173
  const source = skillPath === projectSkillPath ? 'project' : 'builtin';
@@ -152,6 +187,9 @@ export function loadSkill(_ctx, args) {
152
187
  }
153
188
  }
154
189
 
190
+ // 提取 createdBy/createdAt
191
+ const meta = _parseSkillMeta(skillName, source === 'project' ? projectSkillsDir : SKILLS_DIR);
192
+
155
193
  return JSON.stringify({
156
194
  success: true,
157
195
  data: {
@@ -159,6 +197,8 @@ export function loadSkill(_ctx, args) {
159
197
  source,
160
198
  content,
161
199
  charCount: content.length,
200
+ createdBy: source === 'project' ? meta.createdBy : null,
201
+ createdAt: source === 'project' ? meta.createdAt : null,
162
202
  useCase: SKILL_USE_CASES[skillName] || null,
163
203
  relatedSkills: _getRelatedSkills(skillName),
164
204
  },
@@ -167,7 +207,7 @@ export function loadSkill(_ctx, args) {
167
207
  // 列出所有可用 Skills
168
208
  const available = new Set();
169
209
  try { fs.readdirSync(SKILLS_DIR, { withFileTypes: true }).filter(d => d.isDirectory()).forEach(d => available.add(d.name)); } catch {}
170
- try { fs.readdirSync(PROJECT_SKILLS_DIR, { withFileTypes: true }).filter(d => d.isDirectory()).forEach(d => available.add(d.name)); } catch {}
210
+ try { fs.readdirSync(_getProjectSkillsDir(), { withFileTypes: true }).filter(d => d.isDirectory()).forEach(d => available.add(d.name)); } catch {}
171
211
 
172
212
  return JSON.stringify({
173
213
  success: false,
@@ -185,7 +225,7 @@ export function loadSkill(_ctx, args) {
185
225
  // ═══════════════════════════════════════════════════════════
186
226
 
187
227
  /**
188
- * 创建项目级 Skill — 写入 .autosnippet/skills/<name>/SKILL.md
228
+ * 创建项目级 Skill — 写入 {projectRoot}/AutoSnippet/skills/<name>/SKILL.md
189
229
  * 创建后自动 regenerate 编辑器索引(.cursor/rules/autosnippet-skills.mdc)
190
230
  *
191
231
  * @param {object} _ctx MCP context
@@ -193,7 +233,7 @@ export function loadSkill(_ctx, args) {
193
233
  * @returns {string} JSON envelope
194
234
  */
195
235
  export function createSkill(_ctx, args) {
196
- const { name, description, content, overwrite = false } = args || {};
236
+ const { name, description, content, overwrite = false, createdBy = 'external-ai', title } = args || {};
197
237
 
198
238
  // ── 参数校验 ──
199
239
  if (!name || !description || !content) {
@@ -227,7 +267,8 @@ export function createSkill(_ctx, args) {
227
267
  }
228
268
 
229
269
  // 检查同名项目级 Skill
230
- const skillDir = path.join(PROJECT_SKILLS_DIR, name);
270
+ const projectSkillsDir = _getProjectSkillsDir();
271
+ const skillDir = path.join(projectSkillsDir, name);
231
272
  const skillPath = path.join(skillDir, 'SKILL.md');
232
273
  if (fs.existsSync(skillPath) && !overwrite) {
233
274
  return JSON.stringify({
@@ -243,13 +284,25 @@ export function createSkill(_ctx, args) {
243
284
  try {
244
285
  fs.mkdirSync(skillDir, { recursive: true });
245
286
 
246
- const frontmatter = [
287
+ // 自动推断 title: 优先使用传入参数,否则从 content 的第一个 # heading 提取
288
+ const resolvedTitle = title || (() => {
289
+ const m = (content || '').match(/^#\s+(.+)/m);
290
+ return m ? m[1].trim() : '';
291
+ })();
292
+
293
+ const fmLines = [
247
294
  '---',
248
295
  `name: ${name}`,
296
+ ];
297
+ if (resolvedTitle) fmLines.push(`title: "${resolvedTitle.replace(/"/g, '\\"')}"`);
298
+ fmLines.push(
249
299
  `description: ${description}`,
300
+ `createdBy: ${createdBy}`,
301
+ `createdAt: ${new Date().toISOString()}`,
250
302
  '---',
251
303
  '',
252
- ].join('\n');
304
+ );
305
+ const frontmatter = fmLines.join('\n');
253
306
 
254
307
  fs.writeFileSync(skillPath, frontmatter + content, 'utf8');
255
308
  } catch (err) {
@@ -262,6 +315,13 @@ export function createSkill(_ctx, args) {
262
315
  // ── regenerate 编辑器索引 ──
263
316
  const indexResult = _regenerateEditorIndex();
264
317
 
318
+ // ── 清理 SignalCollector 已创建的 pendingSuggestions ──
319
+ try {
320
+ if (global._signalCollector) {
321
+ global._signalCollector.removePendingSuggestion(name);
322
+ }
323
+ } catch { /* silent */ }
324
+
265
325
  return JSON.stringify({
266
326
  success: true,
267
327
  data: {
@@ -284,19 +344,23 @@ function _regenerateEditorIndex() {
284
344
  try {
285
345
  // 扫描项目级 Skills
286
346
  let projectSkills = [];
347
+ const projectSkillsDir = _getProjectSkillsDir();
287
348
  try {
288
- const dirs = fs.readdirSync(PROJECT_SKILLS_DIR, { withFileTypes: true })
349
+ const dirs = fs.readdirSync(projectSkillsDir, { withFileTypes: true })
289
350
  .filter(d => d.isDirectory())
290
351
  .map(d => d.name);
291
352
  for (const name of dirs) {
292
- const summary = _parseSkillSummary(name, PROJECT_SKILLS_DIR);
293
- projectSkills.push({ name, summary });
353
+ const meta = _parseSkillMeta(name, projectSkillsDir);
354
+ projectSkills.push({ name, summary: meta.description });
294
355
  }
295
356
  } catch { /* no project skills dir */ }
296
357
 
358
+ const projectRoot = _getProjectRoot();
359
+ const rulesDir = path.join(projectRoot, '.cursor', 'rules');
360
+
297
361
  if (projectSkills.length === 0) {
298
362
  // 没有项目级 Skills 时,删除索引文件(如果存在)
299
- const indexPath = path.join(PROJECT_ROOT, '.cursor', 'rules', 'autosnippet-skills.mdc');
363
+ const indexPath = path.join(rulesDir, 'autosnippet-skills.mdc');
300
364
  try { fs.unlinkSync(indexPath); } catch { /* not exists */ }
301
365
  return { success: true, skillCount: 0 };
302
366
  }
@@ -321,7 +385,6 @@ function _regenerateEditorIndex() {
321
385
  ].join('\n');
322
386
 
323
387
  // 写入 .cursor/rules/
324
- const rulesDir = path.join(PROJECT_ROOT, '.cursor', 'rules');
325
388
  fs.mkdirSync(rulesDir, { recursive: true });
326
389
  const indexPath = path.join(rulesDir, 'autosnippet-skills.mdc');
327
390
  fs.writeFileSync(indexPath, mdcContent, 'utf8');
@@ -332,6 +395,38 @@ function _regenerateEditorIndex() {
332
395
  }
333
396
  }
334
397
 
398
+ // ═══════════════════════════════════════════════════════════
399
+ // Handler: suggestSkills
400
+ // ═══════════════════════════════════════════════════════════
401
+
402
+ /**
403
+ * 基于项目使用模式分析,推荐创建 Skill
404
+ *
405
+ * 分析维度:Guard 违规模式、Memory 偏好积累、Recipe 分布缺口、候选积压
406
+ * Agent 可根据推荐结果自行决定是否调用 createSkill 创建
407
+ *
408
+ * @param {object} ctx MCP context(含 container)
409
+ * @returns {string} JSON envelope
410
+ */
411
+ export async function suggestSkills(ctx) {
412
+ try {
413
+ const { SkillAdvisor } = await import('../../../service/skills/SkillAdvisor.js');
414
+ const database = ctx?.container?.get?.('database') || null;
415
+ const advisor = new SkillAdvisor(PROJECT_ROOT, { database });
416
+ const result = advisor.suggest();
417
+
418
+ return JSON.stringify({
419
+ success: true,
420
+ data: result,
421
+ });
422
+ } catch (err) {
423
+ return JSON.stringify({
424
+ success: false,
425
+ error: { code: 'SUGGEST_ERROR', message: err.message },
426
+ });
427
+ }
428
+ }
429
+
335
430
  /**
336
431
  * 推荐相关 Skills(基于静态映射)
337
432
  */
@@ -558,13 +558,14 @@ export const TOOLS = [
558
558
  required: ['candidateIds'],
559
559
  },
560
560
  },
561
- // 31. 冷启动知识库初始化(自动创建 9 维度 Candidate)
561
+ // 31. 冷启动知识库初始化(自动创建 9 维度 Candidate + 4 个 Project Skills
562
562
  {
563
563
  name: 'autosnippet_bootstrap_knowledge',
564
564
  description:
565
565
  '项目冷启动:一键初始化知识库(纯启发式,不使用 AI)。覆盖 9 大知识维度。\n' +
566
566
  '自动为每个维度创建 N 条 Candidate(PENDING 状态),基于启发式规则从扫描文件中提取代表性代码。\n' +
567
- '返回 filesByTarget、dependencyGraph、bootstrapCandidates、analysisFramework。\n' +
567
+ 'Phase 5.5 自动为 4 个宏观维度(code-standard, architecture, project-profile, agent-guidelines)生成 Project Skills,写入 AutoSnippet/skills/。\n' +
568
+ '返回 filesByTarget、dependencyGraph、bootstrapCandidates、projectSkills、analysisFramework。\n' +
568
569
  '\n' +
569
570
  '💡 建议:调用前先加载 autosnippet-coldstart Skill(autosnippet_load_skill),获取完整的 9 维度分析指南和最佳实践。\n' +
570
571
  '\n' +
@@ -628,7 +629,7 @@ export const TOOLS = [
628
629
  {
629
630
  name: 'autosnippet_create_skill',
630
631
  description:
631
- '创建一个项目级 Skill 文档,写入 .autosnippet/skills/<name>/SKILL.md。\n' +
632
+ '创建一个项目级 Skill 文档,写入 AutoSnippet/skills/<name>/SKILL.md。\n' +
632
633
  'Skill 是 Agent 的领域知识增强文档,帮助 Agent 正确执行特定任务。\n' +
633
634
  '创建后自动更新编辑器索引(.cursor/rules/autosnippet-skills.mdc),使 Skill 被 AI Agent 被动发现。\n' +
634
635
  '\n' +
@@ -658,10 +659,31 @@ export const TOOLS = [
658
659
  default: false,
659
660
  description: '如果同名 Skill 已存在,是否覆盖(默认 false)',
660
661
  },
662
+ createdBy: {
663
+ type: 'string',
664
+ enum: ['manual', 'user-ai', 'system-ai', 'external-ai'],
665
+ default: 'external-ai',
666
+ description: '创建者类型:manual=用户手动 | user-ai=用户调用AI | system-ai=系统自动 | external-ai=外部AI Agent',
667
+ },
661
668
  },
662
669
  required: ['name', 'description', 'content'],
663
670
  },
664
671
  },
672
+ // 36. Skill 推荐:基于使用模式分析,推荐创建 Skill
673
+ {
674
+ name: 'autosnippet_suggest_skills',
675
+ description:
676
+ '基于项目使用模式分析,推荐创建 Skill。\n' +
677
+ '分析 4 个维度:Guard 违规模式、Memory 偏好积累、Recipe 分布缺口、候选积压率。\n' +
678
+ '返回推荐列表(含 name / description / rationale / priority),Agent 可据此直接调用 autosnippet_create_skill 创建。\n' +
679
+ '\n' +
680
+ '使用时机:\n' +
681
+ ' • 项目使用一段时间后,定期调用检查是否有新的 Skill 需求\n' +
682
+ ' • 用户反复说"我们项目不用…"、"以后都…"等偏好表述时\n' +
683
+ ' • Guard 违规频繁出现同一规则时\n' +
684
+ ' • 候选被大量驳回时',
685
+ inputSchema: { type: 'object', properties: {}, required: [] },
686
+ },
665
687
  // 36. ② 内容润色:Bootstrap 候选 AI 精炼(Phase 6)
666
688
  {
667
689
  name: 'autosnippet_bootstrap_refine',
@@ -1,8 +1,15 @@
1
1
  /**
2
2
  * 请求日志中间件
3
3
  * 使用 res.on('finish') 替代猴子补丁 res.send
4
+ *
5
+ * 精简策略:
6
+ * - GET 请求 + 2xx 状态码: 降为 debug(Dashboard 轮询高频噪音)
7
+ * - 非 GET / 非 2xx / 慢请求(>2s): 保留 info 级别
4
8
  */
5
9
 
10
+ // 轮询/心跳路径 — 完全静默
11
+ const SILENT_PATHS = ['/api/health', '/api/realtime/events', '/api/sse'];
12
+
6
13
  export function requestLogger(logger) {
7
14
  return (req, res, next) => {
8
15
  const startTime = Date.now();
@@ -10,14 +17,26 @@ export function requestLogger(logger) {
10
17
  res.on('finish', () => {
11
18
  const duration = Date.now() - startTime;
12
19
 
13
- logger.info('HTTP Request', {
20
+ // 完全静默的路径
21
+ if (SILENT_PATHS.some(p => req.path.startsWith(p))) return;
22
+
23
+ const logData = {
14
24
  method: req.method,
15
25
  path: req.path,
16
- query: Object.keys(req.query).length > 0 ? req.query : undefined,
17
26
  statusCode: res.statusCode,
18
27
  duration: `${duration}ms`,
19
- timestamp: new Date().toISOString(),
20
- });
28
+ };
29
+
30
+ // 非 GET / 非 2xx / 慢请求 → info; 其余 → debug
31
+ const isNoisy = req.method === 'GET' && res.statusCode >= 200 && res.statusCode < 300 && duration < 2000;
32
+ const isSlow = duration >= 1000;
33
+ if (isSlow) {
34
+ logger.warn(`🐌慢请求: ${req.method} ${req.path} - ${duration}ms`, logData);
35
+ } else if (isNoisy) {
36
+ logger.debug('HTTP', logData);
37
+ } else {
38
+ logger.info('HTTP', logData);
39
+ }
21
40
  });
22
41
 
23
42
  next();
@@ -79,7 +79,9 @@ router.post('/config', asyncHandler(async (req, res) => {
79
79
  const container = getServiceContainer();
80
80
  container.singletons.aiProvider = newProvider;
81
81
  logger.info('AI provider synced to DI container', { provider: provider.toLowerCase(), model: newProvider.model });
82
- } catch { /* container 未初始化时不阻塞 */ }
82
+ } catch (err) {
83
+ logger.debug('DI container 同步 AI provider 失败', { error: err.message });
84
+ }
83
85
 
84
86
  res.json({
85
87
  success: true,
@@ -23,8 +23,9 @@ const AUTH_USERNAME = process.env.ASD_AUTH_USERNAME || 'admin';
23
23
  const AUTH_PASSWORD = process.env.ASD_AUTH_PASSWORD || 'autosnippet';
24
24
  const TOKEN_SECRET = process.env.ASD_AUTH_SECRET || crypto.randomBytes(32).toString('hex');
25
25
 
26
- // 安全警告:使用默认凭据时提示
27
- if (!process.env.ASD_AUTH_USERNAME || !process.env.ASD_AUTH_PASSWORD) {
26
+ // 安全警告:仅在认证启用且使用默认凭据时提示
27
+ const authEnabled = process.env.VITE_AUTH_ENABLED === 'true' || process.env.ASD_AUTH_ENABLED === 'true';
28
+ if (authEnabled && (!process.env.ASD_AUTH_USERNAME || !process.env.ASD_AUTH_PASSWORD)) {
28
29
  console.warn(
29
30
  '[auth] WARNING: Using default credentials (admin/autosnippet). '
30
31
  + 'Set ASD_AUTH_USERNAME and ASD_AUTH_PASSWORD environment variables for production.',
@@ -7,27 +7,14 @@ import express from 'express';
7
7
  import { asyncHandler } from '../middleware/errorHandler.js';
8
8
  import { getServiceContainer } from '../../injection/ServiceContainer.js';
9
9
  import { NotFoundError, ValidationError } from '../../shared/errors/index.js';
10
+ import Logger from '../../infrastructure/logging/Logger.js';
11
+ import { getContext, safeInt } from '../utils/routeHelpers.js';
10
12
 
11
13
  const router = express.Router();
14
+ const logger = Logger.getInstance();
12
15
 
13
16
  const MAX_BATCH_SIZE = 100;
14
17
 
15
- /** 从请求中提取操作上下文 */
16
- function getContext(req) {
17
- return {
18
- userId: req.headers['x-user-id'] || 'anonymous',
19
- ip: req.ip,
20
- userAgent: req.headers['user-agent'] || '',
21
- };
22
- }
23
-
24
- /** 安全的整数解析 */
25
- function safeInt(value, defaultValue, min = 1, max = 1000) {
26
- const parsed = parseInt(value, 10);
27
- if (Number.isNaN(parsed)) return defaultValue;
28
- return Math.max(min, Math.min(max, parsed));
29
- }
30
-
31
18
  /**
32
19
  * GET /api/v1/candidates
33
20
  * 获取候选项列表(支持筛选和分页)
@@ -112,7 +99,9 @@ router.post('/', asyncHandler(async (req, res) => {
112
99
  duplicateCheck = await chatAgent.executeTool('check_duplicate', {
113
100
  candidate: candidateForCheck,
114
101
  });
115
- } catch { /* 查重失败不阻塞创建 */ }
102
+ } catch (err) {
103
+ logger.warn('自动查重失败,不阻塞创建', { error: err.message });
104
+ }
116
105
 
117
106
  res.status(201).json({
118
107
  success: true,
@@ -249,12 +238,35 @@ router.post('/batch-delete', asyncHandler(async (req, res) => {
249
238
  }
250
239
  const container = getServiceContainer();
251
240
  const candidateService = container.get('candidateService');
252
- const candidateRepo = container.get('candidateRepository');
253
- const list = await candidateService.listCandidates({ category: targetName }, { page: 1, pageSize: 2000 });
254
- const items = list.data || list.items || [];
241
+
242
+ // 查两次:按 category 字段 + 全量扫描 metadata.targetName
243
+ // (前端分组 key = metadata.targetName || category,两者可能不同)
244
+ const byCategory = await candidateService.listCandidates({ category: targetName }, { page: 1, pageSize: 2000 });
245
+ const byCategoryItems = byCategory.data || byCategory.items || [];
246
+
247
+ // 全量扫描 metadata.targetName 匹配(避免 category 不一致导致漏删)
248
+ const allList = await candidateService.listCandidates({}, { page: 1, pageSize: 5000 });
249
+ const allItems = allList.data || allList.items || [];
250
+ const byTargetName = allItems.filter(c => {
251
+ const meta = c.metadata || {};
252
+ return meta.targetName === targetName;
253
+ });
254
+
255
+ // 合并去重
256
+ const seen = new Set();
257
+ const merged = [];
258
+ for (const item of [...byCategoryItems, ...byTargetName]) {
259
+ if (!seen.has(item.id)) {
260
+ seen.add(item.id);
261
+ merged.push(item);
262
+ }
263
+ }
264
+
255
265
  let deleted = 0;
256
- for (const item of items) {
257
- try { await candidateRepo.delete(item.id); deleted++; } catch { /* skip */ }
266
+ for (const item of merged) {
267
+ try { await candidateService.deleteCandidate(item.id, { userId: 'batch-delete' }); deleted++; } catch (err) {
268
+ logger.debug('批量删除: 单条失败', { id: item.id, error: err.message });
269
+ }
258
270
  }
259
271
  res.json({ success: true, data: { deleted } });
260
272
  }));
@@ -298,7 +310,9 @@ router.post('/similarity', asyncHandler(async (req, res) => {
298
310
  if (result && result.length > 0) {
299
311
  return res.json({ success: true, data: { similar: result } });
300
312
  }
301
- } catch { /* fallback to text similarity */ }
313
+ } catch (err) {
314
+ logger.debug('SimilarityService 不可用,降级到文本相似度', { error: err.message });
315
+ }
302
316
 
303
317
  // Fallback: text-based similarity
304
318
  const allRecipes = await recipeService.listRecipes({}, { page: 1, pageSize: 500 });
@@ -490,11 +504,21 @@ router.post('/refine-preview', asyncHandler(async (req, res) => {
490
504
 
491
505
  const preview = result.results?.[0]?.preview || {};
492
506
 
493
- // 构建 after 对象
507
+ // 构建 after 对象(增强 code 字段校验:防止 AI 返回截断/片段代码或类型变更)
508
+ const origCode = before.code || '';
509
+ const isOrigMarkdown = /^---\s*\n/.test(origCode) || /^#\s+/.test(origCode) || (origCode.match(/^#{1,3}\s+/gm) || []).length >= 2;
510
+ const isPreviewMarkdown = preview.code && (/^---\s*\n/.test(preview.code) || /^#\s+/.test(preview.code) || (preview.code.match(/^#{1,3}\s+/gm) || []).length >= 2);
511
+ // 防止源代码被转成 Markdown 文档
512
+ const codeTypeChanged = !isOrigMarkdown && isPreviewMarkdown;
513
+ const isCodeValid = preview.code
514
+ && preview.code.length > 50
515
+ && preview.code !== before.code
516
+ && preview.code.length >= before.code.length * 0.4 // 不能太短(防止截断)
517
+ && !codeTypeChanged; // 不允许类型变更
494
518
  const after = {
495
519
  title: preview.title || before.title,
496
520
  summary: preview.summary || before.summary,
497
- code: (preview.code && preview.code.length > 50 && preview.code !== before.code) ? preview.code : before.code,
521
+ code: isCodeValid ? preview.code : before.code,
498
522
  tags: preview.tags ? [...new Set([...(before.tags || []), ...preview.tags])] : before.tags,
499
523
  confidence: (typeof preview.confidence === 'number' && preview.confidence !== 0.6) ? preview.confidence : before.confidence,
500
524
  relations: (preview.relations && Array.isArray(preview.relations) && preview.relations.length > 0) ? preview.relations : before.relations,
@@ -238,12 +238,4 @@ router.post('/files/save', asyncHandler(async (req, res) => {
238
238
  }
239
239
  }));
240
240
 
241
- /**
242
- * POST /api/v1/commands/execute
243
- * Execute command (stub - not supported for security)
244
- */
245
- router.post('/execute', asyncHandler(async (req, res) => {
246
- res.json({ success: false, error: 'Execute not supported' });
247
- }));
248
-
249
241
  export default router;
@@ -7,27 +7,12 @@ import express from 'express';
7
7
  import { asyncHandler } from '../middleware/errorHandler.js';
8
8
  import { getServiceContainer } from '../../injection/ServiceContainer.js';
9
9
  import { NotFoundError, ValidationError } from '../../shared/errors/index.js';
10
+ import { getContext, safeInt } from '../utils/routeHelpers.js';
10
11
 
11
12
  const router = express.Router();
12
13
 
13
14
  const MAX_BATCH_SIZE = 100;
14
15
 
15
- /** 从请求中提取操作上下文 */
16
- function getContext(req) {
17
- return {
18
- userId: req.headers['x-user-id'] || 'anonymous',
19
- ip: req.ip,
20
- userAgent: req.headers['user-agent'] || '',
21
- };
22
- }
23
-
24
- /** 安全的整数解析 */
25
- function safeInt(value, defaultValue, min = 1, max = 1000) {
26
- const parsed = parseInt(value, 10);
27
- if (Number.isNaN(parsed)) return defaultValue;
28
- return Math.max(min, Math.min(max, parsed));
29
- }
30
-
31
16
  /**
32
17
  * 将 Recipe 实体 → Guard 规则扁平格式(Dashboard GuardView 期望)
33
18
  */