autosnippet 3.2.7 → 3.2.9

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 (147) hide show
  1. package/bin/cli.js +13 -5
  2. package/dashboard/dist/assets/index-BTAsOZv2.js +128 -0
  3. package/dashboard/dist/assets/index-C_72Ct98.css +1 -0
  4. package/dashboard/dist/index.html +2 -2
  5. package/lib/cli/AiScanService.js +26 -29
  6. package/lib/cli/SetupService.js +1 -1
  7. package/lib/core/AstAnalyzer.js +27 -5
  8. package/lib/core/analysis/CallEdgeResolver.js +402 -0
  9. package/lib/core/analysis/CallGraphAnalyzer.js +367 -0
  10. package/lib/core/analysis/CallSiteExtractor.js +629 -0
  11. package/lib/core/analysis/DataFlowInferrer.js +57 -0
  12. package/lib/core/analysis/ImportPathResolver.js +189 -0
  13. package/lib/core/analysis/ImportRecord.js +105 -0
  14. package/lib/core/analysis/SymbolTableBuilder.js +211 -0
  15. package/lib/core/ast/ProjectGraph.js +8 -0
  16. package/lib/core/ast/lang-dart.js +352 -5
  17. package/lib/core/ast/lang-go.js +212 -10
  18. package/lib/core/ast/lang-java.js +205 -1
  19. package/lib/core/ast/lang-kotlin.js +330 -1
  20. package/lib/core/ast/lang-python.js +31 -2
  21. package/lib/core/ast/lang-rust.js +284 -3
  22. package/lib/core/ast/lang-swift.js +180 -1
  23. package/lib/core/ast/lang-typescript.js +290 -1
  24. package/lib/core/discovery/index.js +2 -2
  25. package/lib/external/ai/AiProvider.js +66 -172
  26. package/lib/external/ai/providers/GoogleGeminiProvider.js +23 -1
  27. package/lib/external/mcp/McpServer.js +1 -0
  28. package/lib/external/mcp/handlers/bootstrap/BootstrapSession.js +1 -1
  29. package/lib/external/mcp/handlers/bootstrap/ExternalSubmissionTracker.js +3 -3
  30. package/lib/external/mcp/handlers/bootstrap/MissionBriefingBuilder.js +22 -1
  31. package/lib/external/mcp/handlers/bootstrap/pipeline/IncrementalBootstrap.js +1 -1
  32. package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-configs.js +2 -1
  33. package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-context.js +8 -8
  34. package/lib/external/mcp/handlers/bootstrap/pipeline/noAiFallback.js +1 -1
  35. package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +311 -162
  36. package/lib/external/mcp/handlers/bootstrap/shared/bootstrap-phases.js +102 -7
  37. package/lib/external/mcp/handlers/bootstrap/shared/dimension-sop.js +1 -1
  38. package/lib/external/mcp/handlers/bootstrap-external.js +9 -2
  39. package/lib/external/mcp/handlers/bootstrap-internal.js +19 -8
  40. package/lib/external/mcp/handlers/consolidated.js +9 -0
  41. package/lib/external/mcp/handlers/dimension-complete-external.js +6 -6
  42. package/lib/external/mcp/handlers/guard.js +3 -3
  43. package/lib/external/mcp/handlers/structure.js +62 -0
  44. package/lib/external/mcp/handlers/wiki-external.js +66 -3
  45. package/lib/external/mcp/tools.js +36 -1
  46. package/lib/http/HttpServer.js +1 -1
  47. package/lib/http/middleware/requestLogger.js +1 -0
  48. package/lib/http/routes/ai.js +240 -35
  49. package/lib/http/routes/candidates.js +2 -3
  50. package/lib/http/routes/extract.js +13 -11
  51. package/lib/http/routes/modules.js +2 -2
  52. package/lib/http/routes/recipes.js +9 -5
  53. package/lib/http/routes/remote.js +149 -270
  54. package/lib/http/routes/violations.js +0 -54
  55. package/lib/http/utils/sse-sessions.js +1 -1
  56. package/lib/infrastructure/logging/Logger.js +5 -4
  57. package/lib/infrastructure/monitoring/PerformanceMonitor.js +3 -2
  58. package/lib/injection/ServiceContainer.js +70 -28
  59. package/lib/platform/ScreenCaptureService.js +177 -0
  60. package/lib/platform/ios/index.js +2 -2
  61. package/lib/platform/ios/routes/spm.js +2 -2
  62. package/lib/platform/ios/spm/PackageSwiftParser.js +14 -3
  63. package/lib/platform/ios/spm/SpmDiscoverer.js +123 -17
  64. package/lib/platform/ios/spm/{SpmService.js → SpmHelper.js} +43 -675
  65. package/lib/platform/ios/xcode/XcodeWriteUtils.js +1 -1
  66. package/lib/service/agent/AgentEventBus.js +207 -0
  67. package/lib/service/agent/AgentFactory.js +490 -0
  68. package/lib/service/agent/AgentMessage.js +240 -0
  69. package/lib/service/agent/AgentRouter.js +228 -0
  70. package/lib/service/agent/AgentRuntime.js +1016 -0
  71. package/lib/service/agent/AgentState.js +217 -0
  72. package/lib/service/agent/IntentClassifier.js +331 -0
  73. package/lib/service/agent/LarkTransport.js +389 -0
  74. package/lib/service/agent/capabilities.js +408 -0
  75. package/lib/service/{chat → agent/context}/ContextWindow.js +37 -12
  76. package/lib/service/{chat → agent/context}/ExplorationTracker.js +77 -22
  77. package/lib/service/{chat → agent/core}/ChatAgentPrompts.js +14 -2
  78. package/lib/service/agent/core/LoopContext.js +170 -0
  79. package/lib/service/agent/core/MessageAdapter.js +223 -0
  80. package/lib/service/agent/core/ToolExecutionPipeline.js +376 -0
  81. package/lib/service/{chat → agent/domain}/ChatAgentTasks.js +19 -98
  82. package/lib/service/{chat → agent/domain}/EpisodicConsolidator.js +7 -7
  83. package/lib/service/{chat → agent/domain}/EvidenceCollector.js +4 -2
  84. package/lib/service/{chat/AnalystAgent.js → agent/domain/insight-analyst.js} +37 -172
  85. package/lib/service/{chat/HandoffProtocol.js → agent/domain/insight-gate.js} +91 -123
  86. package/lib/service/agent/domain/insight-producer.js +267 -0
  87. package/lib/service/agent/domain/scan-prompts.js +105 -0
  88. package/lib/service/agent/forced-summary.js +266 -0
  89. package/lib/service/agent/index.js +91 -0
  90. package/lib/service/{chat → agent}/memory/ActiveContext.js +3 -1
  91. package/lib/service/{chat → agent}/memory/MemoryCoordinator.js +7 -7
  92. package/lib/service/{chat/ProjectSemanticMemory.js → agent/memory/PersistentMemory.js} +359 -89
  93. package/lib/service/{chat → agent}/memory/SessionStore.js +5 -4
  94. package/lib/service/{chat → agent}/memory/index.js +1 -1
  95. package/lib/service/agent/policies.js +442 -0
  96. package/lib/service/agent/presets.js +303 -0
  97. package/lib/service/agent/strategies.js +717 -0
  98. package/lib/service/{chat → agent/tools}/ToolRegistry.js +3 -3
  99. package/lib/service/agent/tools/ai-analysis.js +75 -0
  100. package/lib/service/{chat → agent}/tools/ast-graph.js +229 -32
  101. package/lib/service/{chat → agent}/tools/composite.js +2 -1
  102. package/lib/service/{chat → agent}/tools/guard.js +1 -121
  103. package/lib/service/{chat → agent}/tools/index.js +33 -22
  104. package/lib/service/{chat → agent}/tools/infrastructure.js +6 -1
  105. package/lib/service/agent/tools/knowledge-graph.js +112 -0
  106. package/lib/service/agent/tools/scan-recipe.js +189 -0
  107. package/lib/service/agent/tools/system-interaction.js +476 -0
  108. package/lib/service/automation/DirectiveDetector.js +0 -1
  109. package/lib/service/automation/FileWatcher.js +0 -8
  110. package/lib/service/automation/handlers/CreateHandler.js +7 -3
  111. package/lib/service/automation/handlers/DraftHandler.js +7 -6
  112. package/lib/service/cursor/CursorDeliveryPipeline.js +167 -1
  113. package/lib/service/knowledge/CodeEntityGraph.js +327 -2
  114. package/lib/service/knowledge/KnowledgeService.js +5 -1
  115. package/lib/service/module/ModuleService.js +49 -73
  116. package/lib/service/skills/SignalCollector.js +26 -19
  117. package/lib/service/snippet/codecs/VSCodeCodec.js +1 -1
  118. package/lib/service/wiki/WikiGenerator.js +1 -1
  119. package/lib/shared/FieldSpec.js +1 -1
  120. package/lib/shared/PathGuard.js +1 -1
  121. package/lib/shared/StyleGuide.js +1 -1
  122. package/package.json +4 -1
  123. package/resources/native-ui/screenshot.swift +228 -0
  124. package/dashboard/dist/assets/index-BaGY7kJI.css +0 -1
  125. package/dashboard/dist/assets/index-DfHY_3ln.js +0 -128
  126. package/lib/core/discovery/SpmDiscoverer.js +0 -5
  127. package/lib/external/mcp/handlers/bootstrap/pipeline/EpisodicMemory.js +0 -749
  128. package/lib/external/mcp/handlers/bootstrap/pipeline/ToolResultCache.js +0 -277
  129. package/lib/http/routes/spm.js +0 -5
  130. package/lib/infrastructure/external/XcodeAutomation.js +0 -15
  131. package/lib/service/chat/ChatAgent.js +0 -1602
  132. package/lib/service/chat/Memory.js +0 -161
  133. package/lib/service/chat/ProducerAgent.js +0 -431
  134. package/lib/service/chat/ReasoningTrace.js +0 -523
  135. package/lib/service/chat/TaskPipeline.js +0 -357
  136. package/lib/service/chat/WorkingMemory.js +0 -357
  137. package/lib/service/chat/memory/PersistentMemory.js +0 -450
  138. package/lib/service/chat/tools/ai-analysis.js +0 -267
  139. package/lib/service/chat/tools/knowledge-graph.js +0 -234
  140. package/lib/service/chat/tools.js +0 -18
  141. package/lib/service/snippet/PlaceholderConverter.js +0 -5
  142. package/lib/service/snippet/codecs/XcodeCodec.js +0 -5
  143. /package/lib/service/{chat → agent}/ConversationStore.js +0 -0
  144. /package/lib/service/{chat → agent}/tools/_shared.js +0 -0
  145. /package/lib/service/{chat → agent}/tools/lifecycle.js +0 -0
  146. /package/lib/service/{chat → agent}/tools/project-access.js +0 -0
  147. /package/lib/service/{chat → agent}/tools/query.js +0 -0
@@ -1,161 +0,0 @@
1
- /**
2
- * @deprecated Phase 5: 已由 PersistentMemory 取代。
3
- * 数据可通过 `PersistentMemory.migrateFromLegacy(projectRoot)` 迁移到 SQLite。
4
- * 新代码请使用 `import { PersistentMemory } from './memory/PersistentMemory.js'`
5
- *
6
- * Memory — 跨对话轻量记忆(带 source 隔离 + 去重)
7
- *
8
- * 设计:
9
- * - JSONL 文件存储,每行一条记忆
10
- * - 支持 TTL 自动过期
11
- * - 上限 maxEntries,超出时截断旧条目
12
- * - 读写均做静默降级(Memory 是增强,不是核心路径)
13
- * - source 标签: 'user'(用户对话) / 'system'(SignalCollector 等后台)
14
- * - 去重: 相同 type+content 的记忆不重复写入
15
- *
16
- * 记忆类型:
17
- * - preference: 用户偏好("我们不用 singleton"、"以后用 DI")
18
- * - decision: 关键决策("Network 模块审核通过")
19
- * - context: 项目上下文("主语言是 Swift,使用 SPM")
20
- *
21
- * 隔离规则:
22
- * toPromptSection({ source }) 可按 source 过滤
23
- * 用户对话只看 source=user 的记忆
24
- * 系统分析可看全部记忆
25
- *
26
- * 文件路径: .autosnippet/memory.jsonl
27
- */
28
-
29
- import fs from 'node:fs';
30
- import path from 'node:path';
31
- import pathGuard from '../../shared/PathGuard.js';
32
-
33
- export class Memory {
34
- #filePath;
35
- #maxEntries;
36
-
37
- /**
38
- * @param {string} projectRoot — 用户项目根目录
39
- * @param {object} [opts]
40
- * @param {number} [opts.maxEntries=50] — 最大记忆条数
41
- */
42
- constructor(projectRoot, { maxEntries = 50 } = {}) {
43
- this.#filePath = path.join(projectRoot, '.autosnippet', 'memory.jsonl');
44
- this.#maxEntries = maxEntries;
45
- // 路径安全检查
46
- pathGuard.assertProjectWriteSafe(this.#filePath);
47
- }
48
-
49
- /**
50
- * 读取最近 N 条记忆,过滤过期项
51
- * @param {number} [limit=20]
52
- * @param {object} [opts]
53
- * @param {'user'|'system'} [opts.source] — 按 source 过滤
54
- * @returns {{ ts: string, type: string, content: string, source?: string, ttl?: number }[]}
55
- */
56
- load(limit = 20, { source } = {}) {
57
- try {
58
- if (!fs.existsSync(this.#filePath)) {
59
- return [];
60
- }
61
- const raw = fs.readFileSync(this.#filePath, 'utf-8').trim();
62
- if (!raw) {
63
- return [];
64
- }
65
- const lines = raw.split('\n').filter(Boolean);
66
- const now = Date.now();
67
- return lines
68
- .map((l) => {
69
- try {
70
- return JSON.parse(l);
71
- } catch {
72
- return null;
73
- }
74
- })
75
- .filter(Boolean)
76
- .filter((m) => !m.ttl || now - new Date(m.ts).getTime() < m.ttl * 86400000)
77
- .filter((m) => !source || (m.source || 'user') === source)
78
- .slice(-limit);
79
- } catch {
80
- return [];
81
- }
82
- }
83
-
84
- /**
85
- * 追加一条记忆(自动去重)
86
- * @param {{ type: string, content: string, source?: string, ttl?: number }} entry
87
- */
88
- append(entry) {
89
- try {
90
- // 去重: 检查是否已有相同 type+content 的记忆
91
- const existing = this.load(this.#maxEntries);
92
- const normalizedContent = (entry.content || '').trim().substring(0, 200);
93
- const isDuplicate = existing.some(
94
- (m) => m.type === entry.type && m.content === normalizedContent
95
- );
96
- if (isDuplicate) {
97
- return;
98
- }
99
-
100
- const dir = path.dirname(this.#filePath);
101
- fs.mkdirSync(dir, { recursive: true });
102
- const line = JSON.stringify({
103
- ts: new Date().toISOString(),
104
- source: entry.source || 'user',
105
- ...entry,
106
- content: normalizedContent,
107
- });
108
- fs.appendFileSync(this.#filePath, `${line}\n`, 'utf-8');
109
- this.#compact();
110
- } catch {
111
- /* write failure non-critical */
112
- }
113
- }
114
-
115
- /**
116
- * 生成供系统提示词的记忆摘要
117
- *
118
- * @param {object} [opts]
119
- * @param {'user'|'system'} [opts.source] — 只包含指定 source 的记忆
120
- * - 用户对话建议传 'user' 避免系统分析记忆污染
121
- * - 后台分析可传 undefined 获取全部
122
- * @returns {string}
123
- */
124
- toPromptSection({ source } = {}) {
125
- const memories = this.load(20, { source });
126
- if (memories.length === 0) {
127
- return '';
128
- }
129
- const lines = memories.map((m) => `- [${m.type}] ${m.content}`).join('\n');
130
- return `\n## 历史记忆\n以下是之前对话中积累的项目偏好和决策,请参考:\n${lines}\n`;
131
- }
132
-
133
- /**
134
- * 当前记忆条数
135
- * @param {object} [opts]
136
- * @param {'user'|'system'} [opts.source]
137
- */
138
- size({ source } = {}) {
139
- return this.load(this.#maxEntries, { source }).length;
140
- }
141
-
142
- /**
143
- * 超过 maxEntries 时截断旧条目
144
- */
145
- #compact() {
146
- try {
147
- const raw = fs.readFileSync(this.#filePath, 'utf-8').trim();
148
- if (!raw) {
149
- return;
150
- }
151
- const lines = raw.split('\n').filter(Boolean);
152
- if (lines.length > this.#maxEntries) {
153
- fs.writeFileSync(this.#filePath, `${lines.slice(-this.#maxEntries).join('\n')}\n`, 'utf-8');
154
- }
155
- } catch {
156
- /* ignore */
157
- }
158
- }
159
- }
160
-
161
- export default Memory;
@@ -1,431 +0,0 @@
1
- /**
2
- * ProducerAgent.js — v3.0 生产者 Agent
3
- *
4
- * 职责:
5
- * - 将 Analyst 的分析文本转换为结构化的 submit_knowledge 调用
6
- * - 遵循 PROJECT_SNAPSHOT_STYLE_GUIDE 格式
7
- * - 使用 read_project_file 获取代码片段
8
- * - UnifiedValidator 验证每次提交(替代已废弃的 CandidateGuardrail)
9
- *
10
- * 设计哲学:
11
- * "像编辑把记者的手稿变成标准格式的文章。"
12
- *
13
- * @module ProducerAgent
14
- */
15
-
16
- import Logger from '../../infrastructure/logging/Logger.js';
17
- import { buildProducerStyleGuide, SUBMIT_REQUIREMENTS } from '../../shared/StyleGuide.js';
18
-
19
- // ──────────────────────────────────────────────────────────────────
20
- // System Prompt — Producer 专用 (~150 tokens)
21
- // ──────────────────────────────────────────────────────────────────
22
-
23
- const PRODUCER_SYSTEM_PROMPT = `你是知识管理专家。你会收到一段代码分析文本,需要将其中的知识点转化为结构化的知识候选。
24
-
25
- 核心原则: 分析文本已经包含了所有发现,你的唯一工作是将它们格式化为 submit_knowledge 调用。
26
-
27
- 每个候选必须:
28
- 1. 有清晰的标题 (描述知识点的核心,使用项目真实类名)
29
- 2. 有项目特写风格的正文 (content.markdown 字段,结合代码展示)
30
- 3. 标注相关文件路径
31
- 4. 选择正确的 kind (rule/pattern/fact)
32
- 5. 提供完整的 Cursor 交付字段 (trigger, doClause, whenClause 等)
33
-
34
- 工作流程:
35
- 1. 阅读分析文本,识别每个独立的知识点/发现
36
- 2. 用 read_project_file 批量获取关键代码片段:
37
- read_project_file({ filePaths: ["FileA.m", "FileB.m"], maxLines: 80 })
38
- 3. 立刻调用 submit_knowledge 提交
39
- 4. 重复直到分析中的所有知识点都已提交
40
-
41
- 关键规则:
42
- - 分析中的每个要点/段落都应转化为至少一个候选
43
- - read_project_file 支持 filePaths 数组批量读取多个文件,一次调用完成
44
- - read_project_file 时读取足够多的行数(startLine + maxLines 至少 30 行)
45
- - reasoning.sources 必须是非空数组,填写相关文件路径如 ["FileName.m"]
46
- - 如果分析提到了 3 个模式,就应该提交 3 个候选,不要合并
47
- - 禁止: 不要搜索新文件、不要做额外分析,专注于格式化和提交
48
-
49
- 容错规则:
50
- - 如果 read_project_file 返回"文件不存在"或错误,不要重试同一文件的其他路径变体
51
- - 文件读取失败时,直接使用分析文本中已有的代码和描述来提交候选
52
- - 永远不要因为文件读取失败而跳过知识点 — 分析文本已经包含足够信息
53
- - 先提交候选,再考虑是否需要读取更多代码(提交优先于验证)`;
54
-
55
- // ──────────────────────────────────────────────────────────────────
56
- // Producer 可用工具白名单 — 只做格式化和提交
57
- // ──────────────────────────────────────────────────────────────────
58
-
59
- const PRODUCER_TOOLS = ['submit_knowledge', 'submit_with_check', 'read_project_file'];
60
-
61
- // ──────────────────────────────────────────────────────────────────
62
- // Producer 预算 — 格式化任务,迭代更少
63
- // ──────────────────────────────────────────────────────────────────
64
-
65
- const PRODUCER_BUDGET = {
66
- maxIterations: 24, // was 18 — 与 Analyst 统一
67
- searchBudget: 4,
68
- searchBudgetGrace: 3,
69
- maxSubmits: 10,
70
- softSubmitLimit: 10,
71
- idleRoundsToExit: 3,
72
- };
73
-
74
- // ──────────────────────────────────────────────────────────────────
75
- // 项目特写风格指南 (从共享 StyleGuide.js 获取)
76
- // ──────────────────────────────────────────────────────────────────
77
-
78
- const STYLE_GUIDE = buildProducerStyleGuide();
79
-
80
- // ──────────────────────────────────────────────────────────────────
81
- // Prompt 构建
82
- // ──────────────────────────────────────────────────────────────────
83
-
84
- /** 提交要求 (从共享 StyleGuide.js 获取) */
85
- // SUBMIT_REQUIREMENTS 已经从 import 获取
86
-
87
- /**
88
- * 构建 Producer Prompt (v1 — 用于 AnalysisReport)
89
- *
90
- * @param {import('./HandoffProtocol.js').AnalysisReport} analysisReport
91
- * @param {object} dimConfig — { id, label, allowedKnowledgeTypes, outputType }
92
- * @param {object} projectInfo — { name }
93
- * @returns {string}
94
- */
95
- function buildProducerPrompt(analysisReport, dimConfig, projectInfo) {
96
- const parts = [];
97
-
98
- // §1 任务描述
99
- parts.push(`将以下对 ${projectInfo.name} 项目 "${dimConfig.label}" 维度的分析,转化为知识候选:`);
100
-
101
- // §2 分析内容
102
- parts.push(`---\n${analysisReport.analysisText}\n---`);
103
-
104
- // §3 引用文件
105
- if (analysisReport.referencedFiles.length > 0) {
106
- parts.push(`分析中引用的关键文件: ${analysisReport.referencedFiles.join(', ')}`);
107
- }
108
-
109
- // §4 维度约束
110
- parts.push(`维度约束:
111
- - dimensionId: ${dimConfig.id}
112
- - 允许的 knowledgeType: ${(dimConfig.allowedKnowledgeTypes || []).join(', ') || '(all)'}
113
- - category: ${dimConfig.id}`);
114
-
115
- // §5 写作指南 (只注入一次)
116
- parts.push(STYLE_GUIDE);
117
-
118
- // §6 提交要求
119
- parts.push(SUBMIT_REQUIREMENTS);
120
-
121
- return parts.join('\n\n');
122
- }
123
-
124
- /**
125
- * 构建 Producer Prompt v2 — 用于 AnalysisArtifact
126
- *
127
- * 相比 v1 增加:
128
- * - §3 结构化发现 (findings): 每个发现应转化为至少一个候选
129
- * - §4 代码证据: Analyst 已读取的关键代码片段, Producer 无需重新 read_file
130
- * - §5 负空间信号: 搜索但未找到的模式, 告知 Producer 不要猜测
131
- *
132
- * @param {import('./HandoffProtocol.js').AnalysisArtifact} artifact
133
- * @param {object} dimConfig
134
- * @param {object} projectInfo
135
- * @returns {string}
136
- */
137
- function buildProducerPromptV2(artifact, dimConfig, projectInfo) {
138
- const parts = [];
139
-
140
- // §1 任务描述
141
- parts.push(`将以下对 ${projectInfo.name} 项目 "${dimConfig.label}" 维度的分析,转化为知识候选:`);
142
-
143
- // §2 分析内容
144
- parts.push(`---\n${artifact.analysisText}\n---`);
145
-
146
- // §3 结构化发现 (来自 ActiveContext note_finding)
147
- if (artifact.findings?.length > 0) {
148
- const findingLines = ['## 关键发现 (Analyst 已确认)'];
149
- const sorted = [...artifact.findings].sort((a, b) => b.importance - a.importance);
150
- for (const f of sorted) {
151
- const badge = f.importance >= 8 ? '⚠️' : '📋';
152
- findingLines.push(`${badge} **[${f.importance}/10]** ${f.finding}`);
153
- if (f.evidence) {
154
- findingLines.push(` 证据: ${f.evidence}`);
155
- }
156
- }
157
- findingLines.push('');
158
- findingLines.push('☝️ 上述每个发现都应至少转化为一个候选。');
159
- parts.push(findingLines.join('\n'));
160
- }
161
-
162
- // §4 代码证据 (NEW — 杀手级改进: Producer 无需重新 read_file)
163
- const codeContext = buildCodeContextSection(artifact.evidenceMap);
164
- if (codeContext) {
165
- parts.push(codeContext);
166
- }
167
-
168
- // §5 负空间信号 (NEW — 告知 Producer 哪些模式不存在)
169
- if (artifact.negativeSignals?.length > 0) {
170
- const nsLines = ['## ⛔ 不存在的模式 (不要猜测)'];
171
- for (const ns of artifact.negativeSignals.slice(0, 5)) {
172
- nsLines.push(`- "${ns.searchPattern}" — ${ns.implication}`);
173
- }
174
- parts.push(nsLines.join('\n'));
175
- }
176
-
177
- // §6 引用文件 (简化 — evidenceMap 已包含更丰富的信息)
178
- if (artifact.referencedFiles.length > 0) {
179
- parts.push(`分析中引用的关键文件: ${artifact.referencedFiles.slice(0, 15).join(', ')}`);
180
- }
181
-
182
- // §7 维度约束
183
- parts.push(`维度约束:
184
- - dimensionId: ${dimConfig.id}
185
- - 允许的 knowledgeType: ${(dimConfig.allowedKnowledgeTypes || []).join(', ') || '(all)'}
186
- - category: ${dimConfig.id}`);
187
-
188
- // §8 写作指南 + 提交要求
189
- parts.push(STYLE_GUIDE);
190
- parts.push(SUBMIT_REQUIREMENTS);
191
-
192
- return parts.join('\n\n');
193
- }
194
-
195
- // ──────────────────────────────────────────────────────────────────
196
- // 代码上下文注入 (Producer v2 辅助)
197
- // ──────────────────────────────────────────────────────────────────
198
-
199
- /**
200
- * 从 evidenceMap 构建代码上下文段
201
- *
202
- * 策略: 按代码片段数量排序 (更多片段 = Analyst 更关注此文件)
203
- * 预算: ≤ 4000 chars (约 1000 tokens)
204
- *
205
- * @param {Map<string, import('./EvidenceCollector.js').EvidenceEntry>} evidenceMap
206
- * @returns {string|null}
207
- */
208
- function buildCodeContextSection(evidenceMap) {
209
- if (!evidenceMap || evidenceMap.size === 0) return null;
210
-
211
- const parts = ['## 📄 Analyst 已读取的代码 (直接引用, 无需 read_file)'];
212
- let totalChars = 0;
213
- const BUDGET = 4000;
214
-
215
- // 按代码片段数量排序
216
- const sortedEntries = [...evidenceMap.values()]
217
- .filter((e) => e.codeSnippets.length > 0)
218
- .sort((a, b) => b.codeSnippets.length - a.codeSnippets.length);
219
-
220
- for (const entry of sortedEntries) {
221
- if (totalChars >= BUDGET) break;
222
-
223
- const header = `### ${entry.filePath}${entry.role ? ` (${entry.role})` : ''}`;
224
- parts.push(header);
225
- totalChars += header.length;
226
-
227
- if (entry.summary) {
228
- parts.push(entry.summary);
229
- totalChars += entry.summary.length;
230
- }
231
-
232
- for (const snippet of entry.codeSnippets.slice(0, 2)) {
233
- if (totalChars >= BUDGET) break;
234
- const codeBlock = `\`\`\`\n// L${snippet.startLine}-${snippet.endLine}\n${snippet.content}\n\`\`\``;
235
- if (snippet.analystNote) {
236
- parts.push(`> ${snippet.analystNote}`);
237
- totalChars += snippet.analystNote.length + 4;
238
- }
239
- parts.push(codeBlock);
240
- totalChars += codeBlock.length;
241
- }
242
- }
243
-
244
- return parts.length > 1 ? parts.join('\n') : null;
245
- }
246
-
247
- // ──────────────────────────────────────────────────────────────────
248
- // ProducerAgent 类
249
- // ──────────────────────────────────────────────────────────────────
250
-
251
- export class ProducerAgent {
252
- /** @type {import('./ChatAgent.js').ChatAgent} */
253
- #chatAgent;
254
-
255
- /** @type {import('../../infrastructure/logging/Logger.js').default} */
256
- #logger;
257
-
258
- /**
259
- * @param {object} chatAgent — ChatAgent 实例
260
- */
261
- constructor(chatAgent) {
262
- this.#chatAgent = chatAgent;
263
- this.#logger = Logger.getInstance();
264
- }
265
-
266
- /**
267
- * 将分析报告转化为候选
268
- *
269
- * @param {import('./HandoffProtocol.js').AnalysisReport} analysisReport — Analyst 产出
270
- * @param {object} dimConfig — 维度配置
271
- * @param {object} projectInfo — { name, lang, fileCount }
272
- * @param {object} [options]
273
- * @param {string} [options.sessionId]
274
- * @param {object} [options.budget] — 覆盖默认预算
275
- * @returns {Promise<ProducerResult>}
276
- */
277
- async produce(analysisReport, dimConfig, projectInfo, options = {}) {
278
- const dimId = dimConfig.id;
279
-
280
- // 分析文本为空时直接跳过
281
- if (!analysisReport.analysisText || analysisReport.analysisText.length < 50) {
282
- this.#logger.warn(`[ProducerAgent] ⚠ empty analysis for "${dimId}" — skipping`);
283
- return { candidateCount: 0, toolCalls: [], reply: '' };
284
- }
285
-
286
- // 自动检测: AnalysisArtifact (v2) vs AnalysisReport (v1)
287
- const isArtifact = !!(analysisReport.evidenceMap || analysisReport.findings);
288
- const prompt = isArtifact
289
- ? buildProducerPromptV2(analysisReport, dimConfig, projectInfo)
290
- : buildProducerPrompt(analysisReport, dimConfig, projectInfo);
291
- this.#logger.info(
292
- `[ProducerAgent] ▶ producing candidates for "${dimId}" — prompt ${prompt.length} chars${isArtifact ? ' (v2 artifact)' : ''}`
293
- );
294
-
295
- const budget = options.budget
296
- ? { ...PRODUCER_BUDGET, ...options.budget }
297
- : { ...PRODUCER_BUDGET };
298
-
299
- try {
300
- const result = await this.#chatAgent.execute(prompt, {
301
- source: 'system',
302
- conversationId: options.sessionId ? `producer-${options.sessionId}-${dimId}` : undefined,
303
- budget,
304
- systemPromptOverride: PRODUCER_SYSTEM_PROMPT,
305
- allowedTools: PRODUCER_TOOLS,
306
- strategy: 'producer',
307
- temperature: 0.3,
308
- projectLanguage: projectInfo.lang,
309
- // v5.0: 统一 MemoryCoordinator (修复 B2: Producer 获得 WM distilled context)
310
- memoryCoordinator: options.memoryCoordinator || undefined,
311
- dimensionMeta: {
312
- id: dimId,
313
- outputType: dimConfig.outputType || 'candidate',
314
- allowedKnowledgeTypes: dimConfig.allowedKnowledgeTypes || [],
315
- },
316
- });
317
-
318
- // 统计提交 (区分成功/失败)
319
- const submitCalls = (result.toolCalls || []).filter(
320
- (tc) =>
321
- (tc.tool || tc.name) === 'submit_knowledge' ||
322
- (tc.tool || tc.name) === 'submit_with_check'
323
- );
324
- const successCount = submitCalls.filter((tc) => {
325
- const res = tc.result;
326
- if (!res) {
327
- return true; // 无结果信息默认成功
328
- }
329
- if (typeof res === 'string') {
330
- return !res.includes('rejected') && !res.includes('error');
331
- }
332
- return res.status !== 'rejected' && res.status !== 'error';
333
- }).length;
334
- const rejectedCount = submitCalls.length - successCount;
335
-
336
- // ── 拒绝率过高时追加一轮修正尝试 ──
337
- if (
338
- rejectedCount > successCount &&
339
- rejectedCount >= 2 &&
340
- options.retryOnRejection !== false
341
- ) {
342
- this.#logger.info(
343
- `[ProducerAgent] 高拒绝率 (${rejectedCount}/${submitCalls.length}) — 尝试修正`
344
- );
345
- try {
346
- const retryPrompt = `你的 ${rejectedCount} 个提交被拒绝了。请根据拒绝原因改进后重新提交,确保:\n1. content.markdown 字段 ≥ 200 字符\n2. 包含代码块 (\`\`\`)\n3. 包含来源标注 (来源: FileName.m:行号)\n4. 标题使用项目真实类名\n5. 必填: trigger (@kebab-case)、kind (rule/pattern/fact)、doClause (英文祈使句)`;
347
- const retryResult = await this.#chatAgent.execute(retryPrompt, {
348
- source: 'system',
349
- conversationId: options.sessionId
350
- ? `producer-retry-${options.sessionId}-${dimId}`
351
- : undefined,
352
- budget: { ...budget, maxIterations: 5, maxSubmits: rejectedCount },
353
- systemPromptOverride: PRODUCER_SYSTEM_PROMPT,
354
- allowedTools: PRODUCER_TOOLS,
355
- strategy: 'producer',
356
- temperature: 0.3,
357
- projectLanguage: projectInfo.lang,
358
- memoryCoordinator: options.memoryCoordinator || undefined,
359
- dimensionMeta: {
360
- id: dimId,
361
- outputType: dimConfig.outputType || 'candidate',
362
- allowedKnowledgeTypes: dimConfig.allowedKnowledgeTypes || [],
363
- },
364
- });
365
-
366
- const retrySubmits = (retryResult.toolCalls || []).filter(
367
- (tc) =>
368
- (tc.tool || tc.name) === 'submit_knowledge' ||
369
- (tc.tool || tc.name) === 'submit_with_check'
370
- );
371
- const retrySuccess = retrySubmits.filter((tc) => {
372
- const res = tc.result;
373
- if (!res) {
374
- return true;
375
- }
376
- if (typeof res === 'string') {
377
- return !res.includes('rejected') && !res.includes('error');
378
- }
379
- return res.status !== 'rejected' && res.status !== 'error';
380
- }).length;
381
-
382
- this.#logger.info(`[ProducerAgent] 修正轮: +${retrySuccess} candidates for "${dimId}"`);
383
-
384
- // 合并两轮 token 用量
385
- const baseTokens = result.tokenUsage || { input: 0, output: 0 };
386
- const retryTokens = retryResult.tokenUsage || { input: 0, output: 0 };
387
- const mergedTokenUsage = {
388
- input: (baseTokens.input || 0) + (retryTokens.input || 0),
389
- output: (baseTokens.output || 0) + (retryTokens.output || 0),
390
- };
391
-
392
- return {
393
- candidateCount: successCount + retrySuccess,
394
- rejectedCount: rejectedCount - retrySuccess,
395
- toolCalls: [...(result.toolCalls || []), ...(retryResult.toolCalls || [])],
396
- reply: retryResult.reply || result.reply || '',
397
- tokenUsage: mergedTokenUsage,
398
- reasoningQuality: retryResult.reasoningQuality || result.reasoningQuality || null,
399
- };
400
- } catch (retryErr) {
401
- this.#logger.warn(`[ProducerAgent] 修正轮失败 "${dimId}": ${retryErr.message}`);
402
- }
403
- }
404
-
405
- this.#logger.info(
406
- `[ProducerAgent] ✅ dimension "${dimId}" — ${successCount} candidates created (${rejectedCount} rejected), ${result.toolCalls?.length || 0} total tool calls`
407
- );
408
-
409
- return {
410
- candidateCount: successCount,
411
- rejectedCount,
412
- toolCalls: result.toolCalls || [],
413
- reply: result.reply || '',
414
- tokenUsage: result.tokenUsage || null,
415
- reasoningQuality: result.reasoningQuality || null,
416
- };
417
- } catch (err) {
418
- this.#logger.error(`[ProducerAgent] ❌ dimension "${dimId}" error: ${err.message}`);
419
- return { candidateCount: 0, toolCalls: [], reply: '' };
420
- }
421
- }
422
- }
423
-
424
- /**
425
- * @typedef {object} ProducerResult
426
- * @property {number} candidateCount
427
- * @property {Array} toolCalls
428
- * @property {string} reply
429
- */
430
-
431
- export default ProducerAgent;