autosnippet 2.8.3 → 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.
Files changed (110) hide show
  1. package/README.md +5 -5
  2. package/bin/cli.js +5 -33
  3. package/config/constitution.yaml +9 -2
  4. package/dashboard/dist/assets/{icons-B_Xg4B-s.js → icons-BkT3XrKf.js} +105 -100
  5. package/dashboard/dist/assets/index-BsB7DzW4.css +1 -0
  6. package/dashboard/dist/assets/index-DdmQMrJJ.js +155 -0
  7. package/dashboard/dist/index.html +3 -3
  8. package/lib/cli/AiScanService.js +13 -11
  9. package/lib/cli/KnowledgeSyncService.js +343 -0
  10. package/lib/cli/SetupService.js +9 -27
  11. package/lib/core/ast/ProjectGraph.js +160 -0
  12. package/lib/core/gateway/GatewayActionRegistry.js +48 -58
  13. package/lib/domain/index.js +16 -11
  14. package/lib/domain/knowledge/KnowledgeEntry.js +351 -0
  15. package/lib/domain/knowledge/KnowledgeRepository.js +123 -0
  16. package/lib/domain/knowledge/Lifecycle.js +109 -0
  17. package/lib/domain/knowledge/index.js +27 -0
  18. package/lib/domain/knowledge/values/Constraints.js +125 -0
  19. package/lib/domain/knowledge/values/Content.js +86 -0
  20. package/lib/domain/knowledge/values/Quality.js +93 -0
  21. package/lib/domain/knowledge/values/Reasoning.js +69 -0
  22. package/lib/domain/knowledge/values/Relations.js +168 -0
  23. package/lib/domain/knowledge/values/Stats.js +87 -0
  24. package/lib/domain/knowledge/values/index.js +9 -0
  25. package/lib/external/ai/AiProvider.js +48 -0
  26. package/lib/external/ai/providers/GoogleGeminiProvider.js +12 -3
  27. package/lib/external/mcp/McpServer.js +7 -5
  28. package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +3 -2
  29. package/lib/external/mcp/handlers/bootstrap.js +121 -12
  30. package/lib/external/mcp/handlers/browse.js +77 -73
  31. package/lib/external/mcp/handlers/candidate.js +29 -276
  32. package/lib/external/mcp/handlers/guard.js +2 -0
  33. package/lib/external/mcp/handlers/knowledge.js +205 -0
  34. package/lib/external/mcp/handlers/skill.js +4 -2
  35. package/lib/external/mcp/handlers/structure.js +25 -23
  36. package/lib/external/mcp/handlers/system.js +10 -12
  37. package/lib/external/mcp/tools.js +125 -138
  38. package/lib/http/HttpServer.js +4 -8
  39. package/lib/http/middleware/requestLogger.js +3 -3
  40. package/lib/http/routes/ai.js +17 -1
  41. package/lib/http/routes/extract.js +48 -4
  42. package/lib/http/routes/knowledge.js +246 -0
  43. package/lib/http/routes/search.js +12 -17
  44. package/lib/http/routes/skills.js +44 -1
  45. package/lib/infrastructure/cache/GraphCache.js +143 -0
  46. package/lib/infrastructure/database/migrations/015_create_token_usage.js +27 -0
  47. package/lib/infrastructure/database/migrations/016_unified_knowledge_entries.js +395 -0
  48. package/lib/infrastructure/external/XcodeAutomation.js +187 -103
  49. package/lib/infrastructure/realtime/RealtimeService.js +14 -2
  50. package/lib/injection/ServiceContainer.js +164 -63
  51. package/lib/repository/knowledge/KnowledgeRepository.impl.js +373 -0
  52. package/lib/repository/token/TokenUsageStore.js +162 -0
  53. package/lib/service/automation/DirectiveDetector.js +2 -3
  54. package/lib/service/automation/FileWatcher.js +67 -28
  55. package/lib/service/automation/XcodeIntegration.js +931 -156
  56. package/lib/service/automation/handlers/AlinkHandler.js +6 -4
  57. package/lib/service/automation/handlers/CreateHandler.js +53 -18
  58. package/lib/service/automation/handlers/GuardHandler.js +183 -20
  59. package/lib/service/automation/handlers/SearchHandler.js +35 -17
  60. package/lib/service/chat/AnalystAgent.js +25 -14
  61. package/lib/service/chat/CandidateGuardrail.js +1 -1
  62. package/lib/service/chat/ChatAgent.js +280 -48
  63. package/lib/service/chat/ContextWindow.js +92 -8
  64. package/lib/service/chat/HandoffProtocol.js +26 -1
  65. package/lib/service/chat/ProducerAgent.js +11 -9
  66. package/lib/service/chat/tools.js +298 -194
  67. package/lib/service/guard/GuardCheckEngine.js +114 -10
  68. package/lib/service/guard/GuardService.js +59 -48
  69. package/lib/service/knowledge/ConfidenceRouter.js +159 -0
  70. package/lib/service/knowledge/KnowledgeFileWriter.js +602 -0
  71. package/lib/service/knowledge/KnowledgeService.js +725 -0
  72. package/lib/service/search/SearchEngine.js +92 -19
  73. package/lib/service/skills/SignalCollector.js +15 -9
  74. package/lib/service/skills/SkillAdvisor.js +13 -11
  75. package/lib/service/snippet/SnippetFactory.js +5 -5
  76. package/lib/service/spm/SpmService.js +119 -18
  77. package/package.json +1 -1
  78. package/scripts/install-cursor-skill.js +0 -6
  79. package/scripts/migrate-md-to-knowledge.mjs +364 -0
  80. package/skills/autosnippet-analysis/SKILL.md +15 -7
  81. package/skills/autosnippet-candidates/SKILL.md +6 -6
  82. package/skills/autosnippet-coldstart/SKILL.md +7 -3
  83. package/skills/autosnippet-concepts/SKILL.md +7 -6
  84. package/skills/autosnippet-create/SKILL.md +13 -13
  85. package/skills/autosnippet-intent/SKILL.md +3 -2
  86. package/skills/autosnippet-lifecycle/SKILL.md +5 -5
  87. package/skills/autosnippet-recipes/SKILL.md +16 -4
  88. package/templates/constitution.yaml +1 -1
  89. package/templates/copilot-instructions.md +6 -6
  90. package/templates/recipes-setup/README.md +3 -3
  91. package/dashboard/dist/assets/index-CkIih2CC.css +0 -1
  92. package/dashboard/dist/assets/index-Duc8Qk-c.js +0 -197
  93. package/lib/cli/CandidateSyncService.js +0 -261
  94. package/lib/cli/SyncService.js +0 -356
  95. package/lib/domain/candidate/Candidate.js +0 -196
  96. package/lib/domain/candidate/CandidateRepository.js +0 -107
  97. package/lib/domain/candidate/Reasoning.js +0 -52
  98. package/lib/domain/recipe/Recipe.js +0 -421
  99. package/lib/domain/recipe/RecipeRepository.js +0 -54
  100. package/lib/domain/types/CandidateStatus.js +0 -52
  101. package/lib/http/routes/candidates.js +0 -559
  102. package/lib/http/routes/recipes.js +0 -397
  103. package/lib/repository/candidate/CandidateRepository.impl.js +0 -230
  104. package/lib/repository/recipe/RecipeRepository.impl.js +0 -498
  105. package/lib/service/candidate/CandidateAggregator.js +0 -52
  106. package/lib/service/candidate/CandidateFileWriter.js +0 -383
  107. package/lib/service/candidate/CandidateService.js +0 -973
  108. package/lib/service/recipe/RecipeFileWriter.js +0 -514
  109. package/lib/service/recipe/RecipeService.js +0 -786
  110. package/lib/service/recipe/RecipeStatsTracker.js +0 -148
@@ -1,559 +0,0 @@
1
- /**
2
- * 候选项 API 路由
3
- * 管理代码候选项的 CRUD 和生命周期操作
4
- */
5
-
6
- import express from 'express';
7
- import { asyncHandler } from '../middleware/errorHandler.js';
8
- import { getServiceContainer } from '../../injection/ServiceContainer.js';
9
- import { NotFoundError, ValidationError } from '../../shared/errors/index.js';
10
- import Logger from '../../infrastructure/logging/Logger.js';
11
- import { getContext, safeInt } from '../utils/routeHelpers.js';
12
-
13
- const router = express.Router();
14
- const logger = Logger.getInstance();
15
-
16
- const MAX_BATCH_SIZE = 100;
17
-
18
- /**
19
- * GET /api/v1/candidates
20
- * 获取候选项列表(支持筛选和分页)
21
- */
22
- router.get('/', asyncHandler(async (req, res) => {
23
- const { status, language, category, keyword } = req.query;
24
- const page = safeInt(req.query.page, 1);
25
- const pageSize = safeInt(req.query.limit, 20, 1, 1000);
26
-
27
- const container = getServiceContainer();
28
- const candidateService = container.get('candidateService');
29
-
30
- if (keyword) {
31
- const result = await candidateService.searchCandidates(keyword, { page, pageSize });
32
- return res.json({ success: true, data: result });
33
- }
34
-
35
- const filters = {};
36
- if (status) filters.status = status;
37
- if (language) filters.language = language;
38
- if (category) filters.category = category;
39
-
40
- const result = await candidateService.listCandidates(filters, { page, pageSize });
41
- res.json({ success: true, data: result });
42
- }));
43
-
44
- /**
45
- * GET /api/v1/candidates/stats
46
- * 获取候选项统计
47
- */
48
- router.get('/stats', asyncHandler(async (req, res) => {
49
- const container = getServiceContainer();
50
- const candidateService = container.get('candidateService');
51
- const stats = await candidateService.getCandidateStats();
52
- res.json({ success: true, data: stats });
53
- }));
54
-
55
- /**
56
- * GET /api/v1/candidates/:id
57
- * 获取候选项详情
58
- */
59
- router.get('/:id', asyncHandler(async (req, res) => {
60
- const { id } = req.params;
61
- const container = getServiceContainer();
62
- const candidateRepo = container.get('candidateRepository');
63
- const candidate = await candidateRepo.findById(id);
64
-
65
- if (!candidate) {
66
- throw new NotFoundError('Candidate not found', 'candidate', id);
67
- }
68
-
69
- res.json({ success: true, data: candidate });
70
- }));
71
-
72
- /**
73
- * POST /api/v1/candidates
74
- * 创建新候选项 (Gateway 管控: 权限 + 宪法 + 审计)
75
- * 自动查重: 创建后通过 ChatAgent check_duplicate 工具检测重复
76
- */
77
- router.post('/', asyncHandler(async (req, res) => {
78
- const { code, language, category, source, reasoning, metadata } = req.body;
79
-
80
- if (!code || !language || !category) {
81
- throw new ValidationError('code, language and category are required');
82
- }
83
-
84
- const result = await req.gw('candidate:create', 'candidates', {
85
- code, language, category, source, reasoning, metadata,
86
- });
87
-
88
- // 自动查重(非阻塞 — AI 不可用时不影响提交)
89
- let duplicateCheck = null;
90
- try {
91
- const container = getServiceContainer();
92
- const chatAgent = container.get('chatAgent');
93
- const candidateForCheck = {
94
- title: metadata?.title || '',
95
- summary: metadata?.summary_cn || metadata?.summary || '',
96
- code: code,
97
- usageGuide: metadata?.usageGuide_cn || metadata?.usageGuide || '',
98
- };
99
- duplicateCheck = await chatAgent.executeTool('check_duplicate', {
100
- candidate: candidateForCheck,
101
- });
102
- } catch (err) {
103
- logger.warn('自动查重失败,不阻塞创建', { error: err.message });
104
- }
105
-
106
- res.status(201).json({
107
- success: true,
108
- data: result.data,
109
- requestId: result.requestId,
110
- duplicateCheck,
111
- });
112
- }));
113
-
114
- /**
115
- * POST /api/v1/candidates/batch-approve
116
- * 批量批准候选项
117
- */
118
- router.post('/batch-approve', asyncHandler(async (req, res) => {
119
- const { ids } = req.body;
120
-
121
- if (!Array.isArray(ids) || ids.length === 0) {
122
- throw new ValidationError('ids array is required and must not be empty');
123
- }
124
- if (ids.length > MAX_BATCH_SIZE) {
125
- throw new ValidationError(`Batch size exceeds limit of ${MAX_BATCH_SIZE}`);
126
- }
127
-
128
- const container = getServiceContainer();
129
- const candidateService = container.get('candidateService');
130
- const context = getContext(req);
131
-
132
- const results = await Promise.allSettled(
133
- ids.map(id => candidateService.approveCandidate(id, context)),
134
- );
135
-
136
- const approved = results.filter(r => r.status === 'fulfilled').map(r => r.value);
137
- const failed = results
138
- .map((r, i) => r.status === 'rejected' ? { id: ids[i], error: r.reason?.message } : null)
139
- .filter(Boolean);
140
-
141
- res.json({
142
- success: true,
143
- data: { approved, failed, total: ids.length, successCount: approved.length, failureCount: failed.length },
144
- });
145
- }));
146
-
147
- /**
148
- * POST /api/v1/candidates/batch-reject
149
- * 批量驳回候选项
150
- */
151
- router.post('/batch-reject', asyncHandler(async (req, res) => {
152
- const { ids, reasoning } = req.body;
153
-
154
- if (!Array.isArray(ids) || ids.length === 0) {
155
- throw new ValidationError('ids array is required and must not be empty');
156
- }
157
- if (ids.length > MAX_BATCH_SIZE) {
158
- throw new ValidationError(`Batch size exceeds limit of ${MAX_BATCH_SIZE}`);
159
- }
160
- if (!reasoning) {
161
- throw new ValidationError('reasoning is required for rejection');
162
- }
163
-
164
- const container = getServiceContainer();
165
- const candidateService = container.get('candidateService');
166
- const context = getContext(req);
167
-
168
- const results = await Promise.allSettled(
169
- ids.map(id => candidateService.rejectCandidate(id, reasoning, context)),
170
- );
171
-
172
- const rejected = results.filter(r => r.status === 'fulfilled').map(r => r.value);
173
- const failed = results
174
- .map((r, i) => r.status === 'rejected' ? { id: ids[i], error: r.reason?.message } : null)
175
- .filter(Boolean);
176
-
177
- res.json({
178
- success: true,
179
- data: { rejected, failed, total: ids.length, successCount: rejected.length, failureCount: failed.length },
180
- });
181
- }));
182
-
183
- /**
184
- * PATCH /api/v1/candidates/:id/approve
185
- * 批准候选项 (Gateway 管控)
186
- */
187
- router.patch('/:id/approve', asyncHandler(async (req, res) => {
188
- const { id } = req.params;
189
-
190
- const result = await req.gw('candidate:approve', 'candidates', {
191
- candidateId: id,
192
- });
193
-
194
- res.json({ success: true, data: result.data, requestId: result.requestId });
195
- }));
196
-
197
- /**
198
- * PATCH /api/v1/candidates/:id/reject
199
- * 驳回候选项 (Gateway 管控)
200
- */
201
- router.patch('/:id/reject', asyncHandler(async (req, res) => {
202
- const { id } = req.params;
203
- const { reasoning } = req.body;
204
-
205
- if (!reasoning) {
206
- throw new ValidationError('reasoning is required for rejection');
207
- }
208
-
209
- const result = await req.gw('candidate:reject', 'candidates', {
210
- candidateId: id,
211
- reason: reasoning,
212
- });
213
-
214
- res.json({ success: true, data: result.data, requestId: result.requestId });
215
- }));
216
-
217
- /**
218
- * DELETE /api/v1/candidates/:id
219
- * 删除候选项 (Gateway 管控: 权限 + 宪法 + 审计)
220
- */
221
- router.delete('/:id', asyncHandler(async (req, res) => {
222
- const { id } = req.params;
223
- const result = await req.gw('candidate:delete', 'candidates', {
224
- candidateId: id,
225
- confirmed: true,
226
- });
227
- res.json({ success: true, requestId: result.requestId });
228
- }));
229
-
230
- /**
231
- * POST /api/v1/candidates/batch-delete
232
- * 批量删除候选项(按 targetName / category)
233
- */
234
- router.post('/batch-delete', asyncHandler(async (req, res) => {
235
- const { targetName } = req.body;
236
- if (!targetName) {
237
- throw new ValidationError('targetName is required');
238
- }
239
- const container = getServiceContainer();
240
- const candidateService = container.get('candidateService');
241
-
242
- // 查两次:按 category 字段 + 全量扫描 metadata.targetName
243
- // (前端分组 key = metadata.targetName || category,两者可能不同)
244
- const byCategory = await candidateService.listCandidates({ category: targetName }, { page: 1, pageSize: 2000 });
245
- const byCategoryItems = byCategory.data || byCategory.items || [];
246
-
247
- // 全量扫描 metadata.targetName 匹配(避免 category 不一致导致漏删)
248
- const allList = await candidateService.listCandidates({}, { page: 1, pageSize: 5000 });
249
- const allItems = allList.data || allList.items || [];
250
- const byTargetName = allItems.filter(c => {
251
- const meta = c.metadata || {};
252
- return meta.targetName === targetName;
253
- });
254
-
255
- // 合并去重
256
- const seen = new Set();
257
- const merged = [];
258
- for (const item of [...byCategoryItems, ...byTargetName]) {
259
- if (!seen.has(item.id)) {
260
- seen.add(item.id);
261
- merged.push(item);
262
- }
263
- }
264
-
265
- let deleted = 0;
266
- for (const item of merged) {
267
- try { await candidateService.deleteCandidate(item.id, { userId: 'batch-delete' }); deleted++; } catch (err) {
268
- logger.debug('批量删除: 单条失败', { id: item.id, error: err.message });
269
- }
270
- }
271
- res.json({ success: true, data: { deleted } });
272
- }));
273
-
274
- /**
275
- * POST /api/v1/candidates/similarity
276
- * 查找与候选项相似的 Recipe
277
- */
278
- router.post('/similarity', asyncHandler(async (req, res) => {
279
- const { targetName, candidateId, candidate } = req.body;
280
- const container = getServiceContainer();
281
- const candidateService = container.get('candidateService');
282
- const recipeService = container.get('recipeService');
283
-
284
- let cand = candidate;
285
- if (!cand && candidateId) {
286
- const list = await candidateService.listCandidates({}, { page: 1, pageSize: 2000 });
287
- const items = list.data || list.items || [];
288
- const found = items.find(c => c.id === candidateId);
289
- if (found) {
290
- const meta = found.metadata || {};
291
- cand = {
292
- title: meta.title || '',
293
- summary: meta.summary_cn || meta.summary || '',
294
- code: found.code || '',
295
- usageGuide: meta.usageGuide_cn || meta.usageGuide || '',
296
- };
297
- }
298
- }
299
- if (!cand) {
300
- return res.json({ success: true, data: { similar: [] } });
301
- }
302
-
303
- // Try SimilarityService first
304
- try {
305
- const { default: SimilarityService } = await import('../../service/candidate/SimilarityService.js');
306
- const config = container.get('config') || {};
307
- const projectRoot = config.projectRoot || container.singletons?._projectRoot || process.cwd();
308
- const simService = new SimilarityService();
309
- const result = await simService.findSimilarRecipes(projectRoot, cand);
310
- if (result && result.length > 0) {
311
- return res.json({ success: true, data: { similar: result } });
312
- }
313
- } catch (err) {
314
- logger.debug('SimilarityService 不可用,降级到文本相似度', { error: err.message });
315
- }
316
-
317
- // Fallback: text-based similarity
318
- const allRecipes = await recipeService.listRecipes({}, { page: 1, pageSize: 500 });
319
- const recipeItems = allRecipes.data || allRecipes.items || [];
320
- const candText = [cand.title, cand.summary, cand.code, cand.usageGuide].filter(Boolean).join(' ').toLowerCase();
321
- const candWords = new Set(candText.split(/\s+/).filter(w => w.length > 2));
322
- const similar = [];
323
- for (const r of recipeItems) {
324
- const recipeText = [r.title, r.description, (r.content || {}).pattern].filter(Boolean).join(' ').toLowerCase();
325
- const recipeWords = new Set(recipeText.split(/\s+/).filter(w => w.length > 2));
326
- let matches = 0;
327
- for (const w of candWords) { if (recipeWords.has(w)) matches++; }
328
- const similarity = matches / Math.max(candWords.size, recipeWords.size, 1);
329
- if (similarity >= 0.15) {
330
- similar.push({ recipeName: (r.title || r.id) + '.md', similarity: Math.round(similarity * 100) / 100 });
331
- }
332
- }
333
- similar.sort((a, b) => b.similarity - a.similarity);
334
- res.json({ success: true, data: { similar: similar.slice(0, 5) } });
335
- }));
336
-
337
- /**
338
- * POST /api/v1/candidates/:id/apply-to-recipe
339
- * 将候选项应用到 Recipe (Gateway 管控)
340
- */
341
- router.post('/:id/apply-to-recipe', asyncHandler(async (req, res) => {
342
- const { id } = req.params;
343
- const { recipeId } = req.body;
344
-
345
- if (!recipeId) {
346
- throw new ValidationError('recipeId is required');
347
- }
348
-
349
- const result = await req.gw('candidate:apply_to_recipe', 'candidates', {
350
- candidateId: id,
351
- recipeId,
352
- });
353
-
354
- res.json({ success: true, data: result.data, requestId: result.requestId });
355
- }));
356
-
357
- /**
358
- * POST /api/v1/candidates/:id/promote
359
- * 将 APPROVED 候选项一键提升为 Recipe(自动创建 Recipe + 标记 APPLIED)
360
- * Body: { title?, category?, knowledgeType?, trigger?, tags? }
361
- */
362
- router.post('/:id/promote', asyncHandler(async (req, res) => {
363
- const { id } = req.params;
364
- const container = getServiceContainer();
365
- const candidateService = container.get('candidateService');
366
- const context = getContext(req);
367
-
368
- // overrides 允许用户在提升时微调字段
369
- const overrides = {};
370
- const ALLOWED = ['title', 'description', 'category', 'language', 'knowledgeType',
371
- 'complexity', 'scope', 'trigger', 'tags', 'rationale', 'kind', 'relations'];
372
- for (const key of ALLOWED) {
373
- if (req.body[key] !== undefined) overrides[key] = req.body[key];
374
- }
375
-
376
- const result = await candidateService.promoteCandidateToRecipe(id, overrides, context);
377
- res.status(201).json({ success: true, data: result });
378
- }));
379
-
380
- /**
381
- * POST /api/v1/candidates/batch-create
382
- * 批量创建候选项(从外部数据源批量导入)
383
- * Body: { items: Array<{ code, language, category, source?, reasoning?, metadata? }> }
384
- */
385
- router.post('/batch-create', asyncHandler(async (req, res) => {
386
- const { items } = req.body;
387
- if (!Array.isArray(items) || items.length === 0) {
388
- throw new ValidationError('items array is required');
389
- }
390
- if (items.length > MAX_BATCH_SIZE) {
391
- throw new ValidationError(`Max ${MAX_BATCH_SIZE} items per batch`);
392
- }
393
-
394
- const container = getServiceContainer();
395
- const candidateService = container.get('candidateService');
396
- const context = getContext(req);
397
-
398
- let created = 0;
399
- const errors = [];
400
- for (const item of items) {
401
- try {
402
- if (!item.code || !item.language || !item.category) {
403
- errors.push({ index: items.indexOf(item), error: 'code, language and category are required' });
404
- continue;
405
- }
406
- await candidateService.createCandidate({
407
- code: item.code,
408
- language: item.language,
409
- category: item.category,
410
- source: item.source || 'batch-import',
411
- reasoning: item.reasoning || undefined,
412
- metadata: item.metadata || undefined,
413
- }, context);
414
- created++;
415
- } catch (err) {
416
- errors.push({ index: items.indexOf(item), error: err.message });
417
- }
418
- }
419
- res.status(201).json({ success: true, data: { created, failed: errors.length, errors } });
420
- }));
421
-
422
- /**
423
- * POST /api/v1/candidates/enrich
424
- * AI 语义字段补全 — 对候选批量补充缺失的 rationale/knowledgeType/complexity/scope/steps/constraints
425
- * Body: { candidateIds: string[] }
426
- */
427
- router.post('/enrich', asyncHandler(async (req, res) => {
428
- const { candidateIds } = req.body;
429
-
430
- if (!Array.isArray(candidateIds) || candidateIds.length === 0) {
431
- throw new ValidationError('candidateIds array is required');
432
- }
433
- if (candidateIds.length > 20) {
434
- throw new ValidationError('Max 20 candidates per enrichment call');
435
- }
436
-
437
- const container = getServiceContainer();
438
- const chatAgent = container.get('chatAgent');
439
- const result = await chatAgent.executeTool('enrich_candidate', { candidateIds });
440
-
441
- if (result?.error) {
442
- throw new ValidationError(result.error);
443
- }
444
-
445
- res.json({ success: true, data: result });
446
- }));
447
-
448
- /**
449
- * POST /api/v1/candidates/bootstrap-refine
450
- * Phase 6 AI 润色 — 对 Bootstrap 候选批量改进描述、补充关系、调整评分
451
- * Body: { candidateIds?: string[], userPrompt?: string, dryRun?: boolean }
452
- */
453
- router.post('/bootstrap-refine', asyncHandler(async (req, res) => {
454
- const { candidateIds, userPrompt, dryRun } = req.body;
455
-
456
- const container = getServiceContainer();
457
- const chatAgent = container.get('chatAgent');
458
- const result = await chatAgent.executeTool('refine_bootstrap_candidates', { candidateIds, userPrompt, dryRun });
459
-
460
- if (result?.error) {
461
- throw new ValidationError(result.error);
462
- }
463
-
464
- res.json({ success: true, data: result });
465
- }));
466
-
467
- /**
468
- * POST /api/v1/candidates/refine-preview
469
- * 对话式润色预览 — 单条候选 dryRun,返回 before/after 对比
470
- * Body: { candidateId: string, userPrompt?: string }
471
- */
472
- router.post('/refine-preview', asyncHandler(async (req, res) => {
473
- const { candidateId, userPrompt } = req.body;
474
- if (!candidateId) throw new ValidationError('candidateId is required');
475
-
476
- const container = getServiceContainer();
477
- const candidateRepo = container.get('candidateRepository');
478
- const chatAgent = container.get('chatAgent');
479
-
480
- // 获取当前状态 (before)
481
- const candidate = await candidateRepo.findById(candidateId);
482
- if (!candidate) throw new NotFoundError('Candidate not found', 'candidate', candidateId);
483
-
484
- const meta = candidate.metadata || {};
485
- const before = {
486
- title: meta.title || '',
487
- summary: meta.summary || '',
488
- code: candidate.code || '',
489
- tags: meta.tags || [],
490
- confidence: meta.reasoning?.confidence ?? meta.refinedConfidence ?? 0.6,
491
- relations: meta.relations || [],
492
- insight: meta.aiInsight || null,
493
- agentNotes: meta.agentNotes || null,
494
- };
495
-
496
- // DryRun 润色取得 AI 预览
497
- const result = await chatAgent.executeTool('refine_bootstrap_candidates', {
498
- candidateIds: [candidateId],
499
- userPrompt,
500
- dryRun: true,
501
- });
502
-
503
- if (result?.error) throw new ValidationError(result.error);
504
-
505
- const preview = result.results?.[0]?.preview || {};
506
-
507
- // 构建 after 对象(增强 code 字段校验:防止 AI 返回截断/片段代码或类型变更)
508
- const origCode = before.code || '';
509
- const isOrigMarkdown = /^---\s*\n/.test(origCode) || /^#\s+/.test(origCode) || (origCode.match(/^#{1,3}\s+/gm) || []).length >= 2;
510
- const isPreviewMarkdown = preview.code && (/^---\s*\n/.test(preview.code) || /^#\s+/.test(preview.code) || (preview.code.match(/^#{1,3}\s+/gm) || []).length >= 2);
511
- // 防止源代码被转成 Markdown 文档
512
- const codeTypeChanged = !isOrigMarkdown && isPreviewMarkdown;
513
- const isCodeValid = preview.code
514
- && preview.code.length > 50
515
- && preview.code !== before.code
516
- && preview.code.length >= before.code.length * 0.4 // 不能太短(防止截断)
517
- && !codeTypeChanged; // 不允许类型变更
518
- const after = {
519
- title: preview.title || before.title,
520
- summary: preview.summary || before.summary,
521
- code: isCodeValid ? preview.code : before.code,
522
- tags: preview.tags ? [...new Set([...(before.tags || []), ...preview.tags])] : before.tags,
523
- confidence: (typeof preview.confidence === 'number' && preview.confidence !== 0.6) ? preview.confidence : before.confidence,
524
- relations: (preview.relations && Array.isArray(preview.relations) && preview.relations.length > 0) ? preview.relations : before.relations,
525
- insight: preview.insight || before.insight,
526
- agentNotes: (preview.agentNotes && Array.isArray(preview.agentNotes)) ? preview.agentNotes : before.agentNotes,
527
- };
528
-
529
- res.json({ success: true, data: { candidateId, before, after, preview } });
530
- }));
531
-
532
- /**
533
- * POST /api/v1/candidates/refine-apply
534
- * 对话式润色应用 — 确认后真正写入变更(非 dryRun)
535
- * Body: { candidateId: string, userPrompt?: string }
536
- */
537
- router.post('/refine-apply', asyncHandler(async (req, res) => {
538
- const { candidateId, userPrompt } = req.body;
539
- if (!candidateId) throw new ValidationError('candidateId is required');
540
-
541
- const container = getServiceContainer();
542
- const chatAgent = container.get('chatAgent');
543
-
544
- const result = await chatAgent.executeTool('refine_bootstrap_candidates', {
545
- candidateIds: [candidateId],
546
- userPrompt,
547
- dryRun: false,
548
- });
549
-
550
- if (result?.error) throw new ValidationError(result.error);
551
-
552
- // 返回更新后的候选数据,供前端直接替换
553
- const candidateRepo = container.get('candidateRepository');
554
- const updated = await candidateRepo.findById(candidateId);
555
-
556
- res.json({ success: true, data: { ...result, candidate: updated } });
557
- }));
558
-
559
- export default router;