autosnippet 2.5.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 +35 -0
- package/dashboard/dist/assets/{icons-Dtm0E6DS.js → icons-Cq4-iQhP.js} +152 -87
- 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/Gateway.js +19 -4
- package/lib/core/gateway/GatewayActionRegistry.js +2 -2
- package/lib/domain/recipe/Recipe.js +3 -0
- package/lib/external/ai/AiProvider.js +117 -10
- 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/McpServer.js +2 -1
- 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 +126 -31
- package/lib/external/mcp/tools.js +25 -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 +16 -22
- package/lib/http/routes/skills.js +40 -3
- 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/audit/AuditStore.js +18 -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 +62 -3
- 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 +1272 -155
- package/lib/service/chat/ContextWindow.js +730 -0
- package/lib/service/chat/ConversationStore.js +377 -0
- package/lib/service/chat/HandoffProtocol.js +180 -0
- package/lib/service/chat/Memory.js +40 -10
- package/lib/service/chat/ProducerAgent.js +240 -0
- package/lib/service/chat/ToolRegistry.js +149 -5
- package/lib/service/chat/tools.js +1493 -60
- package/lib/service/recipe/RecipeFileWriter.js +12 -1
- package/lib/service/skills/EventAggregator.js +187 -0
- package/lib/service/skills/SignalCollector.js +549 -0
- package/lib/service/skills/SkillAdvisor.js +324 -0
- 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-B7VpZOCz.css +0 -1
- package/dashboard/dist/assets/index-D87IZTmZ.js +0 -187
|
@@ -51,8 +51,13 @@ import { ChatAgent } from '../service/chat/ChatAgent.js';
|
|
|
51
51
|
import { ALL_TOOLS } from '../service/chat/tools.js';
|
|
52
52
|
import { SkillHooks } from '../service/skills/SkillHooks.js';
|
|
53
53
|
|
|
54
|
+
// ─── v3.0: AST ProjectGraph ──────────────────────────
|
|
55
|
+
import ProjectGraph from '../core/ast/ProjectGraph.js';
|
|
56
|
+
|
|
54
57
|
// ─── P3: Infrastructure ──────────────────────────────
|
|
55
|
-
|
|
58
|
+
import { EventBus } from '../infrastructure/event/EventBus.js';
|
|
59
|
+
import { BootstrapTaskManager } from '../service/bootstrap/BootstrapTaskManager.js';
|
|
60
|
+
import { getRealtimeService as _getRealtimeService } from '../infrastructure/realtime/RealtimeService.js';
|
|
56
61
|
|
|
57
62
|
/**
|
|
58
63
|
* DependencyInjection 容器
|
|
@@ -201,7 +206,26 @@ export class ServiceContainer {
|
|
|
201
206
|
return this.singletons.gateway;
|
|
202
207
|
});
|
|
203
208
|
|
|
209
|
+
// EventBus(全局事件总线)
|
|
210
|
+
this.register('eventBus', () => {
|
|
211
|
+
if (!this.singletons.eventBus) {
|
|
212
|
+
this.singletons.eventBus = new EventBus({ maxListeners: 30 });
|
|
213
|
+
}
|
|
214
|
+
return this.singletons.eventBus;
|
|
215
|
+
});
|
|
204
216
|
|
|
217
|
+
// BootstrapTaskManager(冷启动异步任务管理器 — 单例)
|
|
218
|
+
this.register('bootstrapTaskManager', () => {
|
|
219
|
+
if (!this.singletons.bootstrapTaskManager) {
|
|
220
|
+
const eventBus = this.get('eventBus');
|
|
221
|
+
// 延迟 getter: RealtimeService 在 HTTP server 启动后才可用,CLI 模式下不可用
|
|
222
|
+
const getRS = () => {
|
|
223
|
+
try { return _getRealtimeService(); } catch { return null; }
|
|
224
|
+
};
|
|
225
|
+
this.singletons.bootstrapTaskManager = new BootstrapTaskManager({ eventBus, getRealtimeService: getRS });
|
|
226
|
+
}
|
|
227
|
+
return this.singletons.bootstrapTaskManager;
|
|
228
|
+
});
|
|
205
229
|
}
|
|
206
230
|
|
|
207
231
|
/**
|
|
@@ -334,11 +358,13 @@ export class ServiceContainer {
|
|
|
334
358
|
return this.singletons.retrievalFunnel;
|
|
335
359
|
});
|
|
336
360
|
|
|
337
|
-
// JsonVectorAdapter
|
|
361
|
+
// JsonVectorAdapter(同步构造 + 同步 init — 从磁盘 JSON 加载历史向量数据)
|
|
338
362
|
this.register('vectorStore', () => {
|
|
339
363
|
if (!this.singletons.vectorStore) {
|
|
340
364
|
const projectRoot = this.singletons._projectRoot || process.cwd();
|
|
341
|
-
|
|
365
|
+
const store = new JsonVectorAdapter(projectRoot);
|
|
366
|
+
store.initSync(); // 从磁盘加载已有 vector_index.json
|
|
367
|
+
this.singletons.vectorStore = store;
|
|
342
368
|
}
|
|
343
369
|
return this.singletons.vectorStore;
|
|
344
370
|
});
|
|
@@ -475,6 +501,12 @@ export class ServiceContainer {
|
|
|
475
501
|
return this.singletons.toolRegistry;
|
|
476
502
|
});
|
|
477
503
|
|
|
504
|
+
// ProjectGraph (v3.0 AST 结构图 — 懒初始化,首次 get 时构建)
|
|
505
|
+
this.register('projectGraph', () => {
|
|
506
|
+
// 返回已构建的实例;需要外部先调用 buildProjectGraph() 构建
|
|
507
|
+
return this.singletons.projectGraph || null;
|
|
508
|
+
});
|
|
509
|
+
|
|
478
510
|
// AI Provider(供 MCP handler / ChatAgent / 任意服务层使用)
|
|
479
511
|
this.register('aiProvider', () => this.singletons.aiProvider || null);
|
|
480
512
|
|
|
@@ -531,6 +563,33 @@ export class ServiceContainer {
|
|
|
531
563
|
getServiceNames() {
|
|
532
564
|
return Object.keys(this.services);
|
|
533
565
|
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* 构建 ProjectGraph (v3.0 AST 结构图)
|
|
569
|
+
* 应在 bootstrap 流程开始前调用一次
|
|
570
|
+
* @param {string} projectRoot 项目根目录
|
|
571
|
+
* @param {object} [options] 传递给 ProjectGraph.build() 的选项
|
|
572
|
+
* @returns {Promise<import('../core/ast/ProjectGraph.js').default|null>}
|
|
573
|
+
*/
|
|
574
|
+
async buildProjectGraph(projectRoot, options = {}) {
|
|
575
|
+
if (this.singletons.projectGraph) {
|
|
576
|
+
return this.singletons.projectGraph;
|
|
577
|
+
}
|
|
578
|
+
try {
|
|
579
|
+
const graph = await ProjectGraph.build(projectRoot, options);
|
|
580
|
+
this.singletons.projectGraph = graph;
|
|
581
|
+
const overview = graph.getOverview();
|
|
582
|
+
this.logger.info(
|
|
583
|
+
`[ServiceContainer] ProjectGraph built: ${overview.totalClasses} classes, ` +
|
|
584
|
+
`${overview.totalProtocols} protocols, ${overview.totalCategories} categories ` +
|
|
585
|
+
`(${overview.buildTimeMs}ms)`
|
|
586
|
+
);
|
|
587
|
+
return graph;
|
|
588
|
+
} catch (err) {
|
|
589
|
+
this.logger.warn(`[ServiceContainer] ProjectGraph build failed: ${err.message}`);
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
534
593
|
}
|
|
535
594
|
|
|
536
595
|
let containerInstance = null;
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BootstrapTaskManager — 冷启动异步任务管理器
|
|
3
|
+
*
|
|
4
|
+
* 核心职责:
|
|
5
|
+
* 1. 管理 bootstrap 异步任务的生命周期(skeleton → filling → completed)
|
|
6
|
+
* 2. 通过 EventBus 发射进度事件
|
|
7
|
+
* 3. 通过 RealtimeService 推送进度到前端 (Socket.io)
|
|
8
|
+
* 4. 支持查询当前 bootstrap 会话状态
|
|
9
|
+
*
|
|
10
|
+
* 任务状态流:
|
|
11
|
+
* skeleton → filling → completed / failed
|
|
12
|
+
*
|
|
13
|
+
* 事件类型:
|
|
14
|
+
* bootstrap:started — 冷启动开始,携带任务清单
|
|
15
|
+
* bootstrap:task-started — 单个维度/Skill 开始填充
|
|
16
|
+
* bootstrap:task-completed — 单个维度/Skill 填充完成
|
|
17
|
+
* bootstrap:task-failed — 单个任务失败
|
|
18
|
+
* bootstrap:all-completed — 全部任务完成
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import Logger from '../../infrastructure/logging/Logger.js';
|
|
22
|
+
|
|
23
|
+
/** 任务状态枚举 */
|
|
24
|
+
export const TaskStatus = Object.freeze({
|
|
25
|
+
SKELETON: 'skeleton', // 骨架已创建,等待填充
|
|
26
|
+
FILLING: 'filling', // 内容正在填充中
|
|
27
|
+
COMPLETED: 'completed', // 填充完成
|
|
28
|
+
FAILED: 'failed', // 填充失败
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 单个 Bootstrap 会话(一次冷启动的全部上下文)
|
|
33
|
+
*/
|
|
34
|
+
class BootstrapSession {
|
|
35
|
+
constructor(sessionId) {
|
|
36
|
+
this.id = sessionId;
|
|
37
|
+
this.startedAt = Date.now();
|
|
38
|
+
this.completedAt = null;
|
|
39
|
+
this.status = 'running'; // running | completed | failed
|
|
40
|
+
this.tasks = new Map(); // taskId → TaskInfo
|
|
41
|
+
this.summary = null; // 完成后的摘要
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
addTask(taskId, meta) {
|
|
45
|
+
this.tasks.set(taskId, {
|
|
46
|
+
id: taskId,
|
|
47
|
+
status: TaskStatus.SKELETON,
|
|
48
|
+
meta, // { type: 'dimension'|'skill', dimId, label, skillWorthy }
|
|
49
|
+
startedAt: null,
|
|
50
|
+
completedAt: null,
|
|
51
|
+
result: null, // 填充结果摘要
|
|
52
|
+
error: null,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
getTask(taskId) {
|
|
57
|
+
return this.tasks.get(taskId);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
get totalTasks() { return this.tasks.size; }
|
|
61
|
+
get completedTasks() { return [...this.tasks.values()].filter(t => t.status === TaskStatus.COMPLETED).length; }
|
|
62
|
+
get failedTasks() { return [...this.tasks.values()].filter(t => t.status === TaskStatus.FAILED).length; }
|
|
63
|
+
get fillingTasks() { return [...this.tasks.values()].filter(t => t.status === TaskStatus.FILLING).length; }
|
|
64
|
+
get skeletonTasks() { return [...this.tasks.values()].filter(t => t.status === TaskStatus.SKELETON).length; }
|
|
65
|
+
|
|
66
|
+
get isAllDone() {
|
|
67
|
+
return this.skeletonTasks === 0 && this.fillingTasks === 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
get totalToolCalls() {
|
|
71
|
+
let count = 0;
|
|
72
|
+
for (const t of this.tasks.values()) {
|
|
73
|
+
if (t.result?.toolCallCount) count += t.result.toolCallCount;
|
|
74
|
+
}
|
|
75
|
+
return count;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
get progress() {
|
|
79
|
+
const total = this.totalTasks;
|
|
80
|
+
if (total === 0) return 100;
|
|
81
|
+
return Math.round(((this.completedTasks + this.failedTasks) / total) * 100);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
toJSON() {
|
|
85
|
+
return {
|
|
86
|
+
id: this.id,
|
|
87
|
+
status: this.status,
|
|
88
|
+
startedAt: this.startedAt,
|
|
89
|
+
completedAt: this.completedAt,
|
|
90
|
+
progress: this.progress,
|
|
91
|
+
total: this.totalTasks,
|
|
92
|
+
completed: this.completedTasks,
|
|
93
|
+
failed: this.failedTasks,
|
|
94
|
+
filling: this.fillingTasks,
|
|
95
|
+
skeleton: this.skeletonTasks,
|
|
96
|
+
totalToolCalls: this.totalToolCalls,
|
|
97
|
+
tasks: [...this.tasks.values()].map(t => ({
|
|
98
|
+
id: t.id,
|
|
99
|
+
status: t.status,
|
|
100
|
+
meta: t.meta,
|
|
101
|
+
startedAt: t.startedAt,
|
|
102
|
+
completedAt: t.completedAt,
|
|
103
|
+
result: t.result,
|
|
104
|
+
error: t.error,
|
|
105
|
+
})),
|
|
106
|
+
summary: this.summary,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export class BootstrapTaskManager {
|
|
112
|
+
/** @type {BootstrapSession|null} */
|
|
113
|
+
#currentSession = null;
|
|
114
|
+
|
|
115
|
+
/** @type {import('../../infrastructure/event/EventBus.js').EventBus|null} */
|
|
116
|
+
#eventBus = null;
|
|
117
|
+
|
|
118
|
+
/** @type {Function|null} 获取 RealtimeService 的 getter(延迟获取,避免循环依赖) */
|
|
119
|
+
#getRealtimeService = null;
|
|
120
|
+
|
|
121
|
+
constructor({ eventBus, getRealtimeService } = {}) {
|
|
122
|
+
this.#eventBus = eventBus || null;
|
|
123
|
+
this.#getRealtimeService = getRealtimeService || null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ═══════════════════════════════════════════════════════════
|
|
127
|
+
// Session 管理
|
|
128
|
+
// ═══════════════════════════════════════════════════════════
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 启动新的 bootstrap 会话
|
|
132
|
+
*
|
|
133
|
+
* 如果上一个会话仍在运行,自动 abort 后再创建新会话(防止重复触发产出重复 Candidate)。
|
|
134
|
+
*
|
|
135
|
+
* @param {Array<{id: string, meta: object}>} taskDefs — 任务定义列表
|
|
136
|
+
* @returns {BootstrapSession}
|
|
137
|
+
*/
|
|
138
|
+
startSession(taskDefs) {
|
|
139
|
+
// ── 并发锁:如果上一个 session 还在运行,先中止 ──
|
|
140
|
+
if (this.isRunning) {
|
|
141
|
+
Logger.warn(`[Bootstrap] Previous session ${this.#currentSession.id} still running — aborting before starting new session`);
|
|
142
|
+
this.abortSession('Superseded by new bootstrap request');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const sessionId = `bs_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
146
|
+
this.#currentSession = new BootstrapSession(sessionId);
|
|
147
|
+
|
|
148
|
+
for (const { id, meta } of taskDefs) {
|
|
149
|
+
this.#currentSession.addTask(id, meta);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
Logger.info(`[Bootstrap] Session ${sessionId} started with ${taskDefs.length} tasks`);
|
|
153
|
+
this.#emit('bootstrap:started', {
|
|
154
|
+
sessionId,
|
|
155
|
+
tasks: taskDefs.map(t => ({ id: t.id, ...t.meta })),
|
|
156
|
+
total: taskDefs.length,
|
|
157
|
+
startedAt: this.#currentSession.startedAt,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
return this.#currentSession;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 中止当前 bootstrap 会话
|
|
165
|
+
*
|
|
166
|
+
* 将所有未完成的任务标记为 failed,并将 session 标记为 aborted。
|
|
167
|
+
* 异步填充函数通过 `isSessionValid(sessionId)` 检测到 session 已变更后自动退出。
|
|
168
|
+
*
|
|
169
|
+
* @param {string} [reason='Aborted by user']
|
|
170
|
+
*/
|
|
171
|
+
abortSession(reason = 'Aborted by user') {
|
|
172
|
+
const session = this.#currentSession;
|
|
173
|
+
if (!session || session.status !== 'running') return;
|
|
174
|
+
|
|
175
|
+
// 将未完成的任务全部标记 FAILED
|
|
176
|
+
for (const task of session.tasks.values()) {
|
|
177
|
+
if (task.status === TaskStatus.SKELETON || task.status === TaskStatus.FILLING) {
|
|
178
|
+
task.status = TaskStatus.FAILED;
|
|
179
|
+
task.completedAt = Date.now();
|
|
180
|
+
task.error = reason;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
session.status = 'aborted';
|
|
185
|
+
session.completedAt = Date.now();
|
|
186
|
+
session.summary = {
|
|
187
|
+
duration: session.completedAt - session.startedAt,
|
|
188
|
+
totalTasks: session.totalTasks,
|
|
189
|
+
completed: session.completedTasks,
|
|
190
|
+
failed: session.failedTasks,
|
|
191
|
+
aborted: true,
|
|
192
|
+
reason,
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
Logger.info(`[Bootstrap] Session ${session.id} aborted: ${reason}`);
|
|
196
|
+
this.#emit('bootstrap:all-completed', {
|
|
197
|
+
sessionId: session.id,
|
|
198
|
+
summary: session.summary,
|
|
199
|
+
tasks: [...session.tasks.values()].map(t => ({
|
|
200
|
+
id: t.id, status: t.status, meta: t.meta, result: t.result, error: t.error,
|
|
201
|
+
})),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* 验证 sessionId 是否仍然是活跃 session
|
|
207
|
+
*
|
|
208
|
+
* 用于异步填充函数在每次循环迭代前检测:如果 session 已被新请求覆盖,
|
|
209
|
+
* 则当前异步填充应立即停止,避免产出重复内容。
|
|
210
|
+
*
|
|
211
|
+
* @param {string} sessionId
|
|
212
|
+
* @returns {boolean}
|
|
213
|
+
*/
|
|
214
|
+
isSessionValid(sessionId) {
|
|
215
|
+
// Session 在 running 或 completed 状态都有效 — completed 表示维度填充完成,
|
|
216
|
+
// 但 Phase 5.5 Skills 生成仍需运行。只有被新 session 替代时才无效。
|
|
217
|
+
return this.#currentSession?.id === sessionId &&
|
|
218
|
+
(this.#currentSession?.status === 'running' ||
|
|
219
|
+
this.#currentSession?.status === 'completed' ||
|
|
220
|
+
this.#currentSession?.status === 'completed_with_errors');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* 标记单个任务开始填充
|
|
225
|
+
*/
|
|
226
|
+
markTaskFilling(taskId) {
|
|
227
|
+
const session = this.#currentSession;
|
|
228
|
+
if (!session) return;
|
|
229
|
+
const task = session.getTask(taskId);
|
|
230
|
+
if (!task) return;
|
|
231
|
+
|
|
232
|
+
task.status = TaskStatus.FILLING;
|
|
233
|
+
task.startedAt = Date.now();
|
|
234
|
+
|
|
235
|
+
Logger.info(`[Bootstrap] Task "${taskId}" filling started`);
|
|
236
|
+
this.#emit('bootstrap:task-started', {
|
|
237
|
+
sessionId: session.id,
|
|
238
|
+
taskId,
|
|
239
|
+
meta: task.meta,
|
|
240
|
+
progress: session.progress,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* 标记单个任务完成
|
|
246
|
+
* @param {string} taskId
|
|
247
|
+
* @param {object} result — 填充结果摘要 { created, items, ... }
|
|
248
|
+
*/
|
|
249
|
+
markTaskCompleted(taskId, result = {}) {
|
|
250
|
+
const session = this.#currentSession;
|
|
251
|
+
if (!session) return;
|
|
252
|
+
const task = session.getTask(taskId);
|
|
253
|
+
if (!task) return;
|
|
254
|
+
|
|
255
|
+
task.status = TaskStatus.COMPLETED;
|
|
256
|
+
task.completedAt = Date.now();
|
|
257
|
+
task.result = result;
|
|
258
|
+
|
|
259
|
+
Logger.info(`[Bootstrap] Task "${taskId}" completed (${session.completedTasks}/${session.totalTasks})`);
|
|
260
|
+
this.#emit('bootstrap:task-completed', {
|
|
261
|
+
sessionId: session.id,
|
|
262
|
+
taskId,
|
|
263
|
+
meta: task.meta,
|
|
264
|
+
result,
|
|
265
|
+
progress: session.progress,
|
|
266
|
+
completed: session.completedTasks,
|
|
267
|
+
total: session.totalTasks,
|
|
268
|
+
totalToolCalls: session.totalToolCalls,
|
|
269
|
+
elapsedMs: Date.now() - session.startedAt,
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// 检查是否所有任务都已完成
|
|
273
|
+
if (session.isAllDone) {
|
|
274
|
+
this.#finishSession();
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* 标记单个任务失败
|
|
280
|
+
*/
|
|
281
|
+
markTaskFailed(taskId, error) {
|
|
282
|
+
const session = this.#currentSession;
|
|
283
|
+
if (!session) return;
|
|
284
|
+
const task = session.getTask(taskId);
|
|
285
|
+
if (!task) return;
|
|
286
|
+
|
|
287
|
+
task.status = TaskStatus.FAILED;
|
|
288
|
+
task.completedAt = Date.now();
|
|
289
|
+
task.error = typeof error === 'string' ? error : error?.message || 'Unknown error';
|
|
290
|
+
|
|
291
|
+
Logger.warn(`[Bootstrap] Task "${taskId}" failed: ${task.error}`);
|
|
292
|
+
this.#emit('bootstrap:task-failed', {
|
|
293
|
+
sessionId: session.id,
|
|
294
|
+
taskId,
|
|
295
|
+
meta: task.meta,
|
|
296
|
+
error: task.error,
|
|
297
|
+
progress: session.progress,
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// 检查是否所有任务都已完成(包括失败)
|
|
301
|
+
if (session.isAllDone) {
|
|
302
|
+
this.#finishSession();
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ═══════════════════════════════════════════════════════════
|
|
307
|
+
// 查询接口
|
|
308
|
+
// ═══════════════════════════════════════════════════════════
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* 获取当前 session 状态(供 HTTP 轮询)
|
|
312
|
+
*/
|
|
313
|
+
getSessionStatus() {
|
|
314
|
+
if (!this.#currentSession) {
|
|
315
|
+
return { status: 'idle', message: 'No active bootstrap session' };
|
|
316
|
+
}
|
|
317
|
+
return this.#currentSession.toJSON();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* 是否有正在进行的 bootstrap
|
|
322
|
+
*/
|
|
323
|
+
get isRunning() {
|
|
324
|
+
return this.#currentSession?.status === 'running';
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ═══════════════════════════════════════════════════════════
|
|
328
|
+
// 通用进度推送(供 refine 等非 bootstrap 流程复用双通道)
|
|
329
|
+
// ═══════════════════════════════════════════════════════════
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* 向 EventBus + Socket.io 发射任意进度事件
|
|
333
|
+
*
|
|
334
|
+
* 用途:不走 bootstrap session 模型的长操作(如 AI 润色)也能复用
|
|
335
|
+
* 同一套 EventBus + RealtimeService 双通道推送。
|
|
336
|
+
*
|
|
337
|
+
* @param {string} eventName — 事件名(如 'refine:started')
|
|
338
|
+
* @param {object} data — 事件负载
|
|
339
|
+
*/
|
|
340
|
+
emitProgress(eventName, data) {
|
|
341
|
+
this.#emit(eventName, data);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ═══════════════════════════════════════════════════════════
|
|
345
|
+
// 内部方法
|
|
346
|
+
// ═══════════════════════════════════════════════════════════
|
|
347
|
+
|
|
348
|
+
#finishSession() {
|
|
349
|
+
const session = this.#currentSession;
|
|
350
|
+
if (!session) return;
|
|
351
|
+
|
|
352
|
+
session.status = session.failedTasks > 0 ? 'completed_with_errors' : 'completed';
|
|
353
|
+
session.completedAt = Date.now();
|
|
354
|
+
session.summary = {
|
|
355
|
+
duration: session.completedAt - session.startedAt,
|
|
356
|
+
totalTasks: session.totalTasks,
|
|
357
|
+
completed: session.completedTasks,
|
|
358
|
+
failed: session.failedTasks,
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const durationSec = ((session.completedAt - session.startedAt) / 1000).toFixed(1);
|
|
362
|
+
Logger.info(`[Bootstrap] Session ${session.id} finished: ${session.completedTasks} completed, ${session.failedTasks} failed (${durationSec}s)`);
|
|
363
|
+
|
|
364
|
+
this.#emit('bootstrap:all-completed', {
|
|
365
|
+
sessionId: session.id,
|
|
366
|
+
summary: session.summary,
|
|
367
|
+
tasks: [...session.tasks.values()].map(t => ({
|
|
368
|
+
id: t.id,
|
|
369
|
+
status: t.status,
|
|
370
|
+
meta: t.meta,
|
|
371
|
+
result: t.result,
|
|
372
|
+
error: t.error,
|
|
373
|
+
})),
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* 发射事件到 EventBus + 推送到前端 Socket.io
|
|
379
|
+
*/
|
|
380
|
+
#emit(eventName, data) {
|
|
381
|
+
// EventBus(供后端监听者使用)
|
|
382
|
+
if (this.#eventBus) {
|
|
383
|
+
try {
|
|
384
|
+
this.#eventBus.emit(eventName, data);
|
|
385
|
+
} catch (e) {
|
|
386
|
+
Logger.warn(`[Bootstrap] EventBus emit failed: ${e.message}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// RealtimeService(推送到前端 Socket.io)
|
|
391
|
+
if (this.#getRealtimeService) {
|
|
392
|
+
try {
|
|
393
|
+
const realtime = this.#getRealtimeService();
|
|
394
|
+
realtime.broadcastEvent(eventName, data);
|
|
395
|
+
} catch {
|
|
396
|
+
// RealtimeService 可能未初始化(CLI 模式),静默忽略
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
@@ -79,6 +79,15 @@ export class CandidateFileWriter {
|
|
|
79
79
|
if (h !== '[]') lines.push(`_statusHistory: ${h}`);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
// ── 判断 code 是否为 Markdown 内容(项目特写)──
|
|
83
|
+
// 注意: 只匹配 Markdown heading (# + 空格),避免 ObjC 的 #import/#define 误判
|
|
84
|
+
const isSnapshot = candidate.code &&
|
|
85
|
+
(candidate.code.includes('— 项目特写') || /^#{1,3}\s/.test(candidate.code.trimStart()));
|
|
86
|
+
|
|
87
|
+
if (isSnapshot) {
|
|
88
|
+
lines.push('_format: snapshot');
|
|
89
|
+
}
|
|
90
|
+
|
|
82
91
|
// _contentHash 占位索引(后续替换为真实 hash)
|
|
83
92
|
const hashIdx = lines.length;
|
|
84
93
|
lines.push(''); // 占位行
|
|
@@ -86,35 +95,57 @@ export class CandidateFileWriter {
|
|
|
86
95
|
lines.push('---');
|
|
87
96
|
lines.push('');
|
|
88
97
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
lines.push(
|
|
97
|
-
lines.push(candidate.code);
|
|
98
|
-
lines.push('```');
|
|
98
|
+
if (isSnapshot) {
|
|
99
|
+
// ── 项目特写:Markdown 内容直接输出,不包裹 code fence ──
|
|
100
|
+
// 修复: AI 有时在 code 字段中输出字面 \n 而非真实换行 — 统一转换
|
|
101
|
+
let snapshotCode = candidate.code;
|
|
102
|
+
if (snapshotCode.includes('\\n') && snapshotCode.split('\n').length < 5) {
|
|
103
|
+
snapshotCode = snapshotCode.replace(/\\n/g, '\n');
|
|
104
|
+
}
|
|
105
|
+
lines.push(snapshotCode);
|
|
99
106
|
lines.push('');
|
|
100
|
-
}
|
|
101
107
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
// ── Reasoning → blockquote ──
|
|
109
|
+
if (reasoning) {
|
|
110
|
+
const r = typeof reasoning === 'string' ? JSON.parse(reasoning) : reasoning;
|
|
111
|
+
lines.push('<!-- reasoning -->');
|
|
112
|
+
if (r.whyStandard) {
|
|
113
|
+
lines.push(`> **审核理由**: ${r.whyStandard}`);
|
|
114
|
+
}
|
|
115
|
+
if (r.sources?.length > 0) {
|
|
116
|
+
lines.push(`> **来源**: ${r.sources.join(', ')}`);
|
|
117
|
+
}
|
|
109
118
|
lines.push('');
|
|
110
119
|
}
|
|
111
|
-
|
|
112
|
-
|
|
120
|
+
} else {
|
|
121
|
+
// ── 传统代码:保持原有 fence + heading 格式 ──
|
|
122
|
+
const title = metadata.title || metadata.description || `Candidate ${candidate.id.slice(0, 8)}`;
|
|
123
|
+
lines.push(`## ${title}`);
|
|
124
|
+
lines.push('');
|
|
125
|
+
|
|
126
|
+
if (candidate.code) {
|
|
127
|
+
lines.push(`\`\`\`${candidate.language || 'swift'}`);
|
|
128
|
+
lines.push(candidate.code);
|
|
129
|
+
lines.push('```');
|
|
113
130
|
lines.push('');
|
|
114
|
-
|
|
115
|
-
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (reasoning) {
|
|
134
|
+
const r = typeof reasoning === 'string' ? JSON.parse(reasoning) : reasoning;
|
|
135
|
+
if (r.whyStandard) {
|
|
136
|
+
lines.push('## Why Standard');
|
|
137
|
+
lines.push('');
|
|
138
|
+
lines.push(r.whyStandard);
|
|
139
|
+
lines.push('');
|
|
140
|
+
}
|
|
141
|
+
if (r.sources?.length > 0) {
|
|
142
|
+
lines.push('## Sources');
|
|
143
|
+
lines.push('');
|
|
144
|
+
for (const src of r.sources) {
|
|
145
|
+
lines.push(`- ${src}`);
|
|
146
|
+
}
|
|
147
|
+
lines.push('');
|
|
116
148
|
}
|
|
117
|
-
lines.push('');
|
|
118
149
|
}
|
|
119
150
|
}
|
|
120
151
|
|
|
@@ -211,8 +242,10 @@ export class CandidateFileWriter {
|
|
|
211
242
|
if (title) {
|
|
212
243
|
const slug = title
|
|
213
244
|
.toLowerCase()
|
|
214
|
-
.replace(/[^\
|
|
245
|
+
.replace(/[^\p{L}\p{N}\s-]/gu, '') // 保留 Unicode 字母(含CJK)、数字、空格、连字符
|
|
215
246
|
.replace(/\s+/g, '-')
|
|
247
|
+
.replace(/-{2,}/g, '-') // 合并连续连字符
|
|
248
|
+
.replace(/^-|-$/g, '') // 去除首尾连字符
|
|
216
249
|
.slice(0, 60);
|
|
217
250
|
if (slug.length >= 3) return `${slug}.md`;
|
|
218
251
|
}
|
|
@@ -325,9 +358,17 @@ export function parseCandidateMarkdown(content, relPath) {
|
|
|
325
358
|
const bodyMatch = content.match(/^---[\s\S]*?---\s*\r?\n([\s\S]*)$/);
|
|
326
359
|
if (bodyMatch) {
|
|
327
360
|
const body = bodyMatch[1];
|
|
328
|
-
|
|
329
|
-
if (
|
|
330
|
-
|
|
361
|
+
|
|
362
|
+
if (data._format === 'snapshot') {
|
|
363
|
+
// 项目特写格式:Markdown 内容直接输出,reasoning 在 <!-- reasoning --> 之后
|
|
364
|
+
const parts = body.split('<!-- reasoning -->');
|
|
365
|
+
data._bodyCode = parts[0].trim();
|
|
366
|
+
} else {
|
|
367
|
+
// 传统格式:从 code fence 提取
|
|
368
|
+
const codeMatch = body.match(/```\w*\n([\s\S]*?)```/);
|
|
369
|
+
if (codeMatch) {
|
|
370
|
+
data._bodyCode = codeMatch[1].trimEnd();
|
|
371
|
+
}
|
|
331
372
|
}
|
|
332
373
|
}
|
|
333
374
|
|