autosnippet 2.6.0 → 2.7.1
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 +137 -65
- package/bin/api-server.js +5 -0
- package/bin/cli.js +6 -1
- package/dashboard/dist/assets/{icons-rnn04CvH.js → icons-B_Xg4B-s.js} +148 -88
- package/dashboard/dist/assets/index-BjfUm8p9.js +197 -0
- package/dashboard/dist/assets/index-CkIih2CC.css +1 -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 +18 -1
- package/lib/cli/SetupService.js +86 -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 +208 -0
- package/lib/external/ai/providers/GoogleGeminiProvider.js +247 -1
- package/lib/external/ai/providers/OpenAiProvider.js +141 -0
- package/lib/external/mcp/McpServer.js +6 -1
- package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-context.js +216 -0
- package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +657 -0
- package/lib/external/mcp/handlers/bootstrap/pipeline/tier-scheduler.js +160 -0
- package/lib/external/mcp/handlers/bootstrap/skills.js +225 -0
- package/lib/external/mcp/handlers/bootstrap.js +159 -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 +58 -17
- package/lib/external/mcp/tools.js +4 -3
- package/lib/http/middleware/requestLogger.js +23 -4
- package/lib/http/routes/ai.js +158 -2
- 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 +12 -0
- package/lib/infrastructure/database/DatabaseConnection.js +6 -1
- package/lib/infrastructure/logging/Logger.js +86 -3
- package/lib/infrastructure/realtime/RealtimeService.js +2 -5
- package/lib/infrastructure/vector/JsonVectorAdapter.js +26 -1
- package/lib/injection/ServiceContainer.js +55 -2
- package/lib/service/bootstrap/BootstrapTaskManager.js +400 -0
- package/lib/service/candidate/CandidateFileWriter.js +72 -27
- package/lib/service/candidate/CandidateService.js +156 -10
- package/lib/service/chat/AnalystAgent.js +245 -0
- package/lib/service/chat/CandidateGuardrail.js +134 -0
- package/lib/service/chat/ChatAgent.js +1055 -167
- package/lib/service/chat/ContextWindow.js +730 -0
- package/lib/service/chat/ConversationStore.js +3 -0
- package/lib/service/chat/HandoffProtocol.js +181 -0
- package/lib/service/chat/Memory.js +3 -0
- package/lib/service/chat/ProducerAgent.js +293 -0
- package/lib/service/chat/ToolRegistry.js +149 -5
- package/lib/service/chat/tools.js +1404 -61
- package/lib/service/guard/ExclusionManager.js +2 -0
- package/lib/service/guard/RuleLearner.js +2 -0
- package/lib/service/quality/FeedbackCollector.js +2 -0
- package/lib/service/recipe/RecipeFileWriter.js +16 -1
- package/lib/service/recipe/RecipeStatsTracker.js +2 -0
- package/lib/service/skills/SignalCollector.js +33 -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/lib/shared/PathGuard.js +314 -0
- package/package.json +1 -1
- package/resources/native-ui/combined-window.swift +494 -0
- 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
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* orchestrator.js — AI-First Bootstrap 管线
|
|
3
|
+
*
|
|
4
|
+
* 核心架构: Analyst → Gate → Producer (双 Agent 模式)
|
|
5
|
+
*
|
|
6
|
+
* 1. Analyst Agent 自由探索代码 (AST 工具 + 文件搜索)
|
|
7
|
+
* 2. HandoffProtocol 质量门控
|
|
8
|
+
* 3. Producer Agent 格式化输出 (submit_candidate)
|
|
9
|
+
* 4. TierScheduler 分层并行执行
|
|
10
|
+
*
|
|
11
|
+
* @module pipeline/orchestrator
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import fs from 'node:fs/promises';
|
|
16
|
+
import { AnalystAgent } from '../../../../../service/chat/AnalystAgent.js';
|
|
17
|
+
import { ProducerAgent } from '../../../../../service/chat/ProducerAgent.js';
|
|
18
|
+
import { TierScheduler } from './tier-scheduler.js';
|
|
19
|
+
import { DimensionContext, parseDimensionDigest } from './dimension-context.js';
|
|
20
|
+
import Logger from '../../../../../infrastructure/logging/Logger.js';
|
|
21
|
+
|
|
22
|
+
const logger = Logger.getInstance();
|
|
23
|
+
|
|
24
|
+
// ──────────────────────────────────────────────────────────────────
|
|
25
|
+
// P3: 断点续传 — Checkpoint 存储/恢复
|
|
26
|
+
// ──────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
const CHECKPOINT_TTL_MS = 3600_000; // 1小时内有效
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 保存维度级 checkpoint
|
|
32
|
+
* @param {string} projectRoot
|
|
33
|
+
* @param {string} sessionId
|
|
34
|
+
* @param {string} dimId
|
|
35
|
+
* @param {object} result — 维度执行结果
|
|
36
|
+
* @param {object} [digest] — DimensionDigest
|
|
37
|
+
*/
|
|
38
|
+
async function saveDimensionCheckpoint(projectRoot, sessionId, dimId, result, digest = null) {
|
|
39
|
+
try {
|
|
40
|
+
const checkpointDir = path.join(projectRoot, '.autosnippet', 'bootstrap-checkpoint');
|
|
41
|
+
await fs.mkdir(checkpointDir, { recursive: true });
|
|
42
|
+
await fs.writeFile(
|
|
43
|
+
path.join(checkpointDir, `${dimId}.json`),
|
|
44
|
+
JSON.stringify({ dimId, sessionId, ...result, digest, completedAt: Date.now() }),
|
|
45
|
+
);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
logger.warn(`[Bootstrap-v3] checkpoint save failed for "${dimId}": ${err.message}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 加载有效的 checkpoints
|
|
53
|
+
* @param {string} projectRoot
|
|
54
|
+
* @returns {Promise<Map<string, object>>} dimId → checkpoint data
|
|
55
|
+
*/
|
|
56
|
+
async function loadCheckpoints(projectRoot) {
|
|
57
|
+
const checkpoints = new Map();
|
|
58
|
+
try {
|
|
59
|
+
const checkpointDir = path.join(projectRoot, '.autosnippet', 'bootstrap-checkpoint');
|
|
60
|
+
const files = await fs.readdir(checkpointDir).catch(() => []);
|
|
61
|
+
const now = Date.now();
|
|
62
|
+
for (const file of files) {
|
|
63
|
+
if (!file.endsWith('.json')) continue;
|
|
64
|
+
try {
|
|
65
|
+
const content = await fs.readFile(path.join(checkpointDir, file), 'utf-8');
|
|
66
|
+
const data = JSON.parse(content);
|
|
67
|
+
if (data.completedAt && (now - data.completedAt) < CHECKPOINT_TTL_MS) {
|
|
68
|
+
checkpoints.set(data.dimId, data);
|
|
69
|
+
}
|
|
70
|
+
} catch { /* skip corrupt checkpoint */ }
|
|
71
|
+
}
|
|
72
|
+
} catch { /* checkpoint dir doesn't exist */ }
|
|
73
|
+
return checkpoints;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 清理 checkpoint 目录
|
|
78
|
+
* @param {string} projectRoot
|
|
79
|
+
*/
|
|
80
|
+
async function clearCheckpoints(projectRoot) {
|
|
81
|
+
try {
|
|
82
|
+
const checkpointDir = path.join(projectRoot, '.autosnippet', 'bootstrap-checkpoint');
|
|
83
|
+
await fs.rm(checkpointDir, { recursive: true, force: true });
|
|
84
|
+
} catch { /* ignore */ }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ──────────────────────────────────────────────────────────────────
|
|
88
|
+
// v3.0 维度配置 (增加 focusAreas 用于 Analyst prompt)
|
|
89
|
+
// ──────────────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
const DIMENSION_CONFIGS_V3 = {
|
|
92
|
+
'project-profile': {
|
|
93
|
+
label: '项目概貌',
|
|
94
|
+
guide: '分析项目的整体结构、技术栈、模块划分和入口点。',
|
|
95
|
+
focusAreas: [
|
|
96
|
+
'项目结构和模块划分',
|
|
97
|
+
'技术栈和框架依赖',
|
|
98
|
+
'核心入口点和启动流程',
|
|
99
|
+
],
|
|
100
|
+
outputType: 'dual',
|
|
101
|
+
allowedKnowledgeTypes: ['architecture'],
|
|
102
|
+
},
|
|
103
|
+
'objc-deep-scan': {
|
|
104
|
+
label: '深度扫描(常量/Hook)',
|
|
105
|
+
guide: '扫描 #define 宏、extern/static 常量、Method Swizzling hook。',
|
|
106
|
+
focusAreas: [
|
|
107
|
+
'#define 值宏和函数宏',
|
|
108
|
+
'extern/static 常量定义',
|
|
109
|
+
'Method Swizzling hook 和 load/initialize 方法',
|
|
110
|
+
],
|
|
111
|
+
outputType: 'dual',
|
|
112
|
+
allowedKnowledgeTypes: ['code-standard', 'code-pattern'],
|
|
113
|
+
},
|
|
114
|
+
'category-scan': {
|
|
115
|
+
label: '基础类分类方法扫描',
|
|
116
|
+
guide: '扫描 Foundation/UIKit 的 Category/Extension 方法及其实现。',
|
|
117
|
+
focusAreas: [
|
|
118
|
+
'NSString/NSArray/NSDictionary 等基础类的 Category',
|
|
119
|
+
'UIView/UIColor/UIImage 等 UI 组件的 Category',
|
|
120
|
+
'各 Category 方法的使用场景和频率',
|
|
121
|
+
],
|
|
122
|
+
outputType: 'dual',
|
|
123
|
+
allowedKnowledgeTypes: ['code-standard', 'code-pattern'],
|
|
124
|
+
},
|
|
125
|
+
'code-standard': {
|
|
126
|
+
label: '代码规范',
|
|
127
|
+
guide: '分析项目的命名约定、注释风格、文件组织方式。',
|
|
128
|
+
focusAreas: [
|
|
129
|
+
'类名前缀和命名约定 (BD/BDUIKit 等)',
|
|
130
|
+
'方法签名风格和 API 命名',
|
|
131
|
+
'注释风格 (语言/格式/MARK 分段)',
|
|
132
|
+
'文件组织和目录规范',
|
|
133
|
+
],
|
|
134
|
+
outputType: 'dual',
|
|
135
|
+
allowedKnowledgeTypes: ['code-standard', 'code-style'],
|
|
136
|
+
},
|
|
137
|
+
'architecture': {
|
|
138
|
+
label: '架构模式',
|
|
139
|
+
guide: '分析项目的分层架构、模块职责和依赖关系。',
|
|
140
|
+
focusAreas: [
|
|
141
|
+
'分层架构 (MVC/MVVM/其他)',
|
|
142
|
+
'模块间通信方式 (Protocol/Notification/Target-Action)',
|
|
143
|
+
'依赖管理和服务注册',
|
|
144
|
+
'模块边界约束',
|
|
145
|
+
],
|
|
146
|
+
outputType: 'dual',
|
|
147
|
+
allowedKnowledgeTypes: ['architecture', 'module-dependency', 'boundary-constraint'],
|
|
148
|
+
},
|
|
149
|
+
'code-pattern': {
|
|
150
|
+
label: '设计模式',
|
|
151
|
+
guide: '识别项目中使用的设计模式和架构模式。',
|
|
152
|
+
focusAreas: [
|
|
153
|
+
'创建型模式 (Singleton, Factory, Builder)',
|
|
154
|
+
'结构型模式 (Proxy, Adapter, Decorator, Composite)',
|
|
155
|
+
'行为型模式 (Observer, Strategy, Template Method, Delegate)',
|
|
156
|
+
'架构模式 (MVC/MVVM, Service Locator, Coordinator)',
|
|
157
|
+
],
|
|
158
|
+
outputType: 'candidate',
|
|
159
|
+
allowedKnowledgeTypes: ['code-pattern', 'code-relation', 'inheritance'],
|
|
160
|
+
},
|
|
161
|
+
'event-and-data-flow': {
|
|
162
|
+
label: '事件与数据流',
|
|
163
|
+
guide: '分析事件传播和数据状态管理方式。',
|
|
164
|
+
focusAreas: [
|
|
165
|
+
'事件传播 (Delegate/Notification/Block/Target-Action)',
|
|
166
|
+
'数据状态管理 (KVO/属性观察/响应式)',
|
|
167
|
+
'数据持久化方案',
|
|
168
|
+
'数据流转路径和状态同步',
|
|
169
|
+
],
|
|
170
|
+
outputType: 'candidate',
|
|
171
|
+
allowedKnowledgeTypes: ['call-chain', 'data-flow', 'event-and-data-flow'],
|
|
172
|
+
},
|
|
173
|
+
'best-practice': {
|
|
174
|
+
label: '最佳实践',
|
|
175
|
+
guide: '分析错误处理、并发安全、内存管理等工程实践。',
|
|
176
|
+
focusAreas: [
|
|
177
|
+
'错误处理策略和模式',
|
|
178
|
+
'并发安全 (GCD/NSOperation/锁)',
|
|
179
|
+
'内存管理 (ARC 下的弱引用/循环引用处理)',
|
|
180
|
+
'日志规范和调试基础设施',
|
|
181
|
+
],
|
|
182
|
+
outputType: 'candidate',
|
|
183
|
+
allowedKnowledgeTypes: ['best-practice'],
|
|
184
|
+
},
|
|
185
|
+
'agent-guidelines': {
|
|
186
|
+
label: 'Agent 开发注意事项',
|
|
187
|
+
guide: '总结 Agent 在此项目开发时必须遵守的规则和约束。',
|
|
188
|
+
focusAreas: [
|
|
189
|
+
'命名强制规则和前缀约定',
|
|
190
|
+
'线程安全约束',
|
|
191
|
+
'已废弃 API 标记',
|
|
192
|
+
'架构约束注释 (TODO/FIXME)',
|
|
193
|
+
],
|
|
194
|
+
outputType: 'skill',
|
|
195
|
+
allowedKnowledgeTypes: ['boundary-constraint', 'code-standard'],
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// ──────────────────────────────────────────────────────────────────
|
|
200
|
+
// fillDimensionsV3 — v3.0 管线入口
|
|
201
|
+
// ──────────────────────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* fillDimensionsV3 — v3.0 AI-First 维度填充管线
|
|
205
|
+
*
|
|
206
|
+
* @param {object} fillContext — 由 bootstrapKnowledge 构建的上下文
|
|
207
|
+
*/
|
|
208
|
+
export async function fillDimensionsV3(fillContext) {
|
|
209
|
+
const {
|
|
210
|
+
ctx, dimensions, taskManager, sessionId, projectRoot,
|
|
211
|
+
depGraphData, guardAudit, langStats, primaryLang, astProjectSummary,
|
|
212
|
+
skillContext, skillsEnhanced,
|
|
213
|
+
} = fillContext;
|
|
214
|
+
|
|
215
|
+
logger.info('[Bootstrap-v3] ═══ fillDimensionsV3 entered — AI-First pipeline');
|
|
216
|
+
|
|
217
|
+
let allFiles = fillContext.allFiles;
|
|
218
|
+
fillContext.allFiles = null;
|
|
219
|
+
|
|
220
|
+
// ═══════════════════════════════════════════════════════════
|
|
221
|
+
// Step 0: AI 可用性检查
|
|
222
|
+
// ═══════════════════════════════════════════════════════════
|
|
223
|
+
let chatAgent = null;
|
|
224
|
+
try {
|
|
225
|
+
chatAgent = ctx.container.get('chatAgent');
|
|
226
|
+
if (chatAgent && !chatAgent.hasRealAI) chatAgent = null;
|
|
227
|
+
if (chatAgent) chatAgent.resetGlobalSubmittedTitles();
|
|
228
|
+
} catch { /* not available */ }
|
|
229
|
+
|
|
230
|
+
if (!chatAgent) {
|
|
231
|
+
logger.info('[Bootstrap-v3] AI not available — aborting v3 pipeline');
|
|
232
|
+
taskManager?.emitProgress('bootstrap:ai-unavailable', {
|
|
233
|
+
message: 'AI 不可用,v3 管线需要 AI。请检查 AI Provider 配置。',
|
|
234
|
+
});
|
|
235
|
+
for (const dim of dimensions) {
|
|
236
|
+
taskManager?.markTaskCompleted(dim.id, { type: 'skipped', reason: 'ai-unavailable' });
|
|
237
|
+
}
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ═══════════════════════════════════════════════════════════
|
|
242
|
+
// Step 0.5: 构建 ProjectGraph
|
|
243
|
+
// ═══════════════════════════════════════════════════════════
|
|
244
|
+
let projectGraph = null;
|
|
245
|
+
try {
|
|
246
|
+
projectGraph = await ctx.container.buildProjectGraph(projectRoot, {
|
|
247
|
+
maxFiles: 500,
|
|
248
|
+
timeoutMs: 15_000,
|
|
249
|
+
});
|
|
250
|
+
if (projectGraph) {
|
|
251
|
+
const overview = projectGraph.getOverview();
|
|
252
|
+
logger.info(`[Bootstrap-v3] ProjectGraph: ${overview.totalClasses} classes, ${overview.totalProtocols} protocols (${overview.buildTimeMs}ms)`);
|
|
253
|
+
}
|
|
254
|
+
} catch (e) {
|
|
255
|
+
logger.warn(`[Bootstrap-v3] ProjectGraph build failed: ${e.message}`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ═══════════════════════════════════════════════════════════
|
|
259
|
+
// Step 1: 构建 Agents + 上下文
|
|
260
|
+
// ═══════════════════════════════════════════════════════════
|
|
261
|
+
const analystAgent = new AnalystAgent(chatAgent, projectGraph, { maxRetries: 1 });
|
|
262
|
+
const producerAgent = new ProducerAgent(chatAgent);
|
|
263
|
+
|
|
264
|
+
// 注入文件缓存
|
|
265
|
+
chatAgent.setFileCache(allFiles);
|
|
266
|
+
|
|
267
|
+
// 项目信息
|
|
268
|
+
const projectInfo = {
|
|
269
|
+
name: path.basename(projectRoot),
|
|
270
|
+
lang: primaryLang || 'objectivec',
|
|
271
|
+
fileCount: allFiles?.length || 0,
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
// 跨维度上下文
|
|
275
|
+
const dimContext = new DimensionContext({
|
|
276
|
+
projectName: projectInfo.name,
|
|
277
|
+
primaryLang: projectInfo.lang,
|
|
278
|
+
fileCount: projectInfo.fileCount,
|
|
279
|
+
targetCount: Object.keys(fillContext.targetFileMap || {}).length,
|
|
280
|
+
modules: Object.keys(fillContext.targetFileMap || {}),
|
|
281
|
+
depGraph: depGraphData || null,
|
|
282
|
+
astMetrics: astProjectSummary?.projectMetrics || null,
|
|
283
|
+
guardSummary: guardAudit?.summary || null,
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// ═══════════════════════════════════════════════════════════
|
|
287
|
+
// Step 2: 按维度分层执行 (Analyst → Gate → Producer)
|
|
288
|
+
// ═══════════════════════════════════════════════════════════
|
|
289
|
+
const concurrency = parseInt(process.env.ASD_PARALLEL_CONCURRENCY || '3', 10);
|
|
290
|
+
const enableParallel = process.env.ASD_PARALLEL_BOOTSTRAP !== 'false';
|
|
291
|
+
const scheduler = new TierScheduler();
|
|
292
|
+
|
|
293
|
+
// 过滤出有定义的维度
|
|
294
|
+
const activeDimIds = dimensions
|
|
295
|
+
.map(d => d.id)
|
|
296
|
+
.filter(id => DIMENSION_CONFIGS_V3[id]);
|
|
297
|
+
|
|
298
|
+
logger.info(`[Bootstrap-v3] Active dimensions: [${activeDimIds.join(', ')}], concurrency=${enableParallel ? concurrency : 1}`);
|
|
299
|
+
|
|
300
|
+
// ── P3: 断点续传 — 加载有效 checkpoints ──
|
|
301
|
+
const completedCheckpoints = await loadCheckpoints(projectRoot);
|
|
302
|
+
const skippedDims = [];
|
|
303
|
+
for (const [dimId, checkpoint] of completedCheckpoints) {
|
|
304
|
+
if (activeDimIds.includes(dimId)) {
|
|
305
|
+
// 恢复 DimensionContext 中的 digest
|
|
306
|
+
if (checkpoint.digest) {
|
|
307
|
+
dimContext.addDimensionDigest(dimId, checkpoint.digest);
|
|
308
|
+
}
|
|
309
|
+
taskManager?.markTaskCompleted(dimId, {
|
|
310
|
+
type: 'checkpoint-restored',
|
|
311
|
+
...checkpoint,
|
|
312
|
+
});
|
|
313
|
+
skippedDims.push(dimId);
|
|
314
|
+
logger.info(`[Bootstrap-v3] ⏩ 跳过已完成维度 (checkpoint): "${dimId}"`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const candidateResults = { created: 0, failed: 0, errors: [] };
|
|
319
|
+
const dimensionCandidates = {};
|
|
320
|
+
const dimensionStats = {}; // P4.2: 维度级统计
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* 执行单个维度: Analyst → Gate → Producer
|
|
324
|
+
*/
|
|
325
|
+
async function executeDimension(dimId) {
|
|
326
|
+
// P3: 跳过已有 checkpoint 的维度
|
|
327
|
+
if (skippedDims.includes(dimId)) {
|
|
328
|
+
const cp = completedCheckpoints.get(dimId);
|
|
329
|
+
const cpResult = {
|
|
330
|
+
candidateCount: cp?.candidateCount || 0,
|
|
331
|
+
rejectedCount: cp?.rejectedCount || 0,
|
|
332
|
+
analysisChars: cp?.analysisChars || 0,
|
|
333
|
+
referencedFiles: cp?.referencedFiles || 0,
|
|
334
|
+
durationMs: cp?.durationMs || 0,
|
|
335
|
+
toolCallCount: cp?.toolCallCount || 0,
|
|
336
|
+
tokenUsage: cp?.tokenUsage || { input: 0, output: 0 },
|
|
337
|
+
skipped: true,
|
|
338
|
+
restoredFromCheckpoint: true,
|
|
339
|
+
};
|
|
340
|
+
// P4.2: 将恢复的维度也记入统计
|
|
341
|
+
dimensionStats[dimId] = cpResult;
|
|
342
|
+
candidateResults.created += cpResult.candidateCount;
|
|
343
|
+
return cpResult;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const dim = dimensions.find(d => d.id === dimId);
|
|
347
|
+
const v3Config = DIMENSION_CONFIGS_V3[dimId];
|
|
348
|
+
if (!dim || !v3Config) {
|
|
349
|
+
return { candidateCount: 0, error: 'dimension not found' };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// 合并 v3 配置和原始维度配置 (保留 skillWorthy, skillMeta 等)
|
|
353
|
+
const dimConfig = {
|
|
354
|
+
...v3Config,
|
|
355
|
+
id: dimId,
|
|
356
|
+
skillWorthy: dim.skillWorthy,
|
|
357
|
+
dualOutput: dim.dualOutput,
|
|
358
|
+
skillMeta: dim.skillMeta,
|
|
359
|
+
knowledgeTypes: dim.knowledgeTypes || v3Config.allowedKnowledgeTypes,
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
// Session 有效性检查
|
|
363
|
+
if (taskManager && !taskManager.isSessionValid(sessionId)) {
|
|
364
|
+
logger.warn(`[Bootstrap-v3] Session superseded — skipping "${dimId}"`);
|
|
365
|
+
return { candidateCount: 0, error: 'session-superseded' };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
taskManager?.markTaskFilling(dimId);
|
|
369
|
+
logger.info(`[Bootstrap-v3] ── Dimension "${dimId}" (${dimConfig.label}) ──`);
|
|
370
|
+
|
|
371
|
+
const dimStartTime = Date.now();
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
// ── Phase 1: Analyst ──
|
|
375
|
+
const analysisReport = await Promise.race([
|
|
376
|
+
analystAgent.analyze(dimConfig, projectInfo, { sessionId, dimensionContext: dimContext }),
|
|
377
|
+
new Promise((_, reject) =>
|
|
378
|
+
setTimeout(() => reject(new Error(`Analyst timeout for "${dimId}"`)), 180_000)),
|
|
379
|
+
]);
|
|
380
|
+
|
|
381
|
+
logger.info(`[Bootstrap-v3] Analyst "${dimId}": ${analysisReport.analysisText.length} chars, ${analysisReport.referencedFiles.length} files (${Date.now() - dimStartTime}ms)`);
|
|
382
|
+
|
|
383
|
+
// ── Phase 2: Producer (如果需要候选输出) ──
|
|
384
|
+
let producerResult = { candidateCount: 0, toolCalls: [], reply: '' };
|
|
385
|
+
// v3 优先使用 DIMENSION_CONFIGS_V3 的 outputType,回退到 baseDimension 的 skillWorthy/dualOutput
|
|
386
|
+
const v3OutputType = DIMENSION_CONFIGS_V3[dimId]?.outputType;
|
|
387
|
+
const needsCandidates = v3OutputType
|
|
388
|
+
? v3OutputType !== 'skill' // 'dual' 或 'candidate' 都产出候选
|
|
389
|
+
: (!dimConfig.skillWorthy || dimConfig.dualOutput);
|
|
390
|
+
|
|
391
|
+
if (needsCandidates && analysisReport.analysisText.length >= 100) {
|
|
392
|
+
producerResult = await Promise.race([
|
|
393
|
+
producerAgent.produce(analysisReport, dimConfig, projectInfo, { sessionId }),
|
|
394
|
+
new Promise((_, reject) =>
|
|
395
|
+
setTimeout(() => reject(new Error(`Producer timeout for "${dimId}"`)), 120_000)),
|
|
396
|
+
]);
|
|
397
|
+
|
|
398
|
+
candidateResults.created += producerResult.candidateCount;
|
|
399
|
+
logger.info(`[Bootstrap-v3] Producer "${dimId}": ${producerResult.candidateCount} candidates (${Date.now() - dimStartTime}ms total)`);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// ── Phase 3: 记录 DimensionDigest ──
|
|
403
|
+
const digest = parseDimensionDigest(producerResult.reply) || {
|
|
404
|
+
summary: `v3 分析: ${analysisReport.analysisText.substring(0, 200)}...`,
|
|
405
|
+
candidateCount: producerResult.candidateCount,
|
|
406
|
+
keyFindings: [],
|
|
407
|
+
crossRefs: {},
|
|
408
|
+
gaps: [],
|
|
409
|
+
};
|
|
410
|
+
dimContext.addDimensionDigest(dimId, digest);
|
|
411
|
+
|
|
412
|
+
// 记录到 DimensionContext
|
|
413
|
+
for (const tc of (producerResult.toolCalls || [])) {
|
|
414
|
+
const tool = tc.tool || tc.name;
|
|
415
|
+
if (tool === 'submit_candidate' || tool === 'submit_with_check') {
|
|
416
|
+
dimContext.addSubmittedCandidate(dimId, {
|
|
417
|
+
title: tc.params?.title || '',
|
|
418
|
+
subTopic: tc.params?.category || '',
|
|
419
|
+
summary: tc.params?.summary || '',
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// 保存分析结果供 Skill 生成
|
|
425
|
+
dimensionCandidates[dimId] = {
|
|
426
|
+
analysisReport,
|
|
427
|
+
producerResult,
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
taskManager?.markTaskCompleted(dimId, {
|
|
431
|
+
type: needsCandidates ? 'candidate' : 'skill',
|
|
432
|
+
extracted: producerResult.candidateCount,
|
|
433
|
+
created: producerResult.candidateCount,
|
|
434
|
+
status: 'v3-complete',
|
|
435
|
+
durationMs: Date.now() - dimStartTime,
|
|
436
|
+
toolCallCount: (analysisReport.metadata?.toolCallCount || 0) + (producerResult.toolCalls?.length || 0),
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
// P4.1: 聚合 token 用量
|
|
440
|
+
const analystTokens = analysisReport.metadata?.tokenUsage || { input: 0, output: 0 };
|
|
441
|
+
const producerTokens = producerResult.tokenUsage || { input: 0, output: 0 };
|
|
442
|
+
const dimTokenUsage = {
|
|
443
|
+
input: (analystTokens.input || 0) + (producerTokens.input || 0),
|
|
444
|
+
output: (analystTokens.output || 0) + (producerTokens.output || 0),
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
const dimResult = {
|
|
448
|
+
candidateCount: producerResult.candidateCount,
|
|
449
|
+
rejectedCount: producerResult.rejectedCount || 0,
|
|
450
|
+
analysisChars: analysisReport.analysisText.length,
|
|
451
|
+
referencedFiles: analysisReport.referencedFiles.length,
|
|
452
|
+
durationMs: Date.now() - dimStartTime,
|
|
453
|
+
toolCallCount: (analysisReport.metadata?.toolCallCount || 0) + (producerResult.toolCalls?.length || 0),
|
|
454
|
+
tokenUsage: dimTokenUsage,
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
// P4.2: 记录维度统计
|
|
458
|
+
dimensionStats[dimId] = dimResult;
|
|
459
|
+
|
|
460
|
+
// P3: 保存 checkpoint
|
|
461
|
+
await saveDimensionCheckpoint(projectRoot, sessionId, dimId, dimResult, digest);
|
|
462
|
+
|
|
463
|
+
return dimResult;
|
|
464
|
+
|
|
465
|
+
} catch (err) {
|
|
466
|
+
logger.error(`[Bootstrap-v3] Dimension "${dimId}" failed: ${err.message}`);
|
|
467
|
+
candidateResults.errors.push({ dimId, error: err.message });
|
|
468
|
+
taskManager?.markTaskCompleted(dimId, { type: 'error', reason: err.message });
|
|
469
|
+
return { candidateCount: 0, error: err.message };
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// ═══════════════════════════════════════════════════════════
|
|
474
|
+
// Step 3: 执行 (并行 or 串行)
|
|
475
|
+
// ═══════════════════════════════════════════════════════════
|
|
476
|
+
const t0 = Date.now();
|
|
477
|
+
|
|
478
|
+
if (enableParallel) {
|
|
479
|
+
const results = await scheduler.execute(executeDimension, {
|
|
480
|
+
concurrency,
|
|
481
|
+
shouldAbort: () => taskManager && !taskManager.isSessionValid(sessionId),
|
|
482
|
+
onTierComplete: (tierIndex, tierResults) => {
|
|
483
|
+
const tierStats = [...tierResults.values()];
|
|
484
|
+
const totalCandidates = tierStats.reduce((s, r) => s + (r.candidateCount || 0), 0);
|
|
485
|
+
logger.info(`[Bootstrap-v3] Tier ${tierIndex + 1} complete: ${tierResults.size} dimensions, ${totalCandidates} candidates`);
|
|
486
|
+
},
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
logger.info(`[Bootstrap-v3] All tiers complete: ${results.size} dimensions in ${Date.now() - t0}ms`);
|
|
490
|
+
} else {
|
|
491
|
+
// 串行: 按 TierScheduler 内部顺序逐个执行
|
|
492
|
+
for (const tier of scheduler.getTiers()) {
|
|
493
|
+
for (const dimId of tier) {
|
|
494
|
+
if (!activeDimIds.includes(dimId)) continue;
|
|
495
|
+
if (taskManager && !taskManager.isSessionValid(sessionId)) break;
|
|
496
|
+
await executeDimension(dimId);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
logger.info(`[Bootstrap-v3] Serial execution complete in ${Date.now() - t0}ms`);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// ═══════════════════════════════════════════════════════════
|
|
503
|
+
// Step 4: Project Skill 生成 (skillWorthy 维度)
|
|
504
|
+
//
|
|
505
|
+
// v3: 直接使用 Analyst 的分析文本作为 Skill 内容
|
|
506
|
+
// 不再通过 buildProjectSkillContent 转换候选数组
|
|
507
|
+
// ═══════════════════════════════════════════════════════════
|
|
508
|
+
const skillResults = { created: 0, failed: 0, skills: [], errors: [] };
|
|
509
|
+
|
|
510
|
+
try {
|
|
511
|
+
const { createSkill } = await import('../../skill.js');
|
|
512
|
+
|
|
513
|
+
for (const dim of dimensions) {
|
|
514
|
+
if (!dim.skillWorthy) continue;
|
|
515
|
+
const dimData = dimensionCandidates[dim.id];
|
|
516
|
+
if (!dimData?.analysisReport?.analysisText) continue;
|
|
517
|
+
if (taskManager && !taskManager.isSessionValid(sessionId)) break;
|
|
518
|
+
|
|
519
|
+
try {
|
|
520
|
+
const skillName = dim.skillMeta?.name || `project-${dim.id}`;
|
|
521
|
+
const skillDescription = dim.skillMeta?.description || `Auto-generated skill for ${dim.label}`;
|
|
522
|
+
|
|
523
|
+
// v3: Analyst 分析文本就是高质量的 Skill 内容
|
|
524
|
+
const analysisText = dimData.analysisReport.analysisText;
|
|
525
|
+
const referencedFiles = dimData.analysisReport.referencedFiles || [];
|
|
526
|
+
|
|
527
|
+
// 构建 Markdown Skill 内容
|
|
528
|
+
const skillContent = [
|
|
529
|
+
`# ${dim.label || dim.id}`,
|
|
530
|
+
'',
|
|
531
|
+
`> Auto-generated by Bootstrap v3 (AI-First). Sources: ${referencedFiles.length} files analyzed.`,
|
|
532
|
+
'',
|
|
533
|
+
analysisText,
|
|
534
|
+
'',
|
|
535
|
+
referencedFiles.length > 0
|
|
536
|
+
? `## Referenced Files\n\n${referencedFiles.map(f => `- \`${f}\``).join('\n')}`
|
|
537
|
+
: '',
|
|
538
|
+
].filter(Boolean).join('\n');
|
|
539
|
+
|
|
540
|
+
const result = createSkill(ctx, {
|
|
541
|
+
name: skillName,
|
|
542
|
+
description: skillDescription,
|
|
543
|
+
content: skillContent,
|
|
544
|
+
overwrite: true,
|
|
545
|
+
createdBy: 'bootstrap-v3',
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
const parsed = JSON.parse(result);
|
|
549
|
+
if (parsed.success) {
|
|
550
|
+
skillResults.created++;
|
|
551
|
+
skillResults.skills.push(skillName);
|
|
552
|
+
logger.info(`[Bootstrap-v3] Skill "${skillName}" created for "${dim.id}"`);
|
|
553
|
+
} else {
|
|
554
|
+
throw new Error(parsed.error?.message || 'createSkill returned failure');
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
taskManager?.markTaskCompleted(dim.id, {
|
|
558
|
+
type: 'skill',
|
|
559
|
+
skillName,
|
|
560
|
+
sourceCount: referencedFiles.length,
|
|
561
|
+
});
|
|
562
|
+
} catch (err) {
|
|
563
|
+
logger.warn(`[Bootstrap-v3] Skill generation failed for "${dim.id}": ${err.message}`);
|
|
564
|
+
skillResults.failed++;
|
|
565
|
+
skillResults.errors.push({ dimId: dim.id, error: err.message });
|
|
566
|
+
taskManager?.markTaskFailed?.(dim.id, err);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
} catch (e) {
|
|
570
|
+
logger.warn(`[Bootstrap-v3] Skill generation module import failed: ${e.message}`);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// ═══════════════════════════════════════════════════════════
|
|
574
|
+
// Summary + P4.2: Bootstrap Report
|
|
575
|
+
// ═══════════════════════════════════════════════════════════
|
|
576
|
+
const totalTimeMs = Date.now() - t0;
|
|
577
|
+
|
|
578
|
+
// P4.1: 汇总所有维度 token 用量
|
|
579
|
+
const totalTokenUsage = { input: 0, output: 0 };
|
|
580
|
+
const totalToolCalls = Object.values(dimensionStats).reduce((sum, s) => sum + (s.toolCallCount || 0), 0);
|
|
581
|
+
for (const stat of Object.values(dimensionStats)) {
|
|
582
|
+
if (stat.tokenUsage) {
|
|
583
|
+
totalTokenUsage.input += stat.tokenUsage.input || 0;
|
|
584
|
+
totalTokenUsage.output += stat.tokenUsage.output || 0;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
logger.info([
|
|
589
|
+
`[Bootstrap-v3] ═══ Pipeline complete ═══`,
|
|
590
|
+
` Candidates: ${candidateResults.created} created, ${candidateResults.errors.length} errors`,
|
|
591
|
+
` Skills: ${skillResults.created} created, ${skillResults.failed} failed`,
|
|
592
|
+
` Time: ${totalTimeMs}ms (${(totalTimeMs / 1000).toFixed(1)}s)`,
|
|
593
|
+
` Mode: ${enableParallel ? `parallel (concurrency=${concurrency})` : 'serial'}`,
|
|
594
|
+
` Tokens: input=${totalTokenUsage.input}, output=${totalTokenUsage.output}`,
|
|
595
|
+
` Tool calls: ${totalToolCalls}`,
|
|
596
|
+
skippedDims.length > 0 ? ` Checkpoints restored: [${skippedDims.join(', ')}]` : '',
|
|
597
|
+
].filter(Boolean).join('\n'));
|
|
598
|
+
|
|
599
|
+
// P4.2: 生成冷启动报告
|
|
600
|
+
try {
|
|
601
|
+
const report = {
|
|
602
|
+
version: '2.7.0',
|
|
603
|
+
timestamp: new Date().toISOString(),
|
|
604
|
+
project: {
|
|
605
|
+
name: projectInfo.name,
|
|
606
|
+
files: projectInfo.fileCount,
|
|
607
|
+
lang: projectInfo.lang,
|
|
608
|
+
},
|
|
609
|
+
duration: {
|
|
610
|
+
totalMs: totalTimeMs,
|
|
611
|
+
totalSec: Math.round(totalTimeMs / 1000),
|
|
612
|
+
},
|
|
613
|
+
dimensions: {},
|
|
614
|
+
totals: {
|
|
615
|
+
candidates: candidateResults.created,
|
|
616
|
+
skills: skillResults.created,
|
|
617
|
+
toolCalls: totalToolCalls,
|
|
618
|
+
tokenUsage: totalTokenUsage,
|
|
619
|
+
errors: candidateResults.errors.length,
|
|
620
|
+
},
|
|
621
|
+
checkpoints: {
|
|
622
|
+
restored: skippedDims,
|
|
623
|
+
},
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
for (const [dimId, stat] of Object.entries(dimensionStats)) {
|
|
627
|
+
report.dimensions[dimId] = {
|
|
628
|
+
candidatesSubmitted: stat.candidateCount || 0,
|
|
629
|
+
candidatesRejected: stat.rejectedCount || 0,
|
|
630
|
+
analysisChars: stat.analysisChars || 0,
|
|
631
|
+
referencedFiles: stat.referencedFiles || 0,
|
|
632
|
+
durationMs: stat.durationMs || 0,
|
|
633
|
+
toolCallCount: stat.toolCallCount || 0,
|
|
634
|
+
tokenUsage: stat.tokenUsage || { input: 0, output: 0 },
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const reportDir = path.join(projectRoot, '.autosnippet');
|
|
639
|
+
await fs.mkdir(reportDir, { recursive: true });
|
|
640
|
+
await fs.writeFile(
|
|
641
|
+
path.join(reportDir, 'bootstrap-report.json'),
|
|
642
|
+
JSON.stringify(report, null, 2),
|
|
643
|
+
);
|
|
644
|
+
logger.info(`[Bootstrap-v3] 📊 Bootstrap report saved to .autosnippet/bootstrap-report.json`);
|
|
645
|
+
} catch (reportErr) {
|
|
646
|
+
logger.warn(`[Bootstrap-v3] Bootstrap report generation failed: ${reportErr.message}`);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// P3: 成功完成后清理 checkpoints
|
|
650
|
+
await clearCheckpoints(projectRoot);
|
|
651
|
+
|
|
652
|
+
// 释放文件缓存
|
|
653
|
+
allFiles = null;
|
|
654
|
+
chatAgent.setFileCache(null);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
export default fillDimensionsV3;
|