autosnippet 2.9.0 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/bin/cli.js +5 -33
- package/config/constitution.yaml +9 -2
- package/dashboard/dist/assets/{icons-CH-H9x0E.js → icons-BkT3XrKf.js} +105 -100
- package/dashboard/dist/assets/index-BsB7DzW4.css +1 -0
- package/dashboard/dist/assets/index-DdmQMrJJ.js +155 -0
- package/dashboard/dist/index.html +3 -3
- package/lib/cli/AiScanService.js +13 -11
- package/lib/cli/KnowledgeSyncService.js +343 -0
- package/lib/cli/SetupService.js +8 -26
- package/lib/core/gateway/GatewayActionRegistry.js +48 -58
- package/lib/domain/index.js +16 -11
- package/lib/domain/knowledge/KnowledgeEntry.js +351 -0
- package/lib/domain/knowledge/KnowledgeRepository.js +123 -0
- package/lib/domain/knowledge/Lifecycle.js +109 -0
- package/lib/domain/knowledge/index.js +27 -0
- package/lib/domain/knowledge/values/Constraints.js +125 -0
- package/lib/domain/knowledge/values/Content.js +86 -0
- package/lib/domain/knowledge/values/Quality.js +93 -0
- package/lib/domain/knowledge/values/Reasoning.js +69 -0
- package/lib/domain/knowledge/values/Relations.js +168 -0
- package/lib/domain/knowledge/values/Stats.js +87 -0
- package/lib/domain/knowledge/values/index.js +9 -0
- package/lib/external/ai/AiProvider.js +48 -0
- package/lib/external/mcp/McpServer.js +7 -5
- package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +2 -2
- package/lib/external/mcp/handlers/bootstrap.js +116 -11
- package/lib/external/mcp/handlers/browse.js +77 -73
- package/lib/external/mcp/handlers/candidate.js +29 -276
- package/lib/external/mcp/handlers/guard.js +2 -0
- package/lib/external/mcp/handlers/knowledge.js +205 -0
- package/lib/external/mcp/handlers/structure.js +25 -23
- package/lib/external/mcp/handlers/system.js +10 -12
- package/lib/external/mcp/tools.js +125 -138
- package/lib/http/HttpServer.js +4 -8
- package/lib/http/routes/extract.js +48 -4
- package/lib/http/routes/knowledge.js +246 -0
- package/lib/http/routes/search.js +12 -17
- package/lib/infrastructure/database/migrations/016_unified_knowledge_entries.js +395 -0
- package/lib/infrastructure/external/XcodeAutomation.js +187 -103
- package/lib/injection/ServiceContainer.js +49 -60
- package/lib/repository/knowledge/KnowledgeRepository.impl.js +373 -0
- package/lib/service/automation/DirectiveDetector.js +2 -3
- package/lib/service/automation/FileWatcher.js +67 -28
- package/lib/service/automation/XcodeIntegration.js +931 -156
- package/lib/service/automation/handlers/AlinkHandler.js +6 -4
- package/lib/service/automation/handlers/CreateHandler.js +53 -18
- package/lib/service/automation/handlers/GuardHandler.js +183 -20
- package/lib/service/automation/handlers/SearchHandler.js +35 -17
- package/lib/service/chat/CandidateGuardrail.js +1 -1
- package/lib/service/chat/ChatAgent.js +46 -45
- package/lib/service/chat/ContextWindow.js +5 -5
- package/lib/service/chat/ProducerAgent.js +7 -7
- package/lib/service/chat/tools.js +130 -123
- package/lib/service/guard/GuardCheckEngine.js +114 -10
- package/lib/service/guard/GuardService.js +59 -48
- package/lib/service/knowledge/ConfidenceRouter.js +159 -0
- package/lib/service/knowledge/KnowledgeFileWriter.js +602 -0
- package/lib/service/knowledge/KnowledgeService.js +725 -0
- package/lib/service/search/SearchEngine.js +92 -19
- package/lib/service/skills/SignalCollector.js +12 -7
- package/lib/service/skills/SkillAdvisor.js +13 -11
- package/lib/service/snippet/SnippetFactory.js +5 -5
- package/package.json +1 -1
- package/scripts/install-cursor-skill.js +0 -6
- package/scripts/migrate-md-to-knowledge.mjs +364 -0
- package/skills/autosnippet-analysis/SKILL.md +15 -7
- package/skills/autosnippet-candidates/SKILL.md +6 -6
- package/skills/autosnippet-coldstart/SKILL.md +7 -3
- package/skills/autosnippet-concepts/SKILL.md +7 -6
- package/skills/autosnippet-create/SKILL.md +13 -13
- package/skills/autosnippet-intent/SKILL.md +3 -2
- package/skills/autosnippet-lifecycle/SKILL.md +5 -5
- package/skills/autosnippet-recipes/SKILL.md +16 -4
- package/templates/constitution.yaml +1 -1
- package/templates/copilot-instructions.md +6 -6
- package/templates/recipes-setup/README.md +3 -3
- package/dashboard/dist/assets/index-CqJRvYRL.js +0 -197
- package/dashboard/dist/assets/index-DICm9PNa.css +0 -1
- package/lib/cli/CandidateSyncService.js +0 -261
- package/lib/cli/SyncService.js +0 -356
- package/lib/domain/candidate/Candidate.js +0 -196
- package/lib/domain/candidate/CandidateRepository.js +0 -107
- package/lib/domain/candidate/Reasoning.js +0 -52
- package/lib/domain/recipe/Recipe.js +0 -421
- package/lib/domain/recipe/RecipeRepository.js +0 -54
- package/lib/domain/types/CandidateStatus.js +0 -52
- package/lib/http/routes/candidates.js +0 -559
- package/lib/http/routes/recipes.js +0 -397
- package/lib/repository/candidate/CandidateRepository.impl.js +0 -230
- package/lib/repository/recipe/RecipeRepository.impl.js +0 -498
- package/lib/service/candidate/CandidateAggregator.js +0 -52
- package/lib/service/candidate/CandidateFileWriter.js +0 -383
- package/lib/service/candidate/CandidateService.js +0 -1001
- package/lib/service/recipe/RecipeFileWriter.js +0 -514
- package/lib/service/recipe/RecipeService.js +0 -786
- package/lib/service/recipe/RecipeStatsTracker.js +0 -148
|
@@ -1,1001 +0,0 @@
|
|
|
1
|
-
import { Candidate, CandidateStatus, Reasoning } from '../../domain/index.js';
|
|
2
|
-
import { isValidStateTransition } from '../../domain/types/CandidateStatus.js';
|
|
3
|
-
import { KnowledgeType, inferKind } from '../../domain/recipe/Recipe.js';
|
|
4
|
-
import Logger from '../../infrastructure/logging/Logger.js';
|
|
5
|
-
import { ValidationError, ConflictError, NotFoundError } from '../../shared/errors/index.js';
|
|
6
|
-
import { v4 as uuidv4 } from 'uuid';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* CandidateService
|
|
10
|
-
* 管理代码候选项的完整生命周期
|
|
11
|
-
* 包括创建、批准、驳回和应用到 Recipe 的业务逻辑
|
|
12
|
-
*/
|
|
13
|
-
export class CandidateService {
|
|
14
|
-
constructor(candidateRepository, auditLogger, gateway, { fileWriter, skillHooks } = {}) {
|
|
15
|
-
this.candidateRepository = candidateRepository;
|
|
16
|
-
this.auditLogger = auditLogger;
|
|
17
|
-
this.gateway = gateway;
|
|
18
|
-
this.fileWriter = fileWriter || null;
|
|
19
|
-
this.skillHooks = skillHooks || null;
|
|
20
|
-
this.logger = Logger.getInstance();
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* 创建新的候选项
|
|
25
|
-
*/
|
|
26
|
-
async createCandidate(data, context) {
|
|
27
|
-
try {
|
|
28
|
-
// 验证输入
|
|
29
|
-
this._validateCreateInput(data);
|
|
30
|
-
|
|
31
|
-
// 创建实体
|
|
32
|
-
const candidate = new Candidate({
|
|
33
|
-
id: uuidv4(),
|
|
34
|
-
code: data.code,
|
|
35
|
-
language: data.language,
|
|
36
|
-
category: data.category,
|
|
37
|
-
source: data.source || 'manual',
|
|
38
|
-
reasoning: data.reasoning
|
|
39
|
-
? new Reasoning({
|
|
40
|
-
whyStandard: data.reasoning.whyStandard,
|
|
41
|
-
sources: data.reasoning.sources || [],
|
|
42
|
-
qualitySignals: data.reasoning.qualitySignals || {},
|
|
43
|
-
alternatives: data.reasoning.alternatives || [],
|
|
44
|
-
confidence: data.reasoning.confidence || 0.7,
|
|
45
|
-
})
|
|
46
|
-
: null,
|
|
47
|
-
createdBy: context.userId,
|
|
48
|
-
status: CandidateStatus.PENDING,
|
|
49
|
-
metadata: data.metadata || {},
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
if (!candidate.isValid()) {
|
|
53
|
-
throw new ValidationError('Invalid candidate data');
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// ── SkillHooks: onCandidateSubmit ──
|
|
57
|
-
if (this.skillHooks) {
|
|
58
|
-
const hookResult = await this.skillHooks.run('onCandidateSubmit', candidate, {
|
|
59
|
-
userId: context.userId,
|
|
60
|
-
});
|
|
61
|
-
if (hookResult?.block) {
|
|
62
|
-
throw new ValidationError(`SkillHook blocked: ${hookResult.reason || 'unknown'}`);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// 保存到数据库
|
|
67
|
-
const created = await this.candidateRepository.create(candidate);
|
|
68
|
-
|
|
69
|
-
// 落盘 .md 文件(Git 友好)
|
|
70
|
-
if (this.fileWriter) {
|
|
71
|
-
this.fileWriter.persistCandidate(created);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// 审计日志
|
|
75
|
-
await this.auditLogger.log({
|
|
76
|
-
action: 'create_candidate',
|
|
77
|
-
resource: `candidate:${created.id}`,
|
|
78
|
-
actor: context.userId,
|
|
79
|
-
result: 'success',
|
|
80
|
-
data: { codeLength: created.code.length },
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
this.logger.info('Candidate created', {
|
|
84
|
-
candidateId: created.id,
|
|
85
|
-
createdBy: context.userId,
|
|
86
|
-
codeLength: created.code.length,
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
return created;
|
|
90
|
-
} catch (error) {
|
|
91
|
-
this.logger.error('Error creating candidate', {
|
|
92
|
-
error: error.message,
|
|
93
|
-
data,
|
|
94
|
-
});
|
|
95
|
-
throw error;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* 批准候选项
|
|
101
|
-
*/
|
|
102
|
-
async approveCandidate(candidateId, context) {
|
|
103
|
-
try {
|
|
104
|
-
const candidate = await this.candidateRepository.findById(candidateId);
|
|
105
|
-
|
|
106
|
-
if (!candidate) {
|
|
107
|
-
throw new NotFoundError('Candidate not found', 'candidate', candidateId);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// 检查状态转移的合法性
|
|
111
|
-
if (!isValidStateTransition(candidate.status, CandidateStatus.APPROVED)) {
|
|
112
|
-
throw new ConflictError(
|
|
113
|
-
`Cannot approve candidate in ${candidate.status} status`,
|
|
114
|
-
`Invalid transition from ${candidate.status} to APPROVED`
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// 执行批准
|
|
119
|
-
candidate.approve(context.userId);
|
|
120
|
-
|
|
121
|
-
// 保存
|
|
122
|
-
const updated = await this.candidateRepository.update(candidateId, {
|
|
123
|
-
status: candidate.status,
|
|
124
|
-
status_history_json: JSON.stringify(candidate.statusHistory),
|
|
125
|
-
approved_by: candidate.approvedBy,
|
|
126
|
-
approved_at: candidate.approvedAt,
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
// 落盘 .md 文件
|
|
130
|
-
if (this.fileWriter) {
|
|
131
|
-
this.fileWriter.persistCandidate(updated);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// 审计日志
|
|
135
|
-
await this.auditLogger.log({
|
|
136
|
-
action: 'approve_candidate',
|
|
137
|
-
resource: `candidate:${candidateId}`,
|
|
138
|
-
actor: context.userId,
|
|
139
|
-
result: 'success',
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
this.logger.info('Candidate approved', {
|
|
143
|
-
candidateId,
|
|
144
|
-
approvedBy: context.userId,
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
return updated;
|
|
148
|
-
} catch (error) {
|
|
149
|
-
this.logger.error('Error approving candidate', {
|
|
150
|
-
candidateId,
|
|
151
|
-
error: error.message,
|
|
152
|
-
});
|
|
153
|
-
throw error;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
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
|
-
|
|
198
|
-
/**
|
|
199
|
-
* 驳回候选项
|
|
200
|
-
*/
|
|
201
|
-
async rejectCandidate(candidateId, reason, context) {
|
|
202
|
-
try {
|
|
203
|
-
const candidate = await this.candidateRepository.findById(candidateId);
|
|
204
|
-
|
|
205
|
-
if (!candidate) {
|
|
206
|
-
throw new NotFoundError('Candidate not found', 'candidate', candidateId);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// 检查状态转移的合法性
|
|
210
|
-
if (!isValidStateTransition(candidate.status, CandidateStatus.REJECTED)) {
|
|
211
|
-
throw new ConflictError(
|
|
212
|
-
`Cannot reject candidate in ${candidate.status} status`,
|
|
213
|
-
`Invalid transition from ${candidate.status} to REJECTED`
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
if (!reason || reason.trim().length === 0) {
|
|
218
|
-
throw new ValidationError('Rejection reason is required');
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// 执行驳回
|
|
222
|
-
candidate.reject(reason, context.userId);
|
|
223
|
-
|
|
224
|
-
// 保存
|
|
225
|
-
const updated = await this.candidateRepository.update(candidateId, {
|
|
226
|
-
status: candidate.status,
|
|
227
|
-
status_history_json: JSON.stringify(candidate.statusHistory),
|
|
228
|
-
rejection_reason: candidate.rejectionReason,
|
|
229
|
-
rejected_by: candidate.rejectedBy,
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
// 落盘 .md 文件(保留驳回原因)
|
|
233
|
-
if (this.fileWriter) {
|
|
234
|
-
this.fileWriter.persistCandidate(updated);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// 审计日志
|
|
238
|
-
await this.auditLogger.log({
|
|
239
|
-
action: 'reject_candidate',
|
|
240
|
-
resource: `candidate:${candidateId}`,
|
|
241
|
-
actor: context.userId,
|
|
242
|
-
result: 'success',
|
|
243
|
-
data: { reason },
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
this.logger.info('Candidate rejected', {
|
|
247
|
-
candidateId,
|
|
248
|
-
rejectedBy: context.userId,
|
|
249
|
-
reason,
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
return updated;
|
|
253
|
-
} catch (error) {
|
|
254
|
-
this.logger.error('Error rejecting candidate', {
|
|
255
|
-
candidateId,
|
|
256
|
-
error: error.message,
|
|
257
|
-
});
|
|
258
|
-
throw error;
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* 将候选项应用到 Recipe(标记为已被用于发布)
|
|
264
|
-
*/
|
|
265
|
-
async applyToRecipe(candidateId, recipeId, context) {
|
|
266
|
-
try {
|
|
267
|
-
const candidate = await this.candidateRepository.findById(candidateId);
|
|
268
|
-
|
|
269
|
-
if (!candidate) {
|
|
270
|
-
throw new NotFoundError('Candidate not found', 'candidate', candidateId);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// 检查状态转移的合法性
|
|
274
|
-
if (!isValidStateTransition(candidate.status, CandidateStatus.APPLIED)) {
|
|
275
|
-
throw new ConflictError(
|
|
276
|
-
`Cannot apply candidate in ${candidate.status} status`,
|
|
277
|
-
`Invalid transition from ${candidate.status} to APPLIED`
|
|
278
|
-
);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// 执行应用
|
|
282
|
-
candidate.applyToRecipe(recipeId);
|
|
283
|
-
|
|
284
|
-
// 保存
|
|
285
|
-
const updated = await this.candidateRepository.update(candidateId, {
|
|
286
|
-
status: candidate.status,
|
|
287
|
-
status_history_json: JSON.stringify(candidate.statusHistory),
|
|
288
|
-
applied_recipe_id: candidate.appliedRecipeId,
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
// 落盘 .md 文件
|
|
292
|
-
if (this.fileWriter) {
|
|
293
|
-
this.fileWriter.persistCandidate(updated);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// 审计日志
|
|
297
|
-
await this.auditLogger.log({
|
|
298
|
-
action: 'apply_candidate_to_recipe',
|
|
299
|
-
resource: `candidate:${candidateId}`,
|
|
300
|
-
actor: context.userId,
|
|
301
|
-
result: 'success',
|
|
302
|
-
data: { recipeId },
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
this.logger.info('Candidate applied to recipe', {
|
|
306
|
-
candidateId,
|
|
307
|
-
recipeId,
|
|
308
|
-
appliedBy: context.userId,
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
return updated;
|
|
312
|
-
} catch (error) {
|
|
313
|
-
this.logger.error('Error applying candidate to recipe', {
|
|
314
|
-
candidateId,
|
|
315
|
-
recipeId,
|
|
316
|
-
error: error.message,
|
|
317
|
-
});
|
|
318
|
-
throw error;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* 将候选项提升为 Recipe(一键转化核心路径)
|
|
324
|
-
*
|
|
325
|
-
* 从 Candidate 数据自动创建 Recipe 并标记 Candidate 为 APPLIED。
|
|
326
|
-
* Candidate 必须处于 APPROVED 状态。
|
|
327
|
-
*
|
|
328
|
-
* @param {string} candidateId
|
|
329
|
-
* @param {object} [overrides] - 可选覆盖字段 (title, category, knowledgeType 等)
|
|
330
|
-
* @param {object} context
|
|
331
|
-
* @returns {Promise<{recipe: object, candidate: object}>}
|
|
332
|
-
*/
|
|
333
|
-
async promoteCandidateToRecipe(candidateId, overrides = {}, context) {
|
|
334
|
-
const candidate = await this.candidateRepository.findById(candidateId);
|
|
335
|
-
if (!candidate) {
|
|
336
|
-
throw new NotFoundError('Candidate not found', 'candidate', candidateId);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// 只有 APPROVED 状态可以提升
|
|
340
|
-
if (candidate.status !== CandidateStatus.APPROVED) {
|
|
341
|
-
throw new ConflictError(
|
|
342
|
-
`Cannot promote candidate in ${candidate.status} status — must be APPROVED first`,
|
|
343
|
-
`Current status: ${candidate.status}`,
|
|
344
|
-
);
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// 从 candidate 元数据 + overrides 构建 Recipe 数据
|
|
348
|
-
const meta = candidate.metadata || {};
|
|
349
|
-
const knowledgeType = overrides.knowledgeType || meta.knowledgeType || KnowledgeType.CODE_PATTERN;
|
|
350
|
-
|
|
351
|
-
const recipeData = {
|
|
352
|
-
title: overrides.title || meta.title || `Recipe from ${candidateId.slice(0, 8)}`,
|
|
353
|
-
description: overrides.description || meta.description || meta.summary || '',
|
|
354
|
-
language: overrides.language || candidate.language,
|
|
355
|
-
category: overrides.category || candidate.category,
|
|
356
|
-
trigger: overrides.trigger || meta.trigger || '',
|
|
357
|
-
knowledgeType,
|
|
358
|
-
kind: overrides.kind || inferKind(knowledgeType),
|
|
359
|
-
complexity: overrides.complexity || meta.complexity || 'intermediate',
|
|
360
|
-
scope: overrides.scope || meta.scope || null,
|
|
361
|
-
tags: overrides.tags || meta.tags || [],
|
|
362
|
-
content: {
|
|
363
|
-
pattern: candidate.code || '',
|
|
364
|
-
rationale: overrides.rationale || meta.rationale || (candidate.reasoning?.whyStandard) || '',
|
|
365
|
-
steps: meta.steps || [],
|
|
366
|
-
codeChanges: meta.codeChanges || [],
|
|
367
|
-
verification: meta.verification || null,
|
|
368
|
-
markdown: '',
|
|
369
|
-
},
|
|
370
|
-
constraints: meta.constraints || {},
|
|
371
|
-
relations: overrides.relations || meta.relations || {},
|
|
372
|
-
sourceCandidate: candidate.id,
|
|
373
|
-
};
|
|
374
|
-
|
|
375
|
-
// 注入 RecipeService 创建 Recipe(通过 container)
|
|
376
|
-
const { getServiceContainer } = await import('../../injection/ServiceContainer.js');
|
|
377
|
-
const container = getServiceContainer();
|
|
378
|
-
const recipeService = container.get('recipeService');
|
|
379
|
-
|
|
380
|
-
const recipe = await recipeService.createRecipe(recipeData, context);
|
|
381
|
-
|
|
382
|
-
// 标记 Candidate 为 APPLIED(复用已有方法逻辑)
|
|
383
|
-
candidate.applyToRecipe(recipe.id);
|
|
384
|
-
const updatedCandidate = await this.candidateRepository.update(candidateId, {
|
|
385
|
-
status: candidate.status,
|
|
386
|
-
status_history_json: JSON.stringify(candidate.statusHistory),
|
|
387
|
-
applied_recipe_id: candidate.appliedRecipeId,
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
if (this.fileWriter) {
|
|
391
|
-
this.fileWriter.persistCandidate(updatedCandidate);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
await this.auditLogger.log({
|
|
395
|
-
action: 'promote_candidate_to_recipe',
|
|
396
|
-
resource: `candidate:${candidateId}`,
|
|
397
|
-
actor: context.userId,
|
|
398
|
-
result: 'success',
|
|
399
|
-
data: { recipeId: recipe.id, recipeTitle: recipe.title },
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
this.logger.info('Candidate promoted to Recipe', {
|
|
403
|
-
candidateId,
|
|
404
|
-
recipeId: recipe.id,
|
|
405
|
-
promotedBy: context.userId,
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
return { recipe, candidate: updatedCandidate };
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
/**
|
|
412
|
-
* 查询候选项列表
|
|
413
|
-
*/
|
|
414
|
-
async listCandidates(filters = {}, pagination = {}) {
|
|
415
|
-
try {
|
|
416
|
-
const { status, language, category, createdBy, source } = filters;
|
|
417
|
-
const { page = 1, pageSize = 20 } = pagination;
|
|
418
|
-
|
|
419
|
-
// 组合查询 — 支持多条件同时筛选
|
|
420
|
-
const dbFilters = {};
|
|
421
|
-
if (status) dbFilters.status = status;
|
|
422
|
-
if (language) dbFilters.language = language;
|
|
423
|
-
if (category) dbFilters.category = category;
|
|
424
|
-
if (source) dbFilters.source = source;
|
|
425
|
-
if (createdBy) dbFilters.created_by = createdBy;
|
|
426
|
-
|
|
427
|
-
return this.candidateRepository.findWithPagination(dbFilters, { page, pageSize });
|
|
428
|
-
} catch (error) {
|
|
429
|
-
this.logger.error('Error listing candidates', {
|
|
430
|
-
error: error.message,
|
|
431
|
-
filters,
|
|
432
|
-
});
|
|
433
|
-
throw error;
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
/**
|
|
438
|
-
* 搜索候选项
|
|
439
|
-
*/
|
|
440
|
-
async searchCandidates(keyword, pagination = {}) {
|
|
441
|
-
try {
|
|
442
|
-
const { page = 1, pageSize = 20 } = pagination;
|
|
443
|
-
return this.candidateRepository.search(keyword, { page, pageSize });
|
|
444
|
-
} catch (error) {
|
|
445
|
-
this.logger.error('Error searching candidates', {
|
|
446
|
-
keyword,
|
|
447
|
-
error: error.message,
|
|
448
|
-
});
|
|
449
|
-
throw error;
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
/**
|
|
454
|
-
* 获取候选项统计
|
|
455
|
-
*/
|
|
456
|
-
async getCandidateStats() {
|
|
457
|
-
try {
|
|
458
|
-
return this.candidateRepository.getStats();
|
|
459
|
-
} catch (error) {
|
|
460
|
-
this.logger.error('Error getting candidate stats', {
|
|
461
|
-
error: error.message,
|
|
462
|
-
});
|
|
463
|
-
throw error;
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
/**
|
|
468
|
-
* ① 结构补齐 — 填充候选缺失的结构性语义字段
|
|
469
|
-
*
|
|
470
|
-
* 目标字段:rationale / knowledgeType / complexity / scope / steps / constraints
|
|
471
|
-
* 写入策略:只填空不覆盖(已有值的字段不动)
|
|
472
|
-
* 建议在 refineBootstrapCandidates()(② 内容润色)之前执行。
|
|
473
|
-
*
|
|
474
|
-
* @param {string[]} candidateIds - 候选 ID 列表
|
|
475
|
-
* @param {object} aiProvider - AiProvider 实例(由调用方注入)
|
|
476
|
-
* @param {object} context
|
|
477
|
-
* @returns {Promise<{enriched: number, results: object[]}>}
|
|
478
|
-
*/
|
|
479
|
-
async enrichCandidates(candidateIds, aiProvider, context) {
|
|
480
|
-
if (!aiProvider || typeof aiProvider.enrichCandidates !== 'function') {
|
|
481
|
-
throw new ValidationError('AI provider with enrichCandidates capability is required');
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
// 1. 从 DB 取出候选
|
|
485
|
-
const candidates = [];
|
|
486
|
-
for (const id of candidateIds) {
|
|
487
|
-
const c = await this.candidateRepository.findById(id);
|
|
488
|
-
if (c) candidates.push(c);
|
|
489
|
-
}
|
|
490
|
-
if (candidates.length === 0) throw new NotFoundError('No candidates found');
|
|
491
|
-
|
|
492
|
-
// 2. 构建 AI 输入(合并 metadata 字段到顶层供 AI 分析)
|
|
493
|
-
const aiInput = candidates.map(c => {
|
|
494
|
-
const m = c.metadata || {};
|
|
495
|
-
return {
|
|
496
|
-
code: c.code,
|
|
497
|
-
language: c.language,
|
|
498
|
-
category: c.category,
|
|
499
|
-
title: m.title || '',
|
|
500
|
-
description: m.description || m.summary || '',
|
|
501
|
-
summary: m.summary || '',
|
|
502
|
-
rationale: m.rationale || '',
|
|
503
|
-
knowledgeType: m.knowledgeType || '',
|
|
504
|
-
complexity: m.complexity || '',
|
|
505
|
-
scope: m.scope || '',
|
|
506
|
-
steps: m.steps || [],
|
|
507
|
-
constraints: m.constraints || {},
|
|
508
|
-
};
|
|
509
|
-
});
|
|
510
|
-
|
|
511
|
-
// 3. 调用 AI 补全
|
|
512
|
-
const enrichResults = await aiProvider.enrichCandidates(aiInput);
|
|
513
|
-
|
|
514
|
-
// 4. 合并结果写回 metadata
|
|
515
|
-
let enrichedCount = 0;
|
|
516
|
-
const results = [];
|
|
517
|
-
for (const enriched of enrichResults) {
|
|
518
|
-
const idx = enriched.index;
|
|
519
|
-
if (idx == null || idx < 0 || idx >= candidates.length) continue;
|
|
520
|
-
const candidate = candidates[idx];
|
|
521
|
-
const meta = { ...(candidate.metadata || {}) };
|
|
522
|
-
let changed = false;
|
|
523
|
-
|
|
524
|
-
const SEMANTIC_KEYS = ['rationale', 'knowledgeType', 'complexity', 'scope', 'steps', 'constraints'];
|
|
525
|
-
for (const key of SEMANTIC_KEYS) {
|
|
526
|
-
if (enriched[key] !== undefined && enriched[key] !== null && enriched[key] !== '') {
|
|
527
|
-
// 只填充缺失的字段
|
|
528
|
-
if (!meta[key] || (typeof meta[key] === 'string' && meta[key].trim() === '') ||
|
|
529
|
-
(Array.isArray(meta[key]) && meta[key].length === 0) ||
|
|
530
|
-
(typeof meta[key] === 'object' && !Array.isArray(meta[key]) && Object.keys(meta[key]).length === 0)) {
|
|
531
|
-
meta[key] = enriched[key];
|
|
532
|
-
changed = true;
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
if (changed) {
|
|
538
|
-
const enrichedCandidate = await this.candidateRepository.update(candidate.id, { metadata_json: JSON.stringify(meta) });
|
|
539
|
-
// 落盘 .md 文件
|
|
540
|
-
if (this.fileWriter && enrichedCandidate) {
|
|
541
|
-
this.fileWriter.persistCandidate(enrichedCandidate);
|
|
542
|
-
}
|
|
543
|
-
enrichedCount++;
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
results.push({
|
|
547
|
-
id: candidate.id,
|
|
548
|
-
enriched: changed,
|
|
549
|
-
filledFields: Object.keys(enriched).filter(k => k !== 'index'),
|
|
550
|
-
});
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
// 5. 审计
|
|
554
|
-
await this.auditLogger.log({
|
|
555
|
-
action: 'enrich_candidates',
|
|
556
|
-
resource: 'candidates',
|
|
557
|
-
actor: context.userId || 'system',
|
|
558
|
-
result: 'success',
|
|
559
|
-
data: { total: candidates.length, enriched: enrichedCount },
|
|
560
|
-
});
|
|
561
|
-
|
|
562
|
-
return { enriched: enrichedCount, total: candidates.length, results };
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
/**
|
|
566
|
-
* ② 内容润色 — 逐条精炼 Bootstrap 候选的内容质量
|
|
567
|
-
*
|
|
568
|
-
* 目标字段:summary / agentNotes / relations / confidence / insight / tags / code
|
|
569
|
-
* 写入策略:覆盖改善(AI 给出更好内容就替换)
|
|
570
|
-
* 建议在 enrichCandidates()(① 结构补齐)之后执行。
|
|
571
|
-
*
|
|
572
|
-
* 对 source='bootstrap' 的候选逐条调用 AI,不会删除/合并候选——仅原地更新。
|
|
573
|
-
*
|
|
574
|
-
* @param {object} aiProvider AI provider 实例
|
|
575
|
-
* @param {object} [options]
|
|
576
|
-
* @param {string[]} [options.candidateIds] 指定候选 ID(默认全部 bootstrap)
|
|
577
|
-
* @param {string} [options.userPrompt] 用户自定义润色提示词(追加到 AI prompt)
|
|
578
|
-
* @param {boolean} [options.dryRun] 仅预览不写入
|
|
579
|
-
* @param {object} [context] { userId }
|
|
580
|
-
* @returns {Promise<{refined: number, total: number, errors: object[], results: object[]}>}
|
|
581
|
-
*/
|
|
582
|
-
async refineBootstrapCandidates(aiProvider, options = {}, context = { userId: 'system' }) {
|
|
583
|
-
if (!aiProvider || typeof aiProvider.chat !== 'function') {
|
|
584
|
-
throw new ValidationError('AI provider with chat capability is required');
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
const onProgress = typeof options.onProgress === 'function' ? options.onProgress : null;
|
|
588
|
-
|
|
589
|
-
// 1. 收集候选
|
|
590
|
-
let candidates;
|
|
591
|
-
if (options.candidateIds?.length) {
|
|
592
|
-
candidates = [];
|
|
593
|
-
for (const id of options.candidateIds) {
|
|
594
|
-
const c = await this.candidateRepository.findById(id);
|
|
595
|
-
if (c) candidates.push(c);
|
|
596
|
-
}
|
|
597
|
-
} else {
|
|
598
|
-
// 查全部 bootstrap 候选(PENDING 状态)
|
|
599
|
-
const all = await this.candidateRepository.findAll({
|
|
600
|
-
source: 'bootstrap',
|
|
601
|
-
status: 'pending',
|
|
602
|
-
});
|
|
603
|
-
candidates = all || [];
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
if (candidates.length === 0) {
|
|
607
|
-
return { refined: 0, total: 0, errors: [], results: [] };
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
// 通知:润色开始
|
|
611
|
-
onProgress?.('refine:started', {
|
|
612
|
-
total: candidates.length,
|
|
613
|
-
candidateIds: candidates.map(c => c.id),
|
|
614
|
-
});
|
|
615
|
-
|
|
616
|
-
// 2. 收集同批次标题列表(供 AI 推断关系)
|
|
617
|
-
const allTitles = candidates.map(c => (c.metadata || {}).title || '').filter(Boolean);
|
|
618
|
-
|
|
619
|
-
// 3. 逐条润色
|
|
620
|
-
const results = [];
|
|
621
|
-
const errors = [];
|
|
622
|
-
let refined = 0;
|
|
623
|
-
let processed = 0;
|
|
624
|
-
|
|
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
|
-
|
|
638
|
-
try {
|
|
639
|
-
const meta = { ...(candidate.metadata || {}) };
|
|
640
|
-
const prompt = this._buildRefinePrompt(candidate, allTitles, options.userPrompt);
|
|
641
|
-
const response = await aiProvider.chat(prompt, { temperature: 0.3 });
|
|
642
|
-
const parsed = aiProvider.extractJSON(response, '{', '}');
|
|
643
|
-
|
|
644
|
-
if (!parsed) {
|
|
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
|
-
});
|
|
652
|
-
continue;
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
if (options.dryRun) {
|
|
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
|
-
});
|
|
663
|
-
continue;
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
// 合并 AI 改进到 metadata
|
|
667
|
-
let changed = false;
|
|
668
|
-
|
|
669
|
-
if (parsed.summary && parsed.summary !== meta.summary) {
|
|
670
|
-
meta.summary = parsed.summary;
|
|
671
|
-
changed = true;
|
|
672
|
-
}
|
|
673
|
-
if (parsed.agentNotes && Array.isArray(parsed.agentNotes)) {
|
|
674
|
-
meta.agentNotes = parsed.agentNotes;
|
|
675
|
-
changed = true;
|
|
676
|
-
}
|
|
677
|
-
if (parsed.relations && Array.isArray(parsed.relations) && parsed.relations.length > 0) {
|
|
678
|
-
meta.relations = parsed.relations;
|
|
679
|
-
changed = true;
|
|
680
|
-
}
|
|
681
|
-
if (typeof parsed.confidence === 'number' && parsed.confidence !== 0.6) {
|
|
682
|
-
meta.refinedConfidence = parsed.confidence;
|
|
683
|
-
changed = true;
|
|
684
|
-
}
|
|
685
|
-
if (parsed.insight) {
|
|
686
|
-
meta.aiInsight = parsed.insight;
|
|
687
|
-
changed = true;
|
|
688
|
-
}
|
|
689
|
-
if (parsed.tags && Array.isArray(parsed.tags)) {
|
|
690
|
-
// 合并 AI 建议的 tag(不重复)
|
|
691
|
-
const existing = new Set(meta.tags || []);
|
|
692
|
-
for (const t of parsed.tags) {
|
|
693
|
-
if (!existing.has(t)) {
|
|
694
|
-
(meta.tags = meta.tags || []).push(t);
|
|
695
|
-
changed = true;
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
// 更新 code(如果 AI 改写了文档 — 增强校验防止截断/片段代码/类型变更)
|
|
701
|
-
let newCode = candidate.code;
|
|
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 类型变更
|
|
710
|
-
newCode = parsed.code;
|
|
711
|
-
changed = true;
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
if (changed) {
|
|
715
|
-
await this.candidateRepository.update(candidate.id, {
|
|
716
|
-
code: newCode,
|
|
717
|
-
metadata_json: JSON.stringify(meta),
|
|
718
|
-
});
|
|
719
|
-
// 落盘 .md 文件
|
|
720
|
-
const updated = await this.candidateRepository.findById(candidate.id);
|
|
721
|
-
if (this.fileWriter && updated) {
|
|
722
|
-
this.fileWriter.persistCandidate(updated);
|
|
723
|
-
}
|
|
724
|
-
refined++;
|
|
725
|
-
}
|
|
726
|
-
|
|
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
|
-
});
|
|
739
|
-
} catch (err) {
|
|
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
|
-
});
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
// 通知:全部完成
|
|
755
|
-
onProgress?.('refine:completed', {
|
|
756
|
-
total: candidates.length,
|
|
757
|
-
refined,
|
|
758
|
-
failed: errors.length,
|
|
759
|
-
});
|
|
760
|
-
|
|
761
|
-
// 4. 审计
|
|
762
|
-
await this.auditLogger.log({
|
|
763
|
-
action: 'refine_bootstrap_candidates',
|
|
764
|
-
resource: 'candidates',
|
|
765
|
-
actor: context.userId || 'system',
|
|
766
|
-
result: 'success',
|
|
767
|
-
data: { total: candidates.length, refined, errors: errors.length },
|
|
768
|
-
});
|
|
769
|
-
|
|
770
|
-
return { refined, total: candidates.length, errors, results };
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
/**
|
|
774
|
-
* 构建 AI 润色 prompt
|
|
775
|
-
* @private
|
|
776
|
-
*/
|
|
777
|
-
_buildRefinePrompt(candidate, allTitles, userPrompt = '') {
|
|
778
|
-
const meta = candidate.metadata || {};
|
|
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
|
-
}
|
|
802
|
-
|
|
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
|
-
}
|
|
821
|
-
|
|
822
|
-
return `# Role
|
|
823
|
-
You are a senior software architect refining a Bootstrap knowledge candidate.\n${userSection}
|
|
824
|
-
|
|
825
|
-
# Current Candidate
|
|
826
|
-
Title: ${meta.title || '(untitled)'}
|
|
827
|
-
Category: ${candidate.category || 'bootstrap'}
|
|
828
|
-
Language: ${candidate.language || 'unknown'}
|
|
829
|
-
Summary: ${meta.summary || '(none)'}
|
|
830
|
-
Tags: ${(meta.tags || []).join(', ')}
|
|
831
|
-
Content Type: ${codeContentType}
|
|
832
|
-
|
|
833
|
-
## Content
|
|
834
|
-
\`\`\`
|
|
835
|
-
${code.substring(0, 3000)}
|
|
836
|
-
\`\`\`
|
|
837
|
-
|
|
838
|
-
# Sibling Candidates (same bootstrap batch)
|
|
839
|
-
${otherTitles || '(none)'}
|
|
840
|
-
|
|
841
|
-
# Tasks
|
|
842
|
-
1. **summary**: Write a precise 1-2 sentence summary (in Chinese) that a coding agent can use to decide relevance
|
|
843
|
-
2. **insight**: One high-level architectural insight about this pattern (in Chinese, nullable)
|
|
844
|
-
3. **agentNotes**: Array of 2-4 actionable rules for the coding agent (in Chinese)
|
|
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>" }]
|
|
846
|
-
5. **confidence**: Float 0-1 rating of this candidate's value (0.3=low, 0.6=medium, 0.9=high)
|
|
847
|
-
6. **tags**: Additional relevant tags (array of strings, optional)
|
|
848
|
-
${codeTask}
|
|
849
|
-
|
|
850
|
-
# Output
|
|
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!' : ''}
|
|
852
|
-
Do NOT wrap in markdown code blocks. Return raw JSON only.`;
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
/**
|
|
856
|
-
* 统一候选创建入口 — MCP / ChatAgent / Bootstrap 共用
|
|
857
|
-
*
|
|
858
|
-
* 从扁平的工具参数(或手工构建的对象)中提取 code / language / category /
|
|
859
|
-
* reasoning / metadata,处理默认值和中英文字段映射,确保所有路径的字段
|
|
860
|
-
* 映射逻辑一致。
|
|
861
|
-
*
|
|
862
|
-
* @param {object} item 候选数据(扁平字段或含 metadata 的对象)
|
|
863
|
-
* @param {string} source 来源标识(mcp / agent / bootstrap)
|
|
864
|
-
* @param {object} [extraMeta]额外 metadata(如 targetName)
|
|
865
|
-
* @param {object} [context] 上下文 { userId }
|
|
866
|
-
* @returns {Promise<object>} 创建后的 Candidate
|
|
867
|
-
*/
|
|
868
|
-
async createFromToolParams(item, source, extraMeta = {}, context = { userId: 'system' }) {
|
|
869
|
-
const metadata = { ...this._buildMetadataFromFlat(item), ...extraMeta };
|
|
870
|
-
const reasoning = this._buildReasoning(item);
|
|
871
|
-
|
|
872
|
-
// ── 去重: 同标题候选已存在于 DB 中则跳过(删除后可重新生成) ──
|
|
873
|
-
const title = item.title || metadata.title || '';
|
|
874
|
-
if (title && this._existsCandidateWithTitle(title)) {
|
|
875
|
-
this.logger.info('Skipped duplicate candidate (title already exists)', { title, source });
|
|
876
|
-
return { skipped: true, reason: 'duplicate_title', title };
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
// 如果 reasoning 为空对象(缺少 whyStandard),生成默认值
|
|
880
|
-
if (!reasoning.whyStandard) {
|
|
881
|
-
reasoning.whyStandard = item.rationale || item.summary || item.description || `Submitted via ${source}`;
|
|
882
|
-
reasoning.sources = reasoning.sources?.length ? reasoning.sources : [source];
|
|
883
|
-
reasoning.confidence = reasoning.confidence || 0.7;
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
return this.createCandidate({
|
|
887
|
-
code: item.code || '',
|
|
888
|
-
language: item.language || '',
|
|
889
|
-
category: item.category || 'general',
|
|
890
|
-
source,
|
|
891
|
-
reasoning,
|
|
892
|
-
metadata,
|
|
893
|
-
}, context);
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
/**
|
|
897
|
-
* 从扁平工具参数构建 metadata 对象(等价于旧 buildCandidateMetadata)
|
|
898
|
-
*/
|
|
899
|
-
_buildMetadataFromFlat(obj) {
|
|
900
|
-
const m = {};
|
|
901
|
-
if (obj.title) m.title = obj.title;
|
|
902
|
-
if (obj.description) m.description = obj.description;
|
|
903
|
-
if (obj.summary_cn || obj.summary) m.summary = obj.summary_cn || obj.summary;
|
|
904
|
-
if (obj.summary_en) m.summary_en = obj.summary_en;
|
|
905
|
-
if (obj.trigger) m.trigger = obj.trigger;
|
|
906
|
-
if (obj.usageGuide_cn || obj.usageGuide) m.usageGuide = obj.usageGuide_cn || obj.usageGuide;
|
|
907
|
-
if (obj.usageGuide_en) m.usageGuide_en = obj.usageGuide_en;
|
|
908
|
-
if (obj.knowledgeType) m.knowledgeType = obj.knowledgeType;
|
|
909
|
-
if (obj.complexity) m.complexity = obj.complexity;
|
|
910
|
-
if (obj.scope) m.scope = obj.scope;
|
|
911
|
-
if (obj.tags) m.tags = obj.tags;
|
|
912
|
-
if (obj.rationale) m.rationale = obj.rationale;
|
|
913
|
-
if (obj.steps) m.steps = obj.steps;
|
|
914
|
-
if (obj.codeChanges) m.codeChanges = obj.codeChanges;
|
|
915
|
-
if (obj.verification) m.verification = obj.verification;
|
|
916
|
-
if (obj.headers) m.headers = obj.headers;
|
|
917
|
-
if (obj.constraints) m.constraints = obj.constraints;
|
|
918
|
-
if (obj.relations) m.relations = obj.relations;
|
|
919
|
-
if (obj.quality) m.quality = obj.quality;
|
|
920
|
-
if (obj.sourceFile) m.sourceFile = obj.sourceFile;
|
|
921
|
-
return m;
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
/**
|
|
925
|
-
* 从工具参数构建 Reasoning 值对象数据
|
|
926
|
-
*/
|
|
927
|
-
_buildReasoning(obj) {
|
|
928
|
-
const r = obj.reasoning;
|
|
929
|
-
if (!r || !r.whyStandard) return {};
|
|
930
|
-
return {
|
|
931
|
-
whyStandard: r.whyStandard,
|
|
932
|
-
sources: Array.isArray(r.sources) ? r.sources : [],
|
|
933
|
-
confidence: typeof r.confidence === 'number' ? r.confidence : 0.7,
|
|
934
|
-
qualitySignals: r.qualitySignals || {},
|
|
935
|
-
alternatives: Array.isArray(r.alternatives) ? r.alternatives : [],
|
|
936
|
-
};
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
/**
|
|
940
|
-
* 验证创建输入
|
|
941
|
-
*/
|
|
942
|
-
_validateCreateInput(data) {
|
|
943
|
-
if (!data.code || data.code.trim().length === 0) {
|
|
944
|
-
throw new ValidationError('Code is required');
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
if (!data.language || data.language.trim().length === 0) {
|
|
948
|
-
throw new ValidationError('Language is required');
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
if (!data.category || data.category.trim().length === 0) {
|
|
952
|
-
throw new ValidationError('Category is required');
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
if (data.code.length > 50 * 1024) {
|
|
956
|
-
throw new ValidationError('Code exceeds maximum size of 50KB');
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
// reasoning 校验 — 提供明确错误信息而非泛化的 "Invalid candidate data"
|
|
960
|
-
if (!data.reasoning) {
|
|
961
|
-
throw new ValidationError('Reasoning is required — provide { whyStandard, sources, confidence }');
|
|
962
|
-
}
|
|
963
|
-
if (!data.reasoning.whyStandard || (typeof data.reasoning.whyStandard === 'string' && data.reasoning.whyStandard.trim().length === 0)) {
|
|
964
|
-
throw new ValidationError('reasoning.whyStandard is required — explain why this code is worth capturing');
|
|
965
|
-
}
|
|
966
|
-
if (!Array.isArray(data.reasoning.sources) || data.reasoning.sources.length === 0) {
|
|
967
|
-
throw new ValidationError('reasoning.sources must be a non-empty array — list at least one source file or reference');
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
// metadata 大小限制
|
|
971
|
-
if (data.metadata) {
|
|
972
|
-
const metaStr = typeof data.metadata === 'string' ? data.metadata : JSON.stringify(data.metadata);
|
|
973
|
-
if (metaStr.length > 200 * 1024) {
|
|
974
|
-
throw new ValidationError('Metadata exceeds maximum size of 200KB');
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
/**
|
|
980
|
-
* 检查是否已存在同标题的候选(DB 中当前存在的)
|
|
981
|
-
* @param {string} title
|
|
982
|
-
* @returns {boolean}
|
|
983
|
-
*/
|
|
984
|
-
_existsCandidateWithTitle(title) {
|
|
985
|
-
try {
|
|
986
|
-
const db = this.candidateRepository.db;
|
|
987
|
-
const titleLower = (title || '').toLowerCase().trim();
|
|
988
|
-
if (!titleLower) return false;
|
|
989
|
-
|
|
990
|
-
// metadata_json 中搜索 title 字段
|
|
991
|
-
const row = db.prepare(
|
|
992
|
-
"SELECT id FROM candidates WHERE LOWER(json_extract(metadata_json, '$.title')) = ?"
|
|
993
|
-
).get(titleLower);
|
|
994
|
-
return !!row;
|
|
995
|
-
} catch {
|
|
996
|
-
return false;
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
export default CandidateService;
|