autosnippet 2.9.0 → 2.10.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 (97) hide show
  1. package/README.md +4 -4
  2. package/bin/cli.js +5 -33
  3. package/config/constitution.yaml +9 -2
  4. package/dashboard/dist/assets/{icons-CH-H9x0E.js → icons-BkT3XrKf.js} +105 -100
  5. package/dashboard/dist/assets/index-BsB7DzW4.css +1 -0
  6. package/dashboard/dist/assets/index-DdmQMrJJ.js +155 -0
  7. package/dashboard/dist/index.html +3 -3
  8. package/lib/cli/AiScanService.js +13 -11
  9. package/lib/cli/KnowledgeSyncService.js +343 -0
  10. package/lib/cli/SetupService.js +8 -26
  11. package/lib/core/gateway/GatewayActionRegistry.js +48 -58
  12. package/lib/domain/index.js +16 -11
  13. package/lib/domain/knowledge/KnowledgeEntry.js +351 -0
  14. package/lib/domain/knowledge/KnowledgeRepository.js +123 -0
  15. package/lib/domain/knowledge/Lifecycle.js +109 -0
  16. package/lib/domain/knowledge/index.js +27 -0
  17. package/lib/domain/knowledge/values/Constraints.js +125 -0
  18. package/lib/domain/knowledge/values/Content.js +86 -0
  19. package/lib/domain/knowledge/values/Quality.js +93 -0
  20. package/lib/domain/knowledge/values/Reasoning.js +69 -0
  21. package/lib/domain/knowledge/values/Relations.js +168 -0
  22. package/lib/domain/knowledge/values/Stats.js +87 -0
  23. package/lib/domain/knowledge/values/index.js +9 -0
  24. package/lib/external/ai/AiProvider.js +48 -0
  25. package/lib/external/mcp/McpServer.js +7 -5
  26. package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +2 -2
  27. package/lib/external/mcp/handlers/bootstrap.js +116 -11
  28. package/lib/external/mcp/handlers/browse.js +77 -73
  29. package/lib/external/mcp/handlers/candidate.js +29 -276
  30. package/lib/external/mcp/handlers/guard.js +2 -0
  31. package/lib/external/mcp/handlers/knowledge.js +205 -0
  32. package/lib/external/mcp/handlers/structure.js +25 -23
  33. package/lib/external/mcp/handlers/system.js +10 -12
  34. package/lib/external/mcp/tools.js +125 -138
  35. package/lib/http/HttpServer.js +4 -8
  36. package/lib/http/routes/extract.js +48 -4
  37. package/lib/http/routes/knowledge.js +246 -0
  38. package/lib/http/routes/search.js +12 -17
  39. package/lib/infrastructure/database/migrations/016_unified_knowledge_entries.js +395 -0
  40. package/lib/infrastructure/external/XcodeAutomation.js +187 -103
  41. package/lib/injection/ServiceContainer.js +49 -60
  42. package/lib/repository/knowledge/KnowledgeRepository.impl.js +373 -0
  43. package/lib/service/automation/DirectiveDetector.js +2 -3
  44. package/lib/service/automation/FileWatcher.js +67 -28
  45. package/lib/service/automation/XcodeIntegration.js +931 -156
  46. package/lib/service/automation/handlers/AlinkHandler.js +6 -4
  47. package/lib/service/automation/handlers/CreateHandler.js +53 -18
  48. package/lib/service/automation/handlers/GuardHandler.js +183 -20
  49. package/lib/service/automation/handlers/SearchHandler.js +35 -17
  50. package/lib/service/chat/CandidateGuardrail.js +1 -1
  51. package/lib/service/chat/ChatAgent.js +46 -45
  52. package/lib/service/chat/ContextWindow.js +5 -5
  53. package/lib/service/chat/ProducerAgent.js +7 -7
  54. package/lib/service/chat/tools.js +130 -123
  55. package/lib/service/guard/GuardCheckEngine.js +114 -10
  56. package/lib/service/guard/GuardService.js +59 -48
  57. package/lib/service/knowledge/ConfidenceRouter.js +159 -0
  58. package/lib/service/knowledge/KnowledgeFileWriter.js +602 -0
  59. package/lib/service/knowledge/KnowledgeService.js +725 -0
  60. package/lib/service/search/SearchEngine.js +92 -19
  61. package/lib/service/skills/SignalCollector.js +12 -7
  62. package/lib/service/skills/SkillAdvisor.js +13 -11
  63. package/lib/service/snippet/SnippetFactory.js +5 -5
  64. package/package.json +1 -1
  65. package/scripts/install-cursor-skill.js +0 -6
  66. package/scripts/migrate-md-to-knowledge.mjs +364 -0
  67. package/skills/autosnippet-analysis/SKILL.md +15 -7
  68. package/skills/autosnippet-candidates/SKILL.md +6 -6
  69. package/skills/autosnippet-coldstart/SKILL.md +7 -3
  70. package/skills/autosnippet-concepts/SKILL.md +7 -6
  71. package/skills/autosnippet-create/SKILL.md +13 -13
  72. package/skills/autosnippet-intent/SKILL.md +3 -2
  73. package/skills/autosnippet-lifecycle/SKILL.md +5 -5
  74. package/skills/autosnippet-recipes/SKILL.md +16 -4
  75. package/templates/constitution.yaml +1 -1
  76. package/templates/copilot-instructions.md +6 -6
  77. package/templates/recipes-setup/README.md +3 -3
  78. package/dashboard/dist/assets/index-CqJRvYRL.js +0 -197
  79. package/dashboard/dist/assets/index-DICm9PNa.css +0 -1
  80. package/lib/cli/CandidateSyncService.js +0 -261
  81. package/lib/cli/SyncService.js +0 -356
  82. package/lib/domain/candidate/Candidate.js +0 -196
  83. package/lib/domain/candidate/CandidateRepository.js +0 -107
  84. package/lib/domain/candidate/Reasoning.js +0 -52
  85. package/lib/domain/recipe/Recipe.js +0 -421
  86. package/lib/domain/recipe/RecipeRepository.js +0 -54
  87. package/lib/domain/types/CandidateStatus.js +0 -52
  88. package/lib/http/routes/candidates.js +0 -559
  89. package/lib/http/routes/recipes.js +0 -397
  90. package/lib/repository/candidate/CandidateRepository.impl.js +0 -230
  91. package/lib/repository/recipe/RecipeRepository.impl.js +0 -498
  92. package/lib/service/candidate/CandidateAggregator.js +0 -52
  93. package/lib/service/candidate/CandidateFileWriter.js +0 -383
  94. package/lib/service/candidate/CandidateService.js +0 -1001
  95. package/lib/service/recipe/RecipeFileWriter.js +0 -514
  96. package/lib/service/recipe/RecipeService.js +0 -786
  97. package/lib/service/recipe/RecipeStatsTracker.js +0 -148
@@ -30,9 +30,11 @@ export async function handleAlink(alinkLine) {
30
30
 
31
31
  let recipeId = null;
32
32
  if (db) {
33
+ const rawDb = typeof db.getDb === 'function' ? db.getDb() : db;
33
34
  try {
34
- const row = db.prepare(
35
- 'SELECT id FROM recipes WHERE trigger = ? LIMIT 1',
35
+ // V3: knowledge_entries, trigger_key
36
+ const row = rawDb.prepare(
37
+ 'SELECT id FROM knowledge_entries WHERE trigger_key = ? AND lifecycle = \'active\' LIMIT 1',
36
38
  ).get(completionKey);
37
39
  if (row) recipeId = row.id;
38
40
  } catch {
@@ -43,8 +45,8 @@ export async function handleAlink(alinkLine) {
43
45
  if (!recipeId) {
44
46
  try {
45
47
  const escaped = completionKey.replace(/[%_\\]/g, ch => `\\${ch}`);
46
- const row = db.prepare(
47
- "SELECT id FROM recipes WHERE trigger LIKE ? ESCAPE '\\' OR title LIKE ? ESCAPE '\\' LIMIT 1",
48
+ const row = rawDb.prepare(
49
+ "SELECT id FROM knowledge_entries WHERE (trigger_key LIKE ? ESCAPE '\\' OR title LIKE ? ESCAPE '\\') AND lifecycle = 'active' LIMIT 1",
48
50
  ).get(`%${escaped}%`, `%${escaped}%`);
49
51
  if (row) recipeId = row.id;
50
52
  } catch { /* silent */ }
@@ -95,16 +95,17 @@ async function silentCreateCandidate(watcher, text, relativePath) {
95
95
  headers: r.headers || [],
96
96
  }));
97
97
 
98
- // 先尝试批量解析
98
+ // 先尝试批量解析(仅对 Recipe Markdown 格式有效)
99
99
  const allRecipes = parser.parseAll(text);
100
- if (allRecipes.length > 0) {
101
- const items = normalize(allRecipes);
100
+ const validRecipes = allRecipes.filter(r => r.title && r.title.trim());
101
+ if (validRecipes.length > 0) {
102
+ const items = normalize(validRecipes);
102
103
  await watcher._resolveHeadersIfNeeded(items[0], relativePath, text);
103
104
  await watcher._appendCandidates(items, 'watch-create');
104
105
  const msg =
105
- allRecipes.length === 1
106
- ? `已创建候选「${allRecipes[0].title}」,请在 Dashboard Candidates 页审核`
107
- : `已创建 ${allRecipes.length} 条候选,请在 Dashboard Candidates 页审核`;
106
+ validRecipes.length === 1
107
+ ? `已创建候选「${validRecipes[0].title}」,请在 Dashboard Candidates 页审核`
108
+ : `已创建 ${validRecipes.length} 条候选,请在 Dashboard Candidates 页审核`;
108
109
  console.log(`✅ [as:create] ${msg}`);
109
110
  watcher._notify(msg);
110
111
  return;
@@ -113,7 +114,7 @@ async function silentCreateCandidate(watcher, text, relativePath) {
113
114
  // 尝试单条解析
114
115
  if (parser.isCompleteRecipe(text)) {
115
116
  const one = parser.parse(text);
116
- if (one) {
117
+ if (one && one.title && one.title.trim()) {
117
118
  const item = normalize([one])[0];
118
119
  await watcher._resolveHeadersIfNeeded(item, relativePath, text);
119
120
  await watcher._appendCandidates([item], 'watch-create');
@@ -123,22 +124,56 @@ async function silentCreateCandidate(watcher, text, relativePath) {
123
124
  }
124
125
  }
125
126
 
126
- // 最后用 AI 摘要(通过 ChatAgent 统一入口)
127
+ // -c 模式:剪贴板内容整体作为一条候选(不拆分)
128
+ // 先用 AI 生成标题和摘要,code 保持剪贴板原文
129
+ const lang = relativePath && /\.swift$/i.test(relativePath) ? 'swift' : 'objc';
130
+ const fileName = relativePath
131
+ ? relativePath.split('/').pop()
132
+ : `clipboard.${lang === 'swift' ? 'swift' : 'm'}`;
133
+
134
+ let title = fileName.replace(/\.\w+$/, '') || 'Clipboard Snippet';
135
+ let summary = '';
136
+ let usageGuide = '';
137
+ let category = 'Utility';
138
+ let headers = [];
139
+ let tags = [];
140
+ let trigger = '';
141
+
142
+ // 尝试用 AI 生成摘要信息(但 code 始终保持原文)
127
143
  try {
128
144
  const { getServiceContainer } = await import('../../../injection/ServiceContainer.js');
129
145
  const container = getServiceContainer();
130
146
  const chatAgent = container.get('chatAgent');
131
- const lang = relativePath && /\.swift$/i.test(relativePath) ? 'swift' : 'objc';
132
- const result = await chatAgent.executeTool('summarize_code', { code: text, language: lang });
133
- if (result && !result.error && result.title && result.code) {
134
- await watcher._appendCandidates([result], 'watch-create');
135
- console.log(`✅ [as:create] 已静默创建候选「${result.title}」`);
136
- watcher._notify(`已创建候选「${result.title}」,请在 Candidates 页审核`);
137
- return;
147
+ const aiResult = await chatAgent.executeTool('summarize_code', { code: text, language: lang });
148
+ if (aiResult && !aiResult.error) {
149
+ title = aiResult.title || title;
150
+ summary = aiResult.summary || aiResult.summary_cn || '';
151
+ usageGuide = aiResult.usageGuide || aiResult.usageGuide_cn || '';
152
+ category = aiResult.category || category;
153
+ headers = aiResult.headers || [];
154
+ tags = aiResult.tags || [];
155
+ trigger = aiResult.trigger || '';
138
156
  }
139
- } catch { /* ChatAgent 不可用 */ }
140
-
141
- throw new Error('无法从剪贴板内容解析出候选');
157
+ } catch { /* AI 不可用,使用默认值 */ }
158
+
159
+ const item = {
160
+ title,
161
+ summary,
162
+ summary_cn: summary,
163
+ trigger,
164
+ category,
165
+ language: lang,
166
+ code: text, // 剪贴板原文整体
167
+ usageGuide,
168
+ usageGuide_cn: usageGuide,
169
+ headers,
170
+ tags,
171
+ };
172
+
173
+ await watcher._resolveHeadersIfNeeded(item, relativePath, text);
174
+ await watcher._appendCandidates([item], 'watch-create');
175
+ console.log(`✅ [as:create -c] 已创建候选「${title}」,请在 Candidates 页审核`);
176
+ watcher._notify(`已创建候选「${title}」,请在 Candidates 页审核`);
142
177
  }
143
178
 
144
179
  /**
@@ -1,42 +1,183 @@
1
1
  /**
2
2
  * GuardHandler — 处理 // as:a (audit/guard/lint) 指令
3
+ *
4
+ * 用法:
5
+ * // as:a — 检查当前文件 (scope=file)
6
+ * // as:a file — 同上,显式 file scope
7
+ * // as:a target — 检查当前文件所在目录树 (scope=target)
8
+ * // as:a project — 检查整个项目所有源文件 (scope=project)
9
+ * // as:a <keyword> — 检查当前文件 + 搜索相关规范
3
10
  */
4
11
 
5
- import { basename } from 'node:path';
12
+ import { basename, join, extname, dirname } from 'node:path';
13
+ import { readFile } from 'node:fs/promises';
14
+
15
+ /** 已知的 scope 关键词 */
16
+ const SCOPE_KEYWORDS = new Set(['file', 'target', 'project', 'all']);
17
+
18
+ /** 支持审计的源文件扩展名 */
19
+ const SOURCE_EXTS = new Set([
20
+ '.m', '.mm', '.h', '.swift',
21
+ '.c', '.cpp', '.cc', '.cxx', '.hpp',
22
+ '.js', '.ts', '.jsx', '.tsx',
23
+ '.java', '.kt', '.py', '.rb', '.go', '.rs',
24
+ ]);
6
25
 
7
26
  /**
8
- * @param {string} fullPath
9
- * @param {string} code
10
- * @param {string} guardLine
27
+ * 递归收集目录下所有源文件路径
11
28
  */
12
- export async function handleGuard(fullPath, code, guardLine) {
29
+ async function collectSourceFiles(dir) {
30
+ const { readdir } = await import('node:fs/promises');
31
+ const files = [];
32
+
33
+ // 跳过的目录
34
+ const SKIP_DIRS = new Set([
35
+ 'node_modules', '.git', 'build', 'DerivedData',
36
+ 'Pods', '.build', 'vendor', 'dist', '.next',
37
+ 'Carthage', 'xcuserdata', '__pycache__',
38
+ ]);
39
+
40
+ async function walk(currentDir) {
41
+ let entries;
42
+ try {
43
+ entries = await readdir(currentDir, { withFileTypes: true });
44
+ } catch {
45
+ return; // 权限不足等情况跳过
46
+ }
47
+ for (const entry of entries) {
48
+ if (entry.name.startsWith('.') && entry.name !== '.') continue;
49
+ const fullPath = join(currentDir, entry.name);
50
+ if (entry.isDirectory()) {
51
+ if (!SKIP_DIRS.has(entry.name)) {
52
+ await walk(fullPath);
53
+ }
54
+ } else if (entry.isFile() && SOURCE_EXTS.has(extname(entry.name).toLowerCase())) {
55
+ files.push(fullPath);
56
+ }
57
+ }
58
+ }
59
+
60
+ await walk(dir);
61
+ return files;
62
+ }
63
+
64
+ /**
65
+ * @param {import('../FileWatcher').default} watcher FileWatcher 实例
66
+ * @param {string} fullPath 当前文件绝对路径
67
+ * @param {string} code 当前文件内容
68
+ * @param {string} guardLine 触发行原文
69
+ */
70
+ export async function handleGuard(watcher, fullPath, code, guardLine) {
13
71
  const rest = guardLine.replace(/^\/\/\s*as:(?:audit|a|lint|l|guard|g)\s*/, '').trim();
14
- console.log(`\n🛡️ [Guard] 正在检查文件: ${basename(fullPath)}`);
72
+ const scopeArg = rest.toLowerCase();
73
+ const isScope = SCOPE_KEYWORDS.has(scopeArg);
74
+ // 确定 scope:无参数或 'file' → file;'target' → target;'project'/'all' → project
75
+ const scope = !rest || scopeArg === 'file' ? 'file'
76
+ : scopeArg === 'target' ? 'target'
77
+ : (scopeArg === 'project' || scopeArg === 'all') ? 'project'
78
+ : 'file'; // 非 scope 关键词回退到 file
15
79
 
16
80
  try {
17
81
  const { detectLanguage } = await import('../../guard/GuardCheckEngine.js');
18
82
  const { ServiceContainer } = await import('../../../injection/ServiceContainer.js');
19
83
  const container = ServiceContainer.getInstance();
20
84
  const engine = container.get('guardCheckEngine');
21
- const language = detectLanguage(fullPath);
22
- const violations = engine.checkCode(code, language);
23
85
 
24
- if (violations.length === 0) {
25
- console.log(` ✅ 无违规`);
26
- } else {
27
- const errors = violations.filter((v) => v.severity === 'error');
28
- const warnings = violations.filter((v) => v.severity === 'warning');
29
- console.log(` 🛡️ ${errors.length} errors, ${warnings.length} warnings`);
30
- for (const v of errors) {
31
- console.log(` ❌ L${v.line} [${v.ruleId}] ${v.message}`);
86
+ /* ── 多文件审计 (target / project) ── */
87
+ if (scope === 'project' || scope === 'target') {
88
+ let scanRoot;
89
+ if (scope === 'project') {
90
+ scanRoot = watcher?.projectRoot;
91
+ } else {
92
+ // target: 扫描当前文件所在目录树
93
+ scanRoot = dirname(fullPath);
94
+ }
95
+
96
+ if (!scanRoot) {
97
+ console.warn(' ⚠️ 无法确定扫描根目录,回退到单文件检查');
98
+ return _auditSingleFile(engine, fullPath, code, detectLanguage, 'file');
99
+ }
100
+
101
+ const scopeLabel = scope === 'project' ? '整个项目' : '当前目录';
102
+ console.log(`\n🛡️ [Guard] 正在扫描${scopeLabel}: ${scanRoot}`);
103
+ const sourcePaths = await collectSourceFiles(scanRoot);
104
+ console.log(` 📁 找到 ${sourcePaths.length} 个源文件`);
105
+
106
+ if (sourcePaths.length === 0) {
107
+ console.log(' ✅ 未找到源文件');
108
+ return;
109
+ }
110
+
111
+ // 读取所有文件内容
112
+ const fileEntries = [];
113
+ let readErrors = 0;
114
+ for (const p of sourcePaths) {
115
+ try {
116
+ const content = await readFile(p, 'utf-8');
117
+ fileEntries.push({ path: p, content });
118
+ } catch {
119
+ readErrors++;
120
+ }
32
121
  }
33
- for (const v of warnings.slice(0, 5)) {
34
- console.log(` ⚠️ L${v.line} [${v.ruleId}] ${v.message}`);
122
+ if (readErrors > 0) {
123
+ console.log(` ⚠️ ${readErrors} 个文件无法读取,已跳过`);
35
124
  }
125
+
126
+ // 批量审计(传递 scope 以启用对应维度规则)
127
+ const report = engine.auditFiles(fileEntries, { scope });
128
+ const { summary } = report;
129
+
130
+ if (summary.totalViolations === 0) {
131
+ console.log(` ✅ ${summary.filesChecked} 个文件全部通过,无违规`);
132
+ } else {
133
+ console.log(` 🛡️ 扫描 ${summary.filesChecked} 个文件:`);
134
+ console.log(` ${summary.totalErrors} errors, ${summary.totalViolations - summary.totalErrors} warnings`);
135
+ console.log(` ${summary.filesWithViolations} 个文件存在问题\n`);
136
+
137
+ // 按文件输出详情(限制输出前 10 个有问题的文件)
138
+ const filesWithIssues = report.files.filter(f => f.summary.total > 0);
139
+ for (const file of filesWithIssues.slice(0, 10)) {
140
+ const rel = file.filePath.replace(scanRoot + '/', '');
141
+ console.log(` 📄 ${rel} (${file.summary.errors}E / ${file.summary.warnings}W)`);
142
+ const errors = file.violations.filter(v => v.severity === 'error');
143
+ const warnings = file.violations.filter(v => v.severity === 'warning');
144
+ for (const v of errors.slice(0, 5)) {
145
+ console.log(` ❌ L${v.line} [${v.ruleId}] ${v.message}`);
146
+ }
147
+ if (errors.length > 5) console.log(` ... 还有 ${errors.length - 5} 个 errors`);
148
+ for (const v of warnings.slice(0, 3)) {
149
+ console.log(` ⚠️ L${v.line} [${v.ruleId}] ${v.message}`);
150
+ }
151
+ if (warnings.length > 3) console.log(` ... 还有 ${warnings.length - 3} 个 warnings`);
152
+ }
153
+ if (filesWithIssues.length > 10) {
154
+ console.log(`\n ... 还有 ${filesWithIssues.length - 10} 个文件有问题,已省略`);
155
+ }
156
+
157
+ // 跨文件问题汇总
158
+ if (report.crossFileViolations?.length > 0) {
159
+ console.log(`\n 🔗 跨文件问题 (${report.crossFileViolations.length}):`);
160
+ for (const v of report.crossFileViolations.slice(0, 10)) {
161
+ console.log(` ⚠️ [${v.ruleId}] ${v.message}`);
162
+ if (v.locations) {
163
+ for (const loc of v.locations.slice(0, 5)) {
164
+ const relLoc = loc.filePath.replace(scanRoot + '/', '');
165
+ console.log(` 📄 ${relLoc}:L${loc.line}`);
166
+ }
167
+ if (v.locations.length > 5) console.log(` ... 还有 ${v.locations.length - 5} 处`);
168
+ }
169
+ }
170
+ }
171
+ }
172
+ return;
36
173
  }
37
174
 
38
- // 如果有关键词,也做语义搜索
39
- if (rest) {
175
+ /* ── 单文件审计 (file scope) ── */
176
+ console.log(`\n🛡️ [Guard] 正在检查文件: ${basename(fullPath)}`);
177
+ _auditSingleFile(engine, fullPath, code, detectLanguage, scope);
178
+
179
+ // 如果有非 scope 关键词,也做语义搜索
180
+ if (rest && !isScope) {
40
181
  try {
41
182
  const searchEngine = container.get('searchEngine');
42
183
  const results = await searchEngine.search(rest, { limit: 3, mode: 'keyword' });
@@ -54,3 +195,25 @@ export async function handleGuard(fullPath, code, guardLine) {
54
195
  console.warn(` ⚠️ Guard 检查失败: ${err.message}`);
55
196
  }
56
197
  }
198
+
199
+ /**
200
+ * 检查单个文件并打印结果
201
+ */
202
+ function _auditSingleFile(engine, fullPath, code, detectLanguage, scope = 'file') {
203
+ const language = detectLanguage(fullPath);
204
+ const violations = engine.checkCode(code, language, { scope });
205
+
206
+ if (violations.length === 0) {
207
+ console.log(` ✅ 无违规`);
208
+ } else {
209
+ const errors = violations.filter((v) => v.severity === 'error');
210
+ const warnings = violations.filter((v) => v.severity === 'warning');
211
+ console.log(` 🛡️ ${errors.length} errors, ${warnings.length} warnings`);
212
+ for (const v of errors) {
213
+ console.log(` ❌ L${v.line} [${v.ruleId}] ${v.message}`);
214
+ }
215
+ for (const v of warnings.slice(0, 5)) {
216
+ console.log(` ⚠️ L${v.line} [${v.ruleId}] ${v.message}`);
217
+ }
218
+ }
219
+ }
@@ -26,22 +26,35 @@ export async function handleSearch(watcher, fullPath, relativePath, searchLine)
26
26
  const container = ServiceContainer.getInstance();
27
27
  const searchEngine = container.get('searchEngine');
28
28
 
29
+ // 诊断:输出索引状态
30
+ const stats = searchEngine.getStats();
31
+ if (stats.totalDocuments === 0) {
32
+ console.log(` ⚠️ 知识库为空(索引 0 条记录),请先通过 asd setup / Dashboard 添加知识条目`);
33
+ } else {
34
+ console.log(` 📊 索引 ${stats.totalDocuments} 条知识`);
35
+ }
36
+
37
+ // BM25 → keyword 逐级降级:空结果也触发降级(中文分词不足时 BM25 可能零命中)
29
38
  try {
30
39
  results = await searchEngine.search(query, { limit: 10, mode: 'bm25' });
40
+ if (!results || (results.items || []).length === 0) {
41
+ results = await searchEngine.search(query, { limit: 10, mode: 'keyword' });
42
+ }
31
43
  } catch {
32
44
  results = await searchEngine.search(query, { limit: 10, mode: 'keyword' });
33
45
  }
34
46
  } catch (err) {
35
47
  console.warn(` ⚠️ 搜索失败: ${err.message}`);
36
- watcher._openDashboard(`/?action=search&q=${encodeURIComponent(query)}`);
48
+ console.log(` ℹ️ 未找到「${query}」的相关结果`);
49
+ watcher._notify(`搜索「${query}」失败: ${err.message}`);
37
50
  return;
38
51
  }
39
52
 
40
53
  const items = normalizeSearchResults(results);
41
54
 
42
55
  if (items.length === 0) {
43
- console.log(` ℹ️ 未找到相关结果`);
44
- watcher._openDashboard(`/?action=search&q=${encodeURIComponent(query)}&path=${encodeURIComponent(relativePath)}`);
56
+ console.log(` ℹ️ 未找到「${query}」的相关结果`);
57
+ watcher._notify(`未找到「${query}」的相关结果`);
45
58
  return;
46
59
  }
47
60
 
@@ -103,35 +116,40 @@ export function normalizeSearchResults(results) {
103
116
  }
104
117
  } catch { /* ignore */ }
105
118
  }
106
- // headers 优先从 dimensions_json 取(V2 标准存储位置)
107
- if (headers.length === 0 && r.dimensions_json) {
119
+ // V3: headers 是独立 JSON 列(字符串),优先解析
120
+ if (headers.length === 0 && r.headers) {
108
121
  try {
109
- const dims = typeof r.dimensions_json === 'string' ? JSON.parse(r.dimensions_json) : r.dimensions_json;
110
- if (Array.isArray(dims.headers) && dims.headers.length > 0) {
111
- headers = dims.headers;
122
+ const parsed = typeof r.headers === 'string' ? JSON.parse(r.headers) : r.headers;
123
+ if (Array.isArray(parsed) && parsed.length > 0) {
124
+ headers = parsed;
112
125
  }
113
126
  } catch { /* ignore */ }
114
127
  }
115
- // 兜底:顶层 r.headers
116
- if (headers.length === 0 && Array.isArray(r.headers) && r.headers.length > 0) {
117
- headers = r.headers;
118
- }
119
- // moduleName: 从 dimensions_json 或 content_json 或顶层提取
120
- let moduleName = null;
121
- if (r.dimensions_json) {
128
+ // V2 兜底:从 dimensions_json 取
129
+ if (headers.length === 0 && r.dimensions_json) {
122
130
  try {
123
131
  const dims = typeof r.dimensions_json === 'string' ? JSON.parse(r.dimensions_json) : r.dimensions_json;
124
- moduleName = dims.moduleName || dims.module_name || null;
132
+ if (Array.isArray(dims.headers) && dims.headers.length > 0) {
133
+ headers = dims.headers;
134
+ }
125
135
  } catch { /* ignore */ }
126
136
  }
137
+ // moduleName: V3 独立列 > content_json > dimensions_json > 顶层
138
+ let moduleName = r.module_name || null;
127
139
  if (!moduleName && r.content_json) {
128
140
  try {
129
141
  const content = typeof r.content_json === 'string' ? JSON.parse(r.content_json) : r.content_json;
130
142
  moduleName = content.moduleName || content.module_name || null;
131
143
  } catch { /* ignore */ }
132
144
  }
145
+ if (!moduleName && r.dimensions_json) {
146
+ try {
147
+ const dims = typeof r.dimensions_json === 'string' ? JSON.parse(r.dimensions_json) : r.dimensions_json;
148
+ moduleName = dims.moduleName || dims.module_name || null;
149
+ } catch { /* ignore */ }
150
+ }
133
151
  if (!moduleName) {
134
- moduleName = r.moduleName || r.module_name || null;
152
+ moduleName = r.moduleName || null;
135
153
  }
136
154
 
137
155
  return {
@@ -27,7 +27,7 @@ export class CandidateGuardrail {
27
27
 
28
28
  /**
29
29
  * 验证候选结构
30
- * @param {object} candidate — submit_candidate 工具参数
30
+ * @param {object} candidate — submit_knowledge 工具参数
31
31
  * @returns {{ valid: boolean, error?: string }}
32
32
  */
33
33
  validateStructure(candidate) {