autosnippet 2.8.3 → 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.
- package/README.md +5 -5
- package/bin/cli.js +5 -33
- package/config/constitution.yaml +9 -2
- package/dashboard/dist/assets/{icons-B_Xg4B-s.js → icons-BkT3XrKf.js} +105 -100
- package/dashboard/dist/assets/index-BsB7DzW4.css +1 -0
- package/dashboard/dist/assets/index-DdmQMrJJ.js +155 -0
- package/dashboard/dist/index.html +3 -3
- package/lib/cli/AiScanService.js +13 -11
- package/lib/cli/KnowledgeSyncService.js +343 -0
- package/lib/cli/SetupService.js +9 -27
- package/lib/core/ast/ProjectGraph.js +160 -0
- package/lib/core/gateway/GatewayActionRegistry.js +48 -58
- package/lib/domain/index.js +16 -11
- package/lib/domain/knowledge/KnowledgeEntry.js +351 -0
- package/lib/domain/knowledge/KnowledgeRepository.js +123 -0
- package/lib/domain/knowledge/Lifecycle.js +109 -0
- package/lib/domain/knowledge/index.js +27 -0
- package/lib/domain/knowledge/values/Constraints.js +125 -0
- package/lib/domain/knowledge/values/Content.js +86 -0
- package/lib/domain/knowledge/values/Quality.js +93 -0
- package/lib/domain/knowledge/values/Reasoning.js +69 -0
- package/lib/domain/knowledge/values/Relations.js +168 -0
- package/lib/domain/knowledge/values/Stats.js +87 -0
- package/lib/domain/knowledge/values/index.js +9 -0
- package/lib/external/ai/AiProvider.js +48 -0
- package/lib/external/ai/providers/GoogleGeminiProvider.js +12 -3
- package/lib/external/mcp/McpServer.js +7 -5
- package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +3 -2
- package/lib/external/mcp/handlers/bootstrap.js +121 -12
- package/lib/external/mcp/handlers/browse.js +77 -73
- package/lib/external/mcp/handlers/candidate.js +29 -276
- package/lib/external/mcp/handlers/guard.js +2 -0
- package/lib/external/mcp/handlers/knowledge.js +205 -0
- package/lib/external/mcp/handlers/skill.js +4 -2
- package/lib/external/mcp/handlers/structure.js +25 -23
- package/lib/external/mcp/handlers/system.js +10 -12
- package/lib/external/mcp/tools.js +125 -138
- package/lib/http/HttpServer.js +4 -8
- package/lib/http/middleware/requestLogger.js +3 -3
- package/lib/http/routes/ai.js +17 -1
- package/lib/http/routes/extract.js +48 -4
- package/lib/http/routes/knowledge.js +246 -0
- package/lib/http/routes/search.js +12 -17
- package/lib/http/routes/skills.js +44 -1
- package/lib/infrastructure/cache/GraphCache.js +143 -0
- package/lib/infrastructure/database/migrations/015_create_token_usage.js +27 -0
- package/lib/infrastructure/database/migrations/016_unified_knowledge_entries.js +395 -0
- package/lib/infrastructure/external/XcodeAutomation.js +187 -103
- package/lib/infrastructure/realtime/RealtimeService.js +14 -2
- package/lib/injection/ServiceContainer.js +164 -63
- package/lib/repository/knowledge/KnowledgeRepository.impl.js +373 -0
- package/lib/repository/token/TokenUsageStore.js +162 -0
- package/lib/service/automation/DirectiveDetector.js +2 -3
- package/lib/service/automation/FileWatcher.js +67 -28
- package/lib/service/automation/XcodeIntegration.js +931 -156
- package/lib/service/automation/handlers/AlinkHandler.js +6 -4
- package/lib/service/automation/handlers/CreateHandler.js +53 -18
- package/lib/service/automation/handlers/GuardHandler.js +183 -20
- package/lib/service/automation/handlers/SearchHandler.js +35 -17
- package/lib/service/chat/AnalystAgent.js +25 -14
- package/lib/service/chat/CandidateGuardrail.js +1 -1
- package/lib/service/chat/ChatAgent.js +280 -48
- package/lib/service/chat/ContextWindow.js +92 -8
- package/lib/service/chat/HandoffProtocol.js +26 -1
- package/lib/service/chat/ProducerAgent.js +11 -9
- package/lib/service/chat/tools.js +298 -194
- package/lib/service/guard/GuardCheckEngine.js +114 -10
- package/lib/service/guard/GuardService.js +59 -48
- package/lib/service/knowledge/ConfidenceRouter.js +159 -0
- package/lib/service/knowledge/KnowledgeFileWriter.js +602 -0
- package/lib/service/knowledge/KnowledgeService.js +725 -0
- package/lib/service/search/SearchEngine.js +92 -19
- package/lib/service/skills/SignalCollector.js +15 -9
- package/lib/service/skills/SkillAdvisor.js +13 -11
- package/lib/service/snippet/SnippetFactory.js +5 -5
- package/lib/service/spm/SpmService.js +119 -18
- package/package.json +1 -1
- package/scripts/install-cursor-skill.js +0 -6
- package/scripts/migrate-md-to-knowledge.mjs +364 -0
- package/skills/autosnippet-analysis/SKILL.md +15 -7
- package/skills/autosnippet-candidates/SKILL.md +6 -6
- package/skills/autosnippet-coldstart/SKILL.md +7 -3
- package/skills/autosnippet-concepts/SKILL.md +7 -6
- package/skills/autosnippet-create/SKILL.md +13 -13
- package/skills/autosnippet-intent/SKILL.md +3 -2
- package/skills/autosnippet-lifecycle/SKILL.md +5 -5
- package/skills/autosnippet-recipes/SKILL.md +16 -4
- package/templates/constitution.yaml +1 -1
- package/templates/copilot-instructions.md +6 -6
- package/templates/recipes-setup/README.md +3 -3
- package/dashboard/dist/assets/index-CkIih2CC.css +0 -1
- package/dashboard/dist/assets/index-Duc8Qk-c.js +0 -197
- package/lib/cli/CandidateSyncService.js +0 -261
- package/lib/cli/SyncService.js +0 -356
- package/lib/domain/candidate/Candidate.js +0 -196
- package/lib/domain/candidate/CandidateRepository.js +0 -107
- package/lib/domain/candidate/Reasoning.js +0 -52
- package/lib/domain/recipe/Recipe.js +0 -421
- package/lib/domain/recipe/RecipeRepository.js +0 -54
- package/lib/domain/types/CandidateStatus.js +0 -52
- package/lib/http/routes/candidates.js +0 -559
- package/lib/http/routes/recipes.js +0 -397
- package/lib/repository/candidate/CandidateRepository.impl.js +0 -230
- package/lib/repository/recipe/RecipeRepository.impl.js +0 -498
- package/lib/service/candidate/CandidateAggregator.js +0 -52
- package/lib/service/candidate/CandidateFileWriter.js +0 -383
- package/lib/service/candidate/CandidateService.js +0 -973
- package/lib/service/recipe/RecipeFileWriter.js +0 -514
- package/lib/service/recipe/RecipeService.js +0 -786
- 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
|
-
|
|
35
|
-
|
|
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 =
|
|
47
|
-
"SELECT id FROM
|
|
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
|
-
|
|
101
|
-
|
|
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
|
-
|
|
106
|
-
? `已创建候选「${
|
|
107
|
-
: `已创建 ${
|
|
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
|
-
//
|
|
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
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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 { /*
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
*
|
|
9
|
-
* @param {string} code
|
|
10
|
-
* @param {string} guardLine
|
|
27
|
+
* 递归收集目录下所有源文件路径
|
|
11
28
|
*/
|
|
12
|
-
|
|
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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
34
|
-
console.log(` ⚠️
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
107
|
-
if (headers.length === 0 && r.
|
|
119
|
+
// V3: headers 是独立 JSON 列(字符串),优先解析
|
|
120
|
+
if (headers.length === 0 && r.headers) {
|
|
108
121
|
try {
|
|
109
|
-
const
|
|
110
|
-
if (Array.isArray(
|
|
111
|
-
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
|
-
//
|
|
116
|
-
if (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
|
-
|
|
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 ||
|
|
152
|
+
moduleName = r.moduleName || null;
|
|
135
153
|
}
|
|
136
154
|
|
|
137
155
|
return {
|
|
@@ -19,20 +19,31 @@ import Logger from '../../infrastructure/logging/Logger.js';
|
|
|
19
19
|
// System Prompt — Analyst 专用 (~100 tokens)
|
|
20
20
|
// ──────────────────────────────────────────────────────────────────
|
|
21
21
|
|
|
22
|
-
const ANALYST_SYSTEM_PROMPT =
|
|
22
|
+
const ANALYST_SYSTEM_PROMPT = `你是一位高级软件架构师,正在深度分析一个真实项目的某个维度。
|
|
23
23
|
|
|
24
|
-
##
|
|
25
|
-
|
|
26
|
-
这些工具返回精确的类继承、协议实现、方法签名信息,比文本搜索更高效。
|
|
27
|
-
2. **文本搜索补充** — search_project_code 用于查找特定模式/关键字
|
|
28
|
-
3. **文件阅读验证** — read_project_file 用于确认具体实现细节
|
|
24
|
+
## 执行计划
|
|
25
|
+
你有 **N 轮**工具调用机会(系统会告知具体数字)。请严格按以下节奏分配:
|
|
29
26
|
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
| 阶段 | 轮次占比 | 目标 |
|
|
28
|
+
|------|---------|------|
|
|
29
|
+
| 1. 全局扫描 | 第 1-3 轮 | get_project_overview + list_project_structure 了解项目结构 |
|
|
30
|
+
| 2. 结构化探索 | 第 4-N×60% 轮 | get_class_hierarchy / get_class_info 理解核心类;search_project_code 批量搜索关键模式 |
|
|
31
|
+
| 3. 深度验证 | 第 N×60%-N×80% 轮 | read_project_file 阅读关键实现,确认细节 |
|
|
32
|
+
| 4. 输出总结 | 最后 20% | **停止调用工具**,直接输出你的分析文本 |
|
|
32
33
|
|
|
34
|
+
## 关键规则
|
|
35
|
+
- **到达 80% 轮次时必须开始写总结**,不要等系统提醒
|
|
36
|
+
- 每一轮都必须调用工具获取新信息,不要花轮次在纯文本思考上
|
|
37
|
+
- 不要重复搜索相同关键词或读取相同文件(系统会返回缓存并扣轮次)
|
|
38
|
+
|
|
39
|
+
## 工具效率
|
|
40
|
+
- **批量搜索**: search_project_code({ patterns: ["keywordA", "keywordB", "keywordC"] }) — 一次搜 3-5 个
|
|
41
|
+
- **批量读文件**: read_project_file({ filePaths: ["a.m", "b.m", "c.m"] }) — 一次读 3-5 个
|
|
42
|
+
- **结构化查询优先**: get_class_hierarchy / get_class_info 比文本搜索更精确高效
|
|
43
|
+
|
|
44
|
+
## 输出要求
|
|
33
45
|
输出你的分析发现,包括具体的文件路径和代码位置。
|
|
34
|
-
|
|
35
|
-
尽可能多地使用工具来获取准确信息,不要猜测。`;
|
|
46
|
+
用自然语言描述你的理解,不需要特定格式。`;
|
|
36
47
|
|
|
37
48
|
// ──────────────────────────────────────────────────────────────────
|
|
38
49
|
// Analyst 可用工具白名单 — 只做探索,不做提交
|
|
@@ -61,12 +72,12 @@ const ANALYST_TOOLS = [
|
|
|
61
72
|
// ──────────────────────────────────────────────────────────────────
|
|
62
73
|
|
|
63
74
|
const ANALYST_BUDGET = {
|
|
64
|
-
maxIterations:
|
|
65
|
-
searchBudget:
|
|
66
|
-
searchBudgetGrace:
|
|
75
|
+
maxIterations: 24, // was 18 — 大项目维度需要充足探索轮次
|
|
76
|
+
searchBudget: 18, // was 14 — 匹配更大探索空间
|
|
77
|
+
searchBudgetGrace: 10, // was 8
|
|
67
78
|
maxSubmits: 0, // Analyst 不提交候选
|
|
68
79
|
softSubmitLimit: 0,
|
|
69
|
-
idleRoundsToExit: 2, //
|
|
80
|
+
idleRoundsToExit: 2, // 减少空转
|
|
70
81
|
};
|
|
71
82
|
|
|
72
83
|
// ──────────────────────────────────────────────────────────────────
|