autosnippet 2.6.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.
- package/bin/cli.js +1 -1
- package/dashboard/dist/assets/{icons-rnn04CvH.js → icons-Cq4-iQhP.js} +148 -88
- package/dashboard/dist/assets/index-DBxH7pVn.css +1 -0
- package/dashboard/dist/assets/index-Dw2F6qAS.js +197 -0
- package/dashboard/dist/assets/{react-markdown-CWxUbOf4.js → react-markdown-BA6FB2NP.js} +1 -1
- package/dashboard/dist/assets/{syntax-highlighter-CJ2drQQb.js → syntax-highlighter-CVLHn9O5.js} +1 -1
- package/dashboard/dist/assets/{vendor-f83ah6cm.js → vendor-BotF760a.js} +61 -61
- package/dashboard/dist/index.html +6 -6
- package/lib/bootstrap.js +1 -1
- package/lib/cli/SetupService.js +33 -8
- package/lib/cli/UpgradeService.js +139 -2
- package/lib/core/ast/ProjectGraph.js +599 -0
- package/lib/core/gateway/GatewayActionRegistry.js +2 -2
- package/lib/domain/recipe/Recipe.js +3 -0
- package/lib/external/ai/AiProvider.js +83 -20
- package/lib/external/ai/providers/ClaudeProvider.js +197 -0
- package/lib/external/ai/providers/GoogleGeminiProvider.js +235 -1
- package/lib/external/ai/providers/OpenAiProvider.js +131 -0
- package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-context.js +216 -0
- package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +468 -0
- package/lib/external/mcp/handlers/bootstrap/pipeline/tier-scheduler.js +162 -0
- package/lib/external/mcp/handlers/bootstrap/skills.js +225 -0
- package/lib/external/mcp/handlers/bootstrap.js +151 -1634
- package/lib/external/mcp/handlers/browse.js +1 -1
- package/lib/external/mcp/handlers/candidate.js +1 -33
- package/lib/external/mcp/handlers/skill.js +54 -17
- package/lib/external/mcp/tools.js +4 -3
- package/lib/http/middleware/requestLogger.js +23 -4
- package/lib/http/routes/ai.js +3 -1
- package/lib/http/routes/auth.js +3 -2
- package/lib/http/routes/candidates.js +49 -25
- package/lib/http/routes/commands.js +0 -8
- package/lib/http/routes/guardRules.js +1 -16
- package/lib/http/routes/recipes.js +4 -17
- package/lib/http/routes/search.js +11 -19
- package/lib/http/routes/skills.js +2 -0
- package/lib/http/routes/snippets.js +0 -33
- package/lib/http/routes/spm.js +37 -63
- package/lib/http/utils/routeHelpers.js +31 -0
- package/lib/infrastructure/config/Paths.js +9 -0
- package/lib/infrastructure/logging/Logger.js +86 -3
- package/lib/infrastructure/realtime/RealtimeService.js +2 -5
- package/lib/infrastructure/vector/JsonVectorAdapter.js +24 -1
- package/lib/injection/ServiceContainer.js +55 -2
- package/lib/service/bootstrap/BootstrapTaskManager.js +400 -0
- package/lib/service/candidate/CandidateFileWriter.js +68 -27
- package/lib/service/candidate/CandidateService.js +156 -10
- package/lib/service/chat/AnalystAgent.js +216 -0
- package/lib/service/chat/CandidateGuardrail.js +134 -0
- package/lib/service/chat/ChatAgent.js +1036 -167
- package/lib/service/chat/ContextWindow.js +730 -0
- package/lib/service/chat/HandoffProtocol.js +180 -0
- package/lib/service/chat/ProducerAgent.js +240 -0
- package/lib/service/chat/ToolRegistry.js +149 -5
- package/lib/service/chat/tools.js +1397 -61
- package/lib/service/recipe/RecipeFileWriter.js +12 -1
- package/lib/service/skills/SignalCollector.js +31 -6
- package/lib/service/skills/SkillAdvisor.js +2 -1
- package/lib/service/skills/SkillHooks.js +13 -5
- package/lib/service/spm/SpmService.js +2 -2
- package/package.json +1 -1
- package/templates/copilot-instructions.md +20 -3
- package/templates/cursor-rules/autosnippet-conventions.mdc +21 -4
- package/templates/cursor-rules/autosnippet-skills.mdc +45 -0
- package/dashboard/dist/assets/index-BBKa3Dgi.js +0 -195
- package/dashboard/dist/assets/index-DLsECfzW.css +0 -1
|
@@ -154,6 +154,47 @@ export class CandidateService {
|
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
/**
|
|
158
|
+
* 删除候选项(DB + .md 文件)
|
|
159
|
+
*/
|
|
160
|
+
async deleteCandidate(candidateId, context = {}) {
|
|
161
|
+
try {
|
|
162
|
+
const candidate = await this.candidateRepository.findById(candidateId);
|
|
163
|
+
|
|
164
|
+
// 先删 .md 文件(即使 DB 中不存在也尝试按 id 清理文件)
|
|
165
|
+
if (this.fileWriter && candidate) {
|
|
166
|
+
this.fileWriter.removeCandidate(candidate);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 删除 DB 记录
|
|
170
|
+
const deleted = await this.candidateRepository.delete(candidateId);
|
|
171
|
+
|
|
172
|
+
// 审计日志
|
|
173
|
+
if (deleted) {
|
|
174
|
+
await this.auditLogger.log({
|
|
175
|
+
action: 'delete_candidate',
|
|
176
|
+
resource: `candidate:${candidateId}`,
|
|
177
|
+
actor: context.userId || 'system',
|
|
178
|
+
result: 'success',
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
this.logger.info('Candidate deleted', {
|
|
183
|
+
candidateId,
|
|
184
|
+
dbDeleted: deleted,
|
|
185
|
+
fileRemoved: !!candidate,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
return deleted;
|
|
189
|
+
} catch (error) {
|
|
190
|
+
this.logger.error('Error deleting candidate', {
|
|
191
|
+
candidateId,
|
|
192
|
+
error: error.message,
|
|
193
|
+
});
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
157
198
|
/**
|
|
158
199
|
* 驳回候选项
|
|
159
200
|
*/
|
|
@@ -543,6 +584,8 @@ export class CandidateService {
|
|
|
543
584
|
throw new ValidationError('AI provider with chat capability is required');
|
|
544
585
|
}
|
|
545
586
|
|
|
587
|
+
const onProgress = typeof options.onProgress === 'function' ? options.onProgress : null;
|
|
588
|
+
|
|
546
589
|
// 1. 收集候选
|
|
547
590
|
let candidates;
|
|
548
591
|
if (options.candidateIds?.length) {
|
|
@@ -564,6 +607,12 @@ export class CandidateService {
|
|
|
564
607
|
return { refined: 0, total: 0, errors: [], results: [] };
|
|
565
608
|
}
|
|
566
609
|
|
|
610
|
+
// 通知:润色开始
|
|
611
|
+
onProgress?.('refine:started', {
|
|
612
|
+
total: candidates.length,
|
|
613
|
+
candidateIds: candidates.map(c => c.id),
|
|
614
|
+
});
|
|
615
|
+
|
|
567
616
|
// 2. 收集同批次标题列表(供 AI 推断关系)
|
|
568
617
|
const allTitles = candidates.map(c => (c.metadata || {}).title || '').filter(Boolean);
|
|
569
618
|
|
|
@@ -571,22 +620,46 @@ export class CandidateService {
|
|
|
571
620
|
const results = [];
|
|
572
621
|
const errors = [];
|
|
573
622
|
let refined = 0;
|
|
623
|
+
let processed = 0;
|
|
574
624
|
|
|
575
625
|
for (const candidate of candidates) {
|
|
626
|
+
processed++;
|
|
627
|
+
const title = (candidate.metadata || {}).title || '';
|
|
628
|
+
|
|
629
|
+
// 通知:开始处理当前候选
|
|
630
|
+
onProgress?.('refine:item-started', {
|
|
631
|
+
candidateId: candidate.id,
|
|
632
|
+
title,
|
|
633
|
+
current: processed,
|
|
634
|
+
total: candidates.length,
|
|
635
|
+
progress: Math.round(((processed - 1) / candidates.length) * 100),
|
|
636
|
+
});
|
|
637
|
+
|
|
576
638
|
try {
|
|
577
639
|
const meta = { ...(candidate.metadata || {}) };
|
|
578
|
-
const title = meta.title || '';
|
|
579
640
|
const prompt = this._buildRefinePrompt(candidate, allTitles, options.userPrompt);
|
|
580
641
|
const response = await aiProvider.chat(prompt, { temperature: 0.3 });
|
|
581
642
|
const parsed = aiProvider.extractJSON(response, '{', '}');
|
|
582
643
|
|
|
583
644
|
if (!parsed) {
|
|
584
645
|
errors.push({ id: candidate.id, title, error: 'AI returned no valid JSON' });
|
|
646
|
+
onProgress?.('refine:item-failed', {
|
|
647
|
+
candidateId: candidate.id, title,
|
|
648
|
+
error: 'AI returned no valid JSON',
|
|
649
|
+
current: processed, total: candidates.length,
|
|
650
|
+
progress: Math.round((processed / candidates.length) * 100),
|
|
651
|
+
});
|
|
585
652
|
continue;
|
|
586
653
|
}
|
|
587
654
|
|
|
588
655
|
if (options.dryRun) {
|
|
589
656
|
results.push({ id: candidate.id, title, preview: parsed });
|
|
657
|
+
onProgress?.('refine:item-completed', {
|
|
658
|
+
candidateId: candidate.id, title, refined: false,
|
|
659
|
+
current: processed, total: candidates.length,
|
|
660
|
+
progress: Math.round((processed / candidates.length) * 100),
|
|
661
|
+
refinedSoFar: refined,
|
|
662
|
+
});
|
|
590
663
|
continue;
|
|
591
664
|
}
|
|
592
665
|
|
|
@@ -624,9 +697,16 @@ export class CandidateService {
|
|
|
624
697
|
}
|
|
625
698
|
}
|
|
626
699
|
|
|
627
|
-
// 更新 code(如果 AI
|
|
700
|
+
// 更新 code(如果 AI 改写了文档 — 增强校验防止截断/片段代码/类型变更)
|
|
628
701
|
let newCode = candidate.code;
|
|
629
|
-
|
|
702
|
+
const origCode = candidate.code || '';
|
|
703
|
+
const origLen = origCode.length;
|
|
704
|
+
const isOrigMarkdown = /^---\s*\n/.test(origCode) || /^#\s+/.test(origCode) || (origCode.match(/^#{1,3}\s+/gm) || []).length >= 2;
|
|
705
|
+
const isNewMarkdown = parsed.code && (/^---\s*\n/.test(parsed.code) || /^#\s+/.test(parsed.code) || (parsed.code.match(/^#{1,3}\s+/gm) || []).length >= 2);
|
|
706
|
+
const codeTypeChanged = !isOrigMarkdown && isNewMarkdown;
|
|
707
|
+
if (parsed.code && parsed.code.length > 50 && parsed.code !== candidate.code
|
|
708
|
+
&& parsed.code.length >= origLen * 0.4 // 不能太短(防止 AI 返回截断片段)
|
|
709
|
+
&& !codeTypeChanged) { // 不允许源代码 → Markdown 类型变更
|
|
630
710
|
newCode = parsed.code;
|
|
631
711
|
changed = true;
|
|
632
712
|
}
|
|
@@ -645,11 +725,39 @@ export class CandidateService {
|
|
|
645
725
|
}
|
|
646
726
|
|
|
647
727
|
results.push({ id: candidate.id, title, refined: changed, fields: Object.keys(parsed) });
|
|
728
|
+
|
|
729
|
+
// 通知:当前候选完成
|
|
730
|
+
onProgress?.('refine:item-completed', {
|
|
731
|
+
candidateId: candidate.id,
|
|
732
|
+
title,
|
|
733
|
+
refined: changed,
|
|
734
|
+
current: processed,
|
|
735
|
+
total: candidates.length,
|
|
736
|
+
progress: Math.round((processed / candidates.length) * 100),
|
|
737
|
+
refinedSoFar: refined,
|
|
738
|
+
});
|
|
648
739
|
} catch (err) {
|
|
649
740
|
errors.push({ id: candidate.id, title: (candidate.metadata || {}).title, error: err.message });
|
|
741
|
+
|
|
742
|
+
// 通知:当前候选失败
|
|
743
|
+
onProgress?.('refine:item-failed', {
|
|
744
|
+
candidateId: candidate.id,
|
|
745
|
+
title,
|
|
746
|
+
error: err.message,
|
|
747
|
+
current: processed,
|
|
748
|
+
total: candidates.length,
|
|
749
|
+
progress: Math.round((processed / candidates.length) * 100),
|
|
750
|
+
});
|
|
650
751
|
}
|
|
651
752
|
}
|
|
652
753
|
|
|
754
|
+
// 通知:全部完成
|
|
755
|
+
onProgress?.('refine:completed', {
|
|
756
|
+
total: candidates.length,
|
|
757
|
+
refined,
|
|
758
|
+
failed: errors.length,
|
|
759
|
+
});
|
|
760
|
+
|
|
653
761
|
// 4. 审计
|
|
654
762
|
await this.auditLogger.log({
|
|
655
763
|
action: 'refine_bootstrap_candidates',
|
|
@@ -669,10 +777,47 @@ export class CandidateService {
|
|
|
669
777
|
_buildRefinePrompt(candidate, allTitles, userPrompt = '') {
|
|
670
778
|
const meta = candidate.metadata || {};
|
|
671
779
|
const otherTitles = allTitles.filter(t => t !== meta.title).map(t => ` - ${t}`).join('\n');
|
|
780
|
+
const code = (candidate.code || '');
|
|
781
|
+
|
|
782
|
+
// ── 检测 code 字段是源代码还是 Markdown 文档 ──
|
|
783
|
+
const isMarkdownDoc = /^---\s*\n/.test(code) // frontmatter
|
|
784
|
+
|| /^#\s+/.test(code) // 以 # 标题开头
|
|
785
|
+
|| (code.match(/^#{1,3}\s+/gm) || []).length >= 2; // 含 ≥2 个 Markdown 标题
|
|
786
|
+
const codeContentType = isMarkdownDoc ? 'markdown-document' : 'source-code';
|
|
787
|
+
|
|
788
|
+
// ── 用户指令段落 ──
|
|
789
|
+
let userSection = '';
|
|
790
|
+
if (userPrompt) {
|
|
791
|
+
userSection = `
|
|
792
|
+
# User Instructions (HIGHEST PRIORITY — STRICTLY follow these)
|
|
793
|
+
${userPrompt}
|
|
794
|
+
|
|
795
|
+
## Scope Restriction
|
|
796
|
+
- ONLY modify fields that are DIRECTLY related to the user instruction above.
|
|
797
|
+
- Do NOT touch or return any field that the user did not ask to change.
|
|
798
|
+
- For example, if user says "增加使用案例", ONLY return the field where usage examples belong (e.g. "code" for markdown docs, or "agentNotes" for source-code candidates). Do NOT return "summary", "confidence", "tags", "insight", or "relations" unless explicitly requested.
|
|
799
|
+
- If you are unsure which field a user instruction maps to, prefer "agentNotes" or "code" (for markdown docs).
|
|
800
|
+
`;
|
|
801
|
+
}
|
|
672
802
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
803
|
+
// ── code 字段的任务描述根据内容类型而不同 ──
|
|
804
|
+
let codeTask;
|
|
805
|
+
if (codeContentType === 'source-code') {
|
|
806
|
+
codeTask = `7. **code**: The content field contains RAW SOURCE CODE (not a Markdown document).
|
|
807
|
+
- Do NOT convert source code into a Markdown document — that breaks the candidate.
|
|
808
|
+
- If the user asks to add usage examples, documentation notes or explanations,
|
|
809
|
+
put them in "agentNotes" (array of strings) instead of modifying "code".
|
|
810
|
+
- Only modify "code" if the user explicitly asks to change the source code itself
|
|
811
|
+
(e.g. fix a bug, add comments, refactor). Return the COMPLETE source code.
|
|
812
|
+
- If no code change is needed, OMIT this field entirely.`;
|
|
813
|
+
} else {
|
|
814
|
+
codeTask = `7. **code**: The content field is a Markdown document.
|
|
815
|
+
- If improvements are needed, return the COMPLETE improved Markdown document.
|
|
816
|
+
- CRITICAL: You MUST return the ENTIRE document content, including ALL sections.
|
|
817
|
+
- Do NOT return only source code fragments or partial snippets.
|
|
818
|
+
- The code blocks inside the Markdown should contain PURE source code.
|
|
819
|
+
- If no code improvement is needed, OMIT this field entirely.`;
|
|
820
|
+
}
|
|
676
821
|
|
|
677
822
|
return `# Role
|
|
678
823
|
You are a senior software architect refining a Bootstrap knowledge candidate.\n${userSection}
|
|
@@ -683,10 +828,11 @@ Category: ${candidate.category || 'bootstrap'}
|
|
|
683
828
|
Language: ${candidate.language || 'unknown'}
|
|
684
829
|
Summary: ${meta.summary || '(none)'}
|
|
685
830
|
Tags: ${(meta.tags || []).join(', ')}
|
|
831
|
+
Content Type: ${codeContentType}
|
|
686
832
|
|
|
687
|
-
## Content
|
|
833
|
+
## Content
|
|
688
834
|
\`\`\`
|
|
689
|
-
${
|
|
835
|
+
${code.substring(0, 3000)}
|
|
690
836
|
\`\`\`
|
|
691
837
|
|
|
692
838
|
# Sibling Candidates (same bootstrap batch)
|
|
@@ -699,10 +845,10 @@ ${otherTitles || '(none)'}
|
|
|
699
845
|
4. **relations**: Array of cross-references to sibling candidates: [{ "type": "DEPENDS_ON|EXTENDS|RELATED|CONFLICTS|ENFORCES|PREREQUISITE", "target": "<exact title from siblings>", "description": "<why>" }]
|
|
700
846
|
5. **confidence**: Float 0-1 rating of this candidate's value (0.3=low, 0.6=medium, 0.9=high)
|
|
701
847
|
6. **tags**: Additional relevant tags (array of strings, optional)
|
|
702
|
-
|
|
848
|
+
${codeTask}
|
|
703
849
|
|
|
704
850
|
# Output
|
|
705
|
-
Return a single JSON object
|
|
851
|
+
Return a single JSON object. Only include fields you want to change.${userPrompt ? '\nREMINDER: Only return fields DIRECTLY related to the user instruction. Omit all others!' : ''}
|
|
706
852
|
Do NOT wrap in markdown code blocks. Return raw JSON only.`;
|
|
707
853
|
}
|
|
708
854
|
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AnalystAgent.js — v3.0 分析者 Agent
|
|
3
|
+
*
|
|
4
|
+
* 职责:
|
|
5
|
+
* - 使用 AST 工具 + 文件搜索工具自由探索代码库
|
|
6
|
+
* - 输出自然语言分析结果 (无格式约束)
|
|
7
|
+
* - 不提交候选、不关心格式
|
|
8
|
+
*
|
|
9
|
+
* 设计哲学:
|
|
10
|
+
* "给 AI 一个任务描述和一套好工具,让它像资深工程师一样自由探索代码库。"
|
|
11
|
+
*
|
|
12
|
+
* @module AnalystAgent
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { buildAnalysisReport, analysisQualityGate, buildRetryPrompt } from './HandoffProtocol.js';
|
|
16
|
+
import Logger from '../../infrastructure/logging/Logger.js';
|
|
17
|
+
|
|
18
|
+
// ──────────────────────────────────────────────────────────────────
|
|
19
|
+
// System Prompt — Analyst 专用 (~100 tokens)
|
|
20
|
+
// ──────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
const ANALYST_SYSTEM_PROMPT = `你是一名高级软件架构师,正在分析一个代码项目。
|
|
23
|
+
使用提供的工具深入探索代码结构,理解设计决策背后的原因。
|
|
24
|
+
输出你的分析发现,包括具体的文件路径和代码位置。
|
|
25
|
+
不需要任何特定格式,用自然语言描述你的理解即可。
|
|
26
|
+
尽可能多地使用工具来获取准确信息,不要猜测。`;
|
|
27
|
+
|
|
28
|
+
// ──────────────────────────────────────────────────────────────────
|
|
29
|
+
// Analyst 可用工具白名单 — 只做探索,不做提交
|
|
30
|
+
// ──────────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
const ANALYST_TOOLS = [
|
|
33
|
+
// AST 结构化分析
|
|
34
|
+
'get_project_overview',
|
|
35
|
+
'get_class_hierarchy',
|
|
36
|
+
'get_class_info',
|
|
37
|
+
'get_protocol_info',
|
|
38
|
+
'get_method_overrides',
|
|
39
|
+
'get_category_map',
|
|
40
|
+
// 文件访问
|
|
41
|
+
'search_project_code',
|
|
42
|
+
'read_project_file',
|
|
43
|
+
'list_project_structure',
|
|
44
|
+
'get_file_summary',
|
|
45
|
+
'semantic_search_code',
|
|
46
|
+
// 前序上下文 (可选)
|
|
47
|
+
'get_previous_analysis',
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
// ──────────────────────────────────────────────────────────────────
|
|
51
|
+
// Analyst 预算 — 自由探索,不需要 PhaseRouter
|
|
52
|
+
// ──────────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
const ANALYST_BUDGET = {
|
|
55
|
+
maxIterations: 20,
|
|
56
|
+
searchBudget: 15, // 探索为主,给更多搜索预算
|
|
57
|
+
searchBudgetGrace: 10,
|
|
58
|
+
maxSubmits: 0, // Analyst 不提交候选
|
|
59
|
+
softSubmitLimit: 0,
|
|
60
|
+
idleRoundsToExit: 3,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// ──────────────────────────────────────────────────────────────────
|
|
64
|
+
// 维度 Prompt 模板
|
|
65
|
+
// ──────────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 构建 Analyst Prompt
|
|
69
|
+
* @param {object} dimConfig — 维度配置 { id, label, guide, focusAreas }
|
|
70
|
+
* @param {object} projectInfo — { name, lang, fileCount }
|
|
71
|
+
* @returns {string}
|
|
72
|
+
*/
|
|
73
|
+
function buildAnalystPrompt(dimConfig, projectInfo) {
|
|
74
|
+
const parts = [];
|
|
75
|
+
|
|
76
|
+
// §1 任务描述
|
|
77
|
+
parts.push(`分析项目 ${projectInfo.name} (${projectInfo.lang}, ${projectInfo.fileCount} 个文件) 的 ${dimConfig.label}。`);
|
|
78
|
+
|
|
79
|
+
// §2 维度指引
|
|
80
|
+
if (dimConfig.guide) {
|
|
81
|
+
parts.push(dimConfig.guide);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// §3 探索焦点
|
|
85
|
+
if (dimConfig.focusAreas?.length > 0) {
|
|
86
|
+
parts.push(`重点关注:\n${dimConfig.focusAreas.map(f => `- ${f}`).join('\n')}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// §4 输出要求
|
|
90
|
+
const outputType = dimConfig.outputType || 'analysis';
|
|
91
|
+
const needsCandidates = outputType === 'dual' || outputType === 'candidate';
|
|
92
|
+
const depthHint = needsCandidates
|
|
93
|
+
? '你的分析将被转化为知识候选,请确保每个发现都有足够的代码证据和文件引用。目标: 发现 3-5 个独立的知识点。'
|
|
94
|
+
: '';
|
|
95
|
+
|
|
96
|
+
parts.push(`请将分析组织成结构化段落,包含:
|
|
97
|
+
1. 在哪些文件/类中发现 (写出具体文件路径)
|
|
98
|
+
2. 具体的实现方式和代码特征
|
|
99
|
+
3. 为什么选择这种方式(设计意图)
|
|
100
|
+
4. 统计数据 (如数量、占比)
|
|
101
|
+
|
|
102
|
+
每个关键发现用编号列表呈现,引用 3 个以上具体文件。
|
|
103
|
+
${depthHint}
|
|
104
|
+
重要: 务必使用 read_project_file 阅读代码确认,不要假设文件存在。引用的每个文件路径都必须是你亲眼看到的。`);
|
|
105
|
+
|
|
106
|
+
// §5 前序上下文提示
|
|
107
|
+
parts.push('可以调用 get_previous_analysis 获取前序维度的分析结果,避免重复分析。');
|
|
108
|
+
|
|
109
|
+
return parts.join('\n\n');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ──────────────────────────────────────────────────────────────────
|
|
113
|
+
// AnalystAgent 类
|
|
114
|
+
// ──────────────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
export class AnalystAgent {
|
|
117
|
+
/** @type {import('./ChatAgent.js').ChatAgent} */
|
|
118
|
+
#chatAgent;
|
|
119
|
+
|
|
120
|
+
/** @type {import('../../core/ast/ProjectGraph.js').default} */
|
|
121
|
+
#projectGraph;
|
|
122
|
+
|
|
123
|
+
/** @type {import('../../infrastructure/logging/Logger.js').default} */
|
|
124
|
+
#logger;
|
|
125
|
+
|
|
126
|
+
/** @type {number} Gate 最大重试次数 */
|
|
127
|
+
#maxRetries;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* @param {object} chatAgent — ChatAgent 实例
|
|
131
|
+
* @param {object} [projectGraph] — ProjectGraph 实例
|
|
132
|
+
* @param {object} [options]
|
|
133
|
+
* @param {number} [options.maxRetries=1] — Gate 失败最大重试次数
|
|
134
|
+
*/
|
|
135
|
+
constructor(chatAgent, projectGraph = null, options = {}) {
|
|
136
|
+
this.#chatAgent = chatAgent;
|
|
137
|
+
this.#projectGraph = projectGraph;
|
|
138
|
+
this.#logger = Logger.getInstance();
|
|
139
|
+
this.#maxRetries = options.maxRetries ?? 1;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 分析指定维度
|
|
144
|
+
*
|
|
145
|
+
* @param {object} dimConfig — 维度配置 { id, label, guide, focusAreas }
|
|
146
|
+
* @param {object} projectInfo — { name, lang, fileCount }
|
|
147
|
+
* @param {object} [options]
|
|
148
|
+
* @param {string} [options.sessionId] — Bootstrap session ID
|
|
149
|
+
* @returns {Promise<import('./HandoffProtocol.js').AnalysisReport>}
|
|
150
|
+
*/
|
|
151
|
+
async analyze(dimConfig, projectInfo, options = {}) {
|
|
152
|
+
const dimId = dimConfig.id;
|
|
153
|
+
const prompt = buildAnalystPrompt(dimConfig, projectInfo);
|
|
154
|
+
|
|
155
|
+
this.#logger.info(`[AnalystAgent] ▶ analyzing dimension "${dimId}" — prompt ${prompt.length} chars`);
|
|
156
|
+
|
|
157
|
+
let retries = 0;
|
|
158
|
+
let lastReport = null;
|
|
159
|
+
|
|
160
|
+
while (retries <= this.#maxRetries) {
|
|
161
|
+
const execPrompt = retries === 0
|
|
162
|
+
? prompt
|
|
163
|
+
: prompt + '\n\n' + buildRetryPrompt(lastReport?._gateReason || 'Analysis too short');
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const result = await this.#chatAgent.execute(execPrompt, {
|
|
167
|
+
source: 'system',
|
|
168
|
+
conversationId: options.sessionId ? `analyst-${options.sessionId}-${dimId}` : undefined,
|
|
169
|
+
budget: ANALYST_BUDGET,
|
|
170
|
+
systemPromptOverride: ANALYST_SYSTEM_PROMPT,
|
|
171
|
+
allowedTools: ANALYST_TOOLS,
|
|
172
|
+
disablePhaseRouter: true,
|
|
173
|
+
temperature: 0.4,
|
|
174
|
+
dimensionMeta: {
|
|
175
|
+
id: dimId,
|
|
176
|
+
outputType: 'analysis',
|
|
177
|
+
allowedKnowledgeTypes: dimConfig.allowedKnowledgeTypes || [],
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// 构建 AnalysisReport
|
|
182
|
+
const report = buildAnalysisReport(result, dimId, this.#projectGraph);
|
|
183
|
+
|
|
184
|
+
// 质量门控 — 传入 outputType 以调整门槛
|
|
185
|
+
const gate = analysisQualityGate(report, { outputType: dimConfig.outputType || 'analysis' });
|
|
186
|
+
if (gate.pass) {
|
|
187
|
+
this.#logger.info(`[AnalystAgent] ✅ dimension "${dimId}" — ${report.analysisText.length} chars, ${report.referencedFiles.length} files referenced, ${report.metadata.toolCallCount} tool calls`);
|
|
188
|
+
return report;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
this.#logger.warn(`[AnalystAgent] ⚠ Gate failed for "${dimId}": ${gate.reason} (action=${gate.action})`);
|
|
192
|
+
|
|
193
|
+
if (gate.action === 'degrade') {
|
|
194
|
+
// 直接降级 — 不重试
|
|
195
|
+
report._gateResult = gate;
|
|
196
|
+
return report;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// retry
|
|
200
|
+
lastReport = report;
|
|
201
|
+
lastReport._gateReason = gate.reason;
|
|
202
|
+
retries++;
|
|
203
|
+
} catch (err) {
|
|
204
|
+
this.#logger.error(`[AnalystAgent] ❌ dimension "${dimId}" error: ${err.message}`);
|
|
205
|
+
// 返回空 report
|
|
206
|
+
return buildAnalysisReport({ reply: '', toolCalls: [] }, dimId, this.#projectGraph);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// 重试耗尽 — 返回最后一次结果
|
|
211
|
+
this.#logger.warn(`[AnalystAgent] Retries exhausted for "${dimId}" — returning last report`);
|
|
212
|
+
return lastReport || buildAnalysisReport({ reply: '', toolCalls: [] }, dimId, this.#projectGraph);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export default AnalystAgent;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CandidateGuardrail.js — Producer 产出的候选验证链
|
|
3
|
+
*
|
|
4
|
+
* 三层验证:
|
|
5
|
+
* 1. 结构验证 — 必填字段、内容长度、knowledgeType 约束
|
|
6
|
+
* 2. 去重验证 — 标题不重复
|
|
7
|
+
* 3. 质量启发式 — 包含代码引用、项目特定内容
|
|
8
|
+
*
|
|
9
|
+
* @module CandidateGuardrail
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export class CandidateGuardrail {
|
|
13
|
+
/** @type {Set<string>} 已提交标题 (小写) */
|
|
14
|
+
#globalTitles;
|
|
15
|
+
|
|
16
|
+
/** @type {object} 维度配置 */
|
|
17
|
+
#dimensionConfig;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {Set<string>} globalTitles — 全局已提交标题集合 (小写)
|
|
21
|
+
* @param {object} dimensionConfig — { allowedKnowledgeTypes, id, outputType }
|
|
22
|
+
*/
|
|
23
|
+
constructor(globalTitles, dimensionConfig) {
|
|
24
|
+
this.#globalTitles = globalTitles;
|
|
25
|
+
this.#dimensionConfig = dimensionConfig;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 验证候选结构
|
|
30
|
+
* @param {object} candidate — submit_candidate 工具参数
|
|
31
|
+
* @returns {{ valid: boolean, error?: string }}
|
|
32
|
+
*/
|
|
33
|
+
validateStructure(candidate) {
|
|
34
|
+
// 必填字段检查
|
|
35
|
+
const required = ['title', 'code'];
|
|
36
|
+
for (const field of required) {
|
|
37
|
+
if (!candidate[field] || String(candidate[field]).trim().length === 0) {
|
|
38
|
+
return { valid: false, error: `缺少必填字段: ${field}` };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 内容长度检查 — 「项目特写」需要足够的代码和描述
|
|
43
|
+
const content = candidate.code || '';
|
|
44
|
+
if (content.length < 200) {
|
|
45
|
+
return { valid: false, error: `内容过短 (${content.length} 字符, 最少 200)。请包含代码片段和项目上下文描述,而非一句话概括。` };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// knowledgeType 约束
|
|
49
|
+
const allowed = this.#dimensionConfig.allowedKnowledgeTypes;
|
|
50
|
+
if (allowed?.length > 0 && candidate.knowledgeType) {
|
|
51
|
+
if (!allowed.includes(candidate.knowledgeType)) {
|
|
52
|
+
return { valid: false, error: `knowledgeType "${candidate.knowledgeType}" 不在允许列表: [${allowed}]` };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { valid: true };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 验证去重
|
|
61
|
+
* @param {object} candidate
|
|
62
|
+
* @returns {{ valid: boolean, error?: string }}
|
|
63
|
+
*/
|
|
64
|
+
validateUniqueness(candidate) {
|
|
65
|
+
const normalizedTitle = (candidate.title || '').toLowerCase().trim();
|
|
66
|
+
if (this.#globalTitles.has(normalizedTitle)) {
|
|
67
|
+
return { valid: false, error: `标题重复: "${candidate.title}"` };
|
|
68
|
+
}
|
|
69
|
+
return { valid: true };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 质量启发式检查
|
|
74
|
+
* @param {object} candidate
|
|
75
|
+
* @returns {{ valid: boolean, error?: string, warning?: string }}
|
|
76
|
+
*/
|
|
77
|
+
validateQuality(candidate) {
|
|
78
|
+
const content = candidate.code || '';
|
|
79
|
+
|
|
80
|
+
// 检查是否包含代码引用或文件路径
|
|
81
|
+
const hasCodeBlock = /```[\s\S]*?```/.test(content) || /\.(m|h|swift|js|ts)(:\d+)?/.test(content);
|
|
82
|
+
const hasSourceRef = /\(来源[::]/.test(content) || /\bFileName\b/.test(content) === false && /[A-Z]\w+\.(m|h|swift|java|kt|js|ts)/.test(content);
|
|
83
|
+
|
|
84
|
+
if (!hasCodeBlock && !hasSourceRef) {
|
|
85
|
+
return { valid: false, error: '内容缺少代码片段或文件引用 — 请用 read_project_file 获取代码后再提交,「项目特写」必须包含真实代码' };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 检查是否是 Skill 摘要式内容(一行式描述、无代码、无结构)
|
|
89
|
+
const lines = content.split('\n').filter(l => l.trim().length > 0);
|
|
90
|
+
if (lines.length <= 2 && !hasCodeBlock) {
|
|
91
|
+
return { valid: false, error: `内容过于简单 (仅 ${lines.length} 行) — 请包含代码片段、设计意图和项目上下文,不要只写一句话概括` };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 检查是否是通用知识而非项目特定
|
|
95
|
+
const genericPatterns = [
|
|
96
|
+
/^(Singleton|Factory|Observer|MVC|MVVM) (pattern|模式)$/i,
|
|
97
|
+
];
|
|
98
|
+
const title = candidate.title || '';
|
|
99
|
+
if (genericPatterns.some(p => p.test(title.trim()))) {
|
|
100
|
+
return { valid: false, error: `标题过于通用: "${title}" — 请加上项目特定的上下文` };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { valid: true };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 完整验证链
|
|
108
|
+
* @param {object} candidate
|
|
109
|
+
* @returns {{ valid: boolean, error?: string, warning?: string }}
|
|
110
|
+
*/
|
|
111
|
+
validate(candidate) {
|
|
112
|
+
const structureResult = this.validateStructure(candidate);
|
|
113
|
+
if (!structureResult.valid) return structureResult;
|
|
114
|
+
|
|
115
|
+
const uniqueResult = this.validateUniqueness(candidate);
|
|
116
|
+
if (!uniqueResult.valid) return uniqueResult;
|
|
117
|
+
|
|
118
|
+
const qualityResult = this.validateQuality(candidate);
|
|
119
|
+
// 质量问题返回 warning 但不阻止提交
|
|
120
|
+
if (!qualityResult.valid) return qualityResult;
|
|
121
|
+
|
|
122
|
+
return { valid: true, warning: qualityResult.warning };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 记录已提交标题(提交成功后调用)
|
|
127
|
+
* @param {string} title
|
|
128
|
+
*/
|
|
129
|
+
recordTitle(title) {
|
|
130
|
+
this.#globalTitles.add((title || '').toLowerCase().trim());
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export default CandidateGuardrail;
|