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.
- package/README.md +5 -5
- package/bin/cli.js +5 -33
- package/config/constitution.yaml +9 -2
- package/dashboard/dist/assets/{icons-B_Xg4B-s.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 +9 -27
- package/lib/core/ast/ProjectGraph.js +160 -0
- 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/ai/providers/GoogleGeminiProvider.js +12 -3
- package/lib/external/mcp/McpServer.js +7 -5
- package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +3 -2
- package/lib/external/mcp/handlers/bootstrap.js +121 -12
- 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/skill.js +4 -2
- 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/middleware/requestLogger.js +3 -3
- package/lib/http/routes/ai.js +17 -1
- 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/http/routes/skills.js +44 -1
- package/lib/infrastructure/cache/GraphCache.js +143 -0
- package/lib/infrastructure/database/migrations/015_create_token_usage.js +27 -0
- package/lib/infrastructure/database/migrations/016_unified_knowledge_entries.js +395 -0
- package/lib/infrastructure/external/XcodeAutomation.js +187 -103
- package/lib/infrastructure/realtime/RealtimeService.js +14 -2
- package/lib/injection/ServiceContainer.js +164 -63
- package/lib/repository/knowledge/KnowledgeRepository.impl.js +373 -0
- package/lib/repository/token/TokenUsageStore.js +162 -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/AnalystAgent.js +25 -14
- package/lib/service/chat/CandidateGuardrail.js +1 -1
- package/lib/service/chat/ChatAgent.js +280 -48
- package/lib/service/chat/ContextWindow.js +92 -8
- package/lib/service/chat/HandoffProtocol.js +26 -1
- package/lib/service/chat/ProducerAgent.js +11 -9
- package/lib/service/chat/tools.js +298 -194
- 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 +15 -9
- package/lib/service/skills/SkillAdvisor.js +13 -11
- package/lib/service/snippet/SnippetFactory.js +5 -5
- package/lib/service/spm/SpmService.js +119 -18
- 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-CkIih2CC.css +0 -1
- package/dashboard/dist/assets/index-Duc8Qk-c.js +0 -197
- 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 -973
- 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,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;
|