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,397 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Recipe 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 logger = Logger.getInstance();
|
|
14
|
-
|
|
15
|
-
const router = express.Router();
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* GET /api/v1/recipes
|
|
19
|
-
* 获取 Recipe 列表(支持筛选和分页)
|
|
20
|
-
*/
|
|
21
|
-
router.get('/', asyncHandler(async (req, res) => {
|
|
22
|
-
const { status, category, language, knowledgeType, kind, keyword, tag } = req.query;
|
|
23
|
-
const page = safeInt(req.query.page, 1);
|
|
24
|
-
const pageSize = safeInt(req.query.limit, 20, 1, 100);
|
|
25
|
-
|
|
26
|
-
const container = getServiceContainer();
|
|
27
|
-
const recipeService = container.get('recipeService');
|
|
28
|
-
|
|
29
|
-
if (keyword) {
|
|
30
|
-
const result = await recipeService.searchRecipes(keyword, { page, pageSize });
|
|
31
|
-
return res.json({ success: true, data: result });
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const filters = {};
|
|
35
|
-
if (status) filters.status = status;
|
|
36
|
-
if (category) filters.category = category;
|
|
37
|
-
if (language) filters.language = language;
|
|
38
|
-
if (knowledgeType) filters.knowledgeType = knowledgeType;
|
|
39
|
-
if (kind) filters.kind = kind;
|
|
40
|
-
if (tag) filters.tag = tag;
|
|
41
|
-
|
|
42
|
-
const result = await recipeService.listRecipes(filters, { page, pageSize });
|
|
43
|
-
res.json({ success: true, data: result });
|
|
44
|
-
}));
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* GET /api/v1/recipes/stats
|
|
48
|
-
* 获取 Recipe 统计
|
|
49
|
-
*/
|
|
50
|
-
router.get('/stats', asyncHandler(async (req, res) => {
|
|
51
|
-
const container = getServiceContainer();
|
|
52
|
-
const recipeService = container.get('recipeService');
|
|
53
|
-
const stats = await recipeService.getRecipeStats();
|
|
54
|
-
res.json({ success: true, data: stats });
|
|
55
|
-
}));
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* GET /api/v1/recipes/recommendations
|
|
59
|
-
* 获取推荐 Recipe
|
|
60
|
-
*/
|
|
61
|
-
router.get('/recommendations', asyncHandler(async (req, res) => {
|
|
62
|
-
const limit = safeInt(req.query.limit, 10, 1, 50);
|
|
63
|
-
const container = getServiceContainer();
|
|
64
|
-
const recipeService = container.get('recipeService');
|
|
65
|
-
const recommendations = await recipeService.getRecommendations(limit);
|
|
66
|
-
res.json({ success: true, data: recommendations });
|
|
67
|
-
}));
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* GET /api/v1/recipes/:id
|
|
71
|
-
* 获取 Recipe 详情
|
|
72
|
-
*/
|
|
73
|
-
router.get('/:id', asyncHandler(async (req, res) => {
|
|
74
|
-
const { id } = req.params;
|
|
75
|
-
const container = getServiceContainer();
|
|
76
|
-
const recipeRepo = container.get('recipeRepository');
|
|
77
|
-
const recipe = await recipeRepo.findById(id);
|
|
78
|
-
|
|
79
|
-
if (!recipe) {
|
|
80
|
-
throw new NotFoundError('Recipe not found', 'recipe', id);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
res.json({ success: true, data: recipe });
|
|
84
|
-
}));
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* POST /api/v1/recipes
|
|
88
|
-
* 创建新 Recipe(草稿状态)(Gateway 管控: 权限 + 宪法 + 审计)
|
|
89
|
-
*/
|
|
90
|
-
router.post('/', asyncHandler(async (req, res) => {
|
|
91
|
-
const {
|
|
92
|
-
title, description, language, category, content,
|
|
93
|
-
relations, constraints, knowledgeType, complexity, scope,
|
|
94
|
-
sourceCandidate, tags, dimensions, trigger,
|
|
95
|
-
summaryCn, summaryEn, usageGuideCn, usageGuideEn,
|
|
96
|
-
} = req.body;
|
|
97
|
-
|
|
98
|
-
if (!title || !language || !category) {
|
|
99
|
-
throw new ValidationError('title, language and category are required');
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const result = await req.gw('recipe:create', 'recipes', {
|
|
103
|
-
title, description, language, category, content,
|
|
104
|
-
relations, constraints, knowledgeType, complexity, scope,
|
|
105
|
-
sourceCandidate, tags, dimensions, trigger,
|
|
106
|
-
summaryCn, summaryEn, usageGuideCn, usageGuideEn,
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
res.status(201).json({ success: true, data: result.data, requestId: result.requestId });
|
|
110
|
-
}));
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* PATCH /api/v1/recipes/:id
|
|
114
|
-
* 通用更新 Recipe(白名单字段)
|
|
115
|
-
*/
|
|
116
|
-
router.patch('/:id', asyncHandler(async (req, res) => {
|
|
117
|
-
const { id } = req.params;
|
|
118
|
-
const container = getServiceContainer();
|
|
119
|
-
const recipeService = container.get('recipeService');
|
|
120
|
-
const context = getContext(req);
|
|
121
|
-
|
|
122
|
-
const recipe = await recipeService.updateRecipe(id, req.body, context);
|
|
123
|
-
res.json({ success: true, data: recipe });
|
|
124
|
-
}));
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* DELETE /api/v1/recipes/:id
|
|
128
|
-
* 删除 Recipe (Gateway 管控: 权限 + 宪法 + 审计)
|
|
129
|
-
*/
|
|
130
|
-
router.delete('/:id', asyncHandler(async (req, res) => {
|
|
131
|
-
const { id } = req.params;
|
|
132
|
-
|
|
133
|
-
const result = await req.gw('recipe:delete', 'recipes', {
|
|
134
|
-
recipeId: id,
|
|
135
|
-
confirmed: true,
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
res.json({ success: true, data: result.data, requestId: result.requestId });
|
|
139
|
-
}));
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* PATCH /api/v1/recipes/:id/publish
|
|
143
|
-
* 发布 Recipe(DRAFT → ACTIVE)(Gateway 管控)
|
|
144
|
-
*/
|
|
145
|
-
router.patch('/:id/publish', asyncHandler(async (req, res) => {
|
|
146
|
-
const { id } = req.params;
|
|
147
|
-
|
|
148
|
-
const result = await req.gw('recipe:publish', 'recipes', {
|
|
149
|
-
recipeId: id,
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
// 发布后自动发现知识图谱关系(非阻塞)
|
|
153
|
-
try {
|
|
154
|
-
const container = getServiceContainer();
|
|
155
|
-
const chatAgent = container.get('chatAgent');
|
|
156
|
-
const recipeRepo = container.get('recipeRepository');
|
|
157
|
-
const published = await recipeRepo.findById(id);
|
|
158
|
-
if (published) {
|
|
159
|
-
// 获取同分类/同语言的其他 Recipe,与新发布的 Recipe 配对分析
|
|
160
|
-
const { items = [], data: listData = [] } = await container.get('recipeService').listRecipes(
|
|
161
|
-
{ language: published.language },
|
|
162
|
-
{ page: 1, pageSize: 20 },
|
|
163
|
-
);
|
|
164
|
-
const peers = (items.length > 0 ? items : listData).filter(r => r.id !== id).slice(0, 10);
|
|
165
|
-
if (peers.length > 0) {
|
|
166
|
-
const recipePairs = peers.map(peer => ({
|
|
167
|
-
a: { id: published.id, title: published.title, category: published.category, language: published.language, code: (published.content || published.code || '').substring(0, 500) },
|
|
168
|
-
b: { id: peer.id, title: peer.title, category: peer.category, language: peer.language, code: (peer.content || peer.code || '').substring(0, 500) },
|
|
169
|
-
}));
|
|
170
|
-
// 异步执行,不阻塞响应
|
|
171
|
-
chatAgent.executeTool('discover_relations', { recipePairs }).catch(err => {
|
|
172
|
-
logger.debug('Auto discover_relations skipped', { error: err.message });
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
} catch (err) {
|
|
177
|
-
logger.warn('Agent 不可用,跳过 discover_relations', { error: err.message });
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
res.json({ success: true, data: result.data, requestId: result.requestId });
|
|
181
|
-
}));
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* PATCH /api/v1/recipes/:id/deprecate
|
|
185
|
-
* 弃用 Recipe (Gateway 管控)
|
|
186
|
-
*/
|
|
187
|
-
router.patch('/:id/deprecate', asyncHandler(async (req, res) => {
|
|
188
|
-
const { id } = req.params;
|
|
189
|
-
const { reason } = req.body;
|
|
190
|
-
|
|
191
|
-
if (!reason) {
|
|
192
|
-
throw new ValidationError('reason is required for deprecation');
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const result = await req.gw('recipe:deprecate', 'recipes', {
|
|
196
|
-
recipeId: id,
|
|
197
|
-
reason,
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
res.json({ success: true, data: result.data, requestId: result.requestId });
|
|
201
|
-
}));
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* PATCH /api/v1/recipes/:id/quality
|
|
205
|
-
* 更新 Recipe 质量指标
|
|
206
|
-
*/
|
|
207
|
-
router.patch('/:id/quality', asyncHandler(async (req, res) => {
|
|
208
|
-
const { id } = req.params;
|
|
209
|
-
const { codeCompleteness, projectAdaptation, documentationClarity } = req.body;
|
|
210
|
-
|
|
211
|
-
const metrics = {};
|
|
212
|
-
if (codeCompleteness !== undefined) metrics.codeCompleteness = codeCompleteness;
|
|
213
|
-
if (projectAdaptation !== undefined) metrics.projectAdaptation = projectAdaptation;
|
|
214
|
-
if (documentationClarity !== undefined) metrics.documentationClarity = documentationClarity;
|
|
215
|
-
|
|
216
|
-
if (Object.keys(metrics).length === 0) {
|
|
217
|
-
throw new ValidationError('At least one quality metric is required');
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const container = getServiceContainer();
|
|
221
|
-
const recipeService = container.get('recipeService');
|
|
222
|
-
const context = getContext(req);
|
|
223
|
-
|
|
224
|
-
const recipe = await recipeService.updateQuality(id, metrics, context);
|
|
225
|
-
res.json({ success: true, data: recipe });
|
|
226
|
-
}));
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* POST /api/v1/recipes/:id/adopt
|
|
230
|
-
* 记录 Recipe 被采纳
|
|
231
|
-
*/
|
|
232
|
-
router.post('/:id/adopt', asyncHandler(async (req, res) => {
|
|
233
|
-
const { id } = req.params;
|
|
234
|
-
const container = getServiceContainer();
|
|
235
|
-
const recipeService = container.get('recipeService');
|
|
236
|
-
await recipeService.incrementAdoption(id);
|
|
237
|
-
res.json({ success: true, message: 'Adoption recorded' });
|
|
238
|
-
}));
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* POST /api/v1/recipes/:id/apply
|
|
242
|
-
* 记录 Recipe 被应用
|
|
243
|
-
*/
|
|
244
|
-
router.post('/:id/apply', asyncHandler(async (req, res) => {
|
|
245
|
-
const { id } = req.params;
|
|
246
|
-
const container = getServiceContainer();
|
|
247
|
-
const recipeService = container.get('recipeService');
|
|
248
|
-
await recipeService.incrementApplication(id);
|
|
249
|
-
res.json({ success: true, message: 'Application recorded' });
|
|
250
|
-
}));
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* POST /api/v1/recipes/batch-record-usage
|
|
254
|
-
* 批量记录 Recipe 使用(按关键词搜索匹配)
|
|
255
|
-
* Body: { items: Array<{ keyword: string, usageType?: 'adoption'|'application' }> }
|
|
256
|
-
*/
|
|
257
|
-
router.post('/batch-record-usage', asyncHandler(async (req, res) => {
|
|
258
|
-
const { items } = req.body;
|
|
259
|
-
if (!Array.isArray(items) || items.length === 0) {
|
|
260
|
-
throw new ValidationError('items array is required');
|
|
261
|
-
}
|
|
262
|
-
if (items.length > 100) {
|
|
263
|
-
throw new ValidationError('Max 100 items per batch');
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const container = getServiceContainer();
|
|
267
|
-
const recipeService = container.get('recipeService');
|
|
268
|
-
let recorded = 0;
|
|
269
|
-
|
|
270
|
-
for (const entry of items) {
|
|
271
|
-
const keyword = entry.keyword || entry.recipeFilePath || '';
|
|
272
|
-
if (!keyword) continue;
|
|
273
|
-
try {
|
|
274
|
-
const result = await recipeService.searchRecipes(
|
|
275
|
-
keyword.replace(/\.md$/, ''),
|
|
276
|
-
{ page: 1, pageSize: 1 },
|
|
277
|
-
);
|
|
278
|
-
const found = result?.items?.[0];
|
|
279
|
-
if (found) {
|
|
280
|
-
const type = entry.usageType || 'adoption';
|
|
281
|
-
await recipeService.incrementUsage(found.id, type);
|
|
282
|
-
recorded++;
|
|
283
|
-
}
|
|
284
|
-
} catch { /* skip individual failures */ }
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
res.json({ success: true, data: { recorded, total: items.length } });
|
|
288
|
-
}));
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* POST /api/v1/recipes/discover-relations
|
|
292
|
-
* AI 批量发现知识图谱关系(异步后台任务)
|
|
293
|
-
* Body: { batchSize?: number }
|
|
294
|
-
*/
|
|
295
|
-
|
|
296
|
-
// 内存中的任务状态(单进程足够)
|
|
297
|
-
const _discoverTask = { running: false, result: null, error: null, startedAt: null, finishedAt: null };
|
|
298
|
-
|
|
299
|
-
// 10 分钟超时保护
|
|
300
|
-
const DISCOVER_TIMEOUT_MS = 10 * 60 * 1000;
|
|
301
|
-
|
|
302
|
-
router.post('/discover-relations', asyncHandler(async (req, res) => {
|
|
303
|
-
if (_discoverTask.running) {
|
|
304
|
-
// 检查是否已超时
|
|
305
|
-
const elapsed = Date.now() - new Date(_discoverTask.startedAt).getTime();
|
|
306
|
-
if (elapsed > DISCOVER_TIMEOUT_MS) {
|
|
307
|
-
_discoverTask.running = false;
|
|
308
|
-
_discoverTask.error = `任务超时(运行 ${Math.round(elapsed / 1000)}s 后强制结束)`;
|
|
309
|
-
_discoverTask.finishedAt = new Date().toISOString();
|
|
310
|
-
return res.json({ success: true, data: { status: 'timeout', error: _discoverTask.error, startedAt: _discoverTask.startedAt } });
|
|
311
|
-
}
|
|
312
|
-
return res.json({ success: true, data: { status: 'running', startedAt: _discoverTask.startedAt, elapsed: Math.round(elapsed / 1000) } });
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
const { batchSize = 20 } = req.body;
|
|
316
|
-
const container = getServiceContainer();
|
|
317
|
-
|
|
318
|
-
// 前置检查:ChatAgent 和 AI Provider 是否可用
|
|
319
|
-
const chatAgent = container.get('chatAgent');
|
|
320
|
-
if (!chatAgent) {
|
|
321
|
-
return res.status(503).json({ success: false, error: { message: 'ChatAgent 服务不可用,请检查服务配置' } });
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
const recipeService = container.get('recipeService');
|
|
325
|
-
if (!recipeService) {
|
|
326
|
-
return res.status(503).json({ success: false, error: { message: 'RecipeService 不可用' } });
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// 检查是否有足够 Recipe
|
|
330
|
-
try {
|
|
331
|
-
const { items = [], data = [] } = await recipeService.listRecipes({}, { page: 1, pageSize: 5 });
|
|
332
|
-
const count = (items.length > 0 ? items : data).length;
|
|
333
|
-
if (count < 2) {
|
|
334
|
-
return res.json({ success: true, data: { status: 'empty', message: `当前只有 ${count} 条 Recipe,至少需要 2 条才能分析关系` } });
|
|
335
|
-
}
|
|
336
|
-
} catch (err) {
|
|
337
|
-
return res.status(500).json({ success: false, error: { message: `检查 Recipe 失败: ${err.message}` } });
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
_discoverTask.running = true;
|
|
341
|
-
_discoverTask.result = null;
|
|
342
|
-
_discoverTask.error = null;
|
|
343
|
-
_discoverTask.finishedAt = null;
|
|
344
|
-
_discoverTask.startedAt = new Date().toISOString();
|
|
345
|
-
|
|
346
|
-
// 后台执行,不阻塞请求
|
|
347
|
-
const timeoutHandle = setTimeout(() => {
|
|
348
|
-
if (_discoverTask.running) {
|
|
349
|
-
_discoverTask.running = false;
|
|
350
|
-
_discoverTask.error = '任务超时(超过 10 分钟)';
|
|
351
|
-
_discoverTask.finishedAt = new Date().toISOString();
|
|
352
|
-
logger.warn('discover-relations timed out');
|
|
353
|
-
}
|
|
354
|
-
}, DISCOVER_TIMEOUT_MS);
|
|
355
|
-
|
|
356
|
-
chatAgent.runTask('discover_all_relations', { batchSize })
|
|
357
|
-
.then(result => { _discoverTask.result = result; })
|
|
358
|
-
.catch(err => {
|
|
359
|
-
_discoverTask.error = err.message || String(err);
|
|
360
|
-
logger.error('discover-relations failed', { error: err.message });
|
|
361
|
-
})
|
|
362
|
-
.finally(() => {
|
|
363
|
-
clearTimeout(timeoutHandle);
|
|
364
|
-
_discoverTask.running = false;
|
|
365
|
-
_discoverTask.finishedAt = new Date().toISOString();
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
res.json({ success: true, data: { status: 'started', startedAt: _discoverTask.startedAt } });
|
|
369
|
-
}));
|
|
370
|
-
|
|
371
|
-
/**
|
|
372
|
-
* GET /api/v1/recipes/discover-relations/status
|
|
373
|
-
* 查询关系发现任务进度
|
|
374
|
-
*/
|
|
375
|
-
router.get('/discover-relations/status', asyncHandler(async (req, res) => {
|
|
376
|
-
const elapsed = _discoverTask.startedAt
|
|
377
|
-
? Math.round((Date.now() - new Date(_discoverTask.startedAt).getTime()) / 1000)
|
|
378
|
-
: 0;
|
|
379
|
-
|
|
380
|
-
if (_discoverTask.running) {
|
|
381
|
-
return res.json({ success: true, data: { status: 'running', startedAt: _discoverTask.startedAt, elapsed } });
|
|
382
|
-
}
|
|
383
|
-
if (_discoverTask.error) {
|
|
384
|
-
return res.json({ success: true, data: { status: 'error', error: _discoverTask.error, startedAt: _discoverTask.startedAt, finishedAt: _discoverTask.finishedAt } });
|
|
385
|
-
}
|
|
386
|
-
if (_discoverTask.result) {
|
|
387
|
-
return res.json({ success: true, data: {
|
|
388
|
-
status: 'done',
|
|
389
|
-
...(_discoverTask.result),
|
|
390
|
-
startedAt: _discoverTask.startedAt,
|
|
391
|
-
finishedAt: _discoverTask.finishedAt,
|
|
392
|
-
} });
|
|
393
|
-
}
|
|
394
|
-
res.json({ success: true, data: { status: 'idle' } });
|
|
395
|
-
}));
|
|
396
|
-
|
|
397
|
-
export default router;
|
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
import { BaseRepository } from '../base/BaseRepository.js';
|
|
2
|
-
import { Candidate, Reasoning, CandidateStatus } from '../../domain/index.js';
|
|
3
|
-
import Logger from '../../infrastructure/logging/Logger.js';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* CandidateRepository 实现
|
|
7
|
-
* 面向 SQLite 数据库的 Candidate 实体持久化
|
|
8
|
-
*/
|
|
9
|
-
export class CandidateRepositoryImpl extends BaseRepository {
|
|
10
|
-
constructor(database) {
|
|
11
|
-
super(database, 'candidates');
|
|
12
|
-
this.logger = Logger.getInstance();
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* 创建 Candidate
|
|
17
|
-
*/
|
|
18
|
-
async create(candidate) {
|
|
19
|
-
if (!candidate || !candidate.isValid()) {
|
|
20
|
-
throw new Error('Invalid candidate entity');
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
try {
|
|
24
|
-
const row = this._mapEntityToRow(candidate);
|
|
25
|
-
const keys = Object.keys(row);
|
|
26
|
-
const placeholders = keys.map(() => '?').join(', ');
|
|
27
|
-
const query = `
|
|
28
|
-
INSERT INTO candidates (${keys.join(', ')})
|
|
29
|
-
VALUES (${placeholders})
|
|
30
|
-
`;
|
|
31
|
-
|
|
32
|
-
const stmt = this.db.prepare(query);
|
|
33
|
-
stmt.run(...Object.values(row));
|
|
34
|
-
|
|
35
|
-
return this.findById(candidate.id);
|
|
36
|
-
} catch (error) {
|
|
37
|
-
this.logger.error('Error creating candidate', {
|
|
38
|
-
candidateId: candidate.id,
|
|
39
|
-
error: error.message,
|
|
40
|
-
});
|
|
41
|
-
throw error;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* 根据状态查询
|
|
47
|
-
*/
|
|
48
|
-
async findByStatus(status, { page = 1, pageSize = 20 } = {}) {
|
|
49
|
-
if (!Object.values(CandidateStatus).includes(status)) {
|
|
50
|
-
throw new Error(`Invalid status: ${status}`);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
try {
|
|
54
|
-
return this.findWithPagination({ status }, { page, pageSize });
|
|
55
|
-
} catch (error) {
|
|
56
|
-
this.logger.error('Error finding by status', {
|
|
57
|
-
status,
|
|
58
|
-
error: error.message,
|
|
59
|
-
});
|
|
60
|
-
throw error;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* 根据编程语言查询
|
|
66
|
-
*/
|
|
67
|
-
async findByLanguage(language, { page = 1, pageSize = 20 } = {}) {
|
|
68
|
-
try {
|
|
69
|
-
return this.findWithPagination({ language }, { page, pageSize });
|
|
70
|
-
} catch (error) {
|
|
71
|
-
this.logger.error('Error finding by language', {
|
|
72
|
-
language,
|
|
73
|
-
error: error.message,
|
|
74
|
-
});
|
|
75
|
-
throw error;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* 根据创建者查询
|
|
81
|
-
*/
|
|
82
|
-
async findByCreatedBy(createdBy, { page = 1, pageSize = 20 } = {}) {
|
|
83
|
-
try {
|
|
84
|
-
return this.findWithPagination({ created_by: createdBy }, { page, pageSize });
|
|
85
|
-
} catch (error) {
|
|
86
|
-
this.logger.error('Error finding by creator', {
|
|
87
|
-
createdBy,
|
|
88
|
-
error: error.message,
|
|
89
|
-
});
|
|
90
|
-
throw error;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* 搜索候选项(按代码或类别)
|
|
96
|
-
*/
|
|
97
|
-
async search(keyword, { page = 1, pageSize = 20 } = {}) {
|
|
98
|
-
try {
|
|
99
|
-
const offset = (page - 1) * pageSize;
|
|
100
|
-
const escaped = keyword.replace(/[%_]/g, '\\$&');
|
|
101
|
-
const like = `%${escaped}%`;
|
|
102
|
-
|
|
103
|
-
// 搜索 code / category / metadata_json(title, description, summary)
|
|
104
|
-
const whereClause = `
|
|
105
|
-
WHERE code LIKE @kw ESCAPE '\\'
|
|
106
|
-
OR category LIKE @kw ESCAPE '\\'
|
|
107
|
-
OR metadata_json LIKE @kw ESCAPE '\\'
|
|
108
|
-
`;
|
|
109
|
-
|
|
110
|
-
// 获取总数
|
|
111
|
-
const countStmt = this.db.prepare(`
|
|
112
|
-
SELECT COUNT(*) as count FROM candidates ${whereClause}
|
|
113
|
-
`);
|
|
114
|
-
const countResult = countStmt.get({ kw: like });
|
|
115
|
-
const total = countResult.count;
|
|
116
|
-
|
|
117
|
-
// 获取分页数据
|
|
118
|
-
const stmt = this.db.prepare(`
|
|
119
|
-
SELECT * FROM candidates ${whereClause}
|
|
120
|
-
ORDER BY created_at DESC
|
|
121
|
-
LIMIT @limit OFFSET @offset
|
|
122
|
-
`);
|
|
123
|
-
const data = stmt.all({ kw: like, limit: pageSize, offset });
|
|
124
|
-
|
|
125
|
-
return {
|
|
126
|
-
data: data.map((row) => this._mapRowToEntity(row)),
|
|
127
|
-
pagination: {
|
|
128
|
-
page,
|
|
129
|
-
pageSize,
|
|
130
|
-
total,
|
|
131
|
-
pages: Math.ceil(total / pageSize),
|
|
132
|
-
},
|
|
133
|
-
};
|
|
134
|
-
} catch (error) {
|
|
135
|
-
this.logger.error('Error searching candidates', {
|
|
136
|
-
keyword,
|
|
137
|
-
error: error.message,
|
|
138
|
-
});
|
|
139
|
-
throw error;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* 获取统计信息
|
|
145
|
-
*/
|
|
146
|
-
async getStats() {
|
|
147
|
-
try {
|
|
148
|
-
const stmt = this.db.prepare(`
|
|
149
|
-
SELECT
|
|
150
|
-
COUNT(*) as total,
|
|
151
|
-
SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending,
|
|
152
|
-
SUM(CASE WHEN status = 'approved' THEN 1 ELSE 0 END) as approved,
|
|
153
|
-
SUM(CASE WHEN status = 'rejected' THEN 1 ELSE 0 END) as rejected,
|
|
154
|
-
SUM(CASE WHEN status = 'applied' THEN 1 ELSE 0 END) as applied
|
|
155
|
-
FROM candidates
|
|
156
|
-
`);
|
|
157
|
-
return stmt.get();
|
|
158
|
-
} catch (error) {
|
|
159
|
-
this.logger.error('Error getting candidate stats', {
|
|
160
|
-
error: error.message,
|
|
161
|
-
});
|
|
162
|
-
throw error;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* 映射 SQLite 行到 Candidate 实体
|
|
168
|
-
*/
|
|
169
|
-
_mapRowToEntity(row) {
|
|
170
|
-
if (!row) return null;
|
|
171
|
-
|
|
172
|
-
const reasoning = row.reasoning_json
|
|
173
|
-
? Reasoning.fromJSON(this._safeJsonParse(row.reasoning_json, {}))
|
|
174
|
-
: null;
|
|
175
|
-
|
|
176
|
-
return new Candidate({
|
|
177
|
-
id: row.id,
|
|
178
|
-
code: row.code,
|
|
179
|
-
language: row.language,
|
|
180
|
-
category: row.category,
|
|
181
|
-
source: row.source,
|
|
182
|
-
reasoning,
|
|
183
|
-
status: row.status,
|
|
184
|
-
statusHistory: this._safeJsonParse(row.status_history_json, []),
|
|
185
|
-
createdBy: row.created_by,
|
|
186
|
-
createdAt: row.created_at,
|
|
187
|
-
updatedAt: row.updated_at,
|
|
188
|
-
approvedBy: row.approved_by,
|
|
189
|
-
approvedAt: row.approved_at,
|
|
190
|
-
rejectionReason: row.rejection_reason,
|
|
191
|
-
rejectedBy: row.rejected_by,
|
|
192
|
-
appliedRecipeId: row.applied_recipe_id,
|
|
193
|
-
metadata: this._safeJsonParse(row.metadata_json, {}),
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
_safeJsonParse(str, fallback) {
|
|
198
|
-
if (!str) return fallback;
|
|
199
|
-
try { return JSON.parse(str); } catch { return fallback; }
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* 映射 Candidate 实体到 SQLite 行
|
|
204
|
-
*/
|
|
205
|
-
_mapEntityToRow(entity) {
|
|
206
|
-
const now = Math.floor(Date.now() / 1000);
|
|
207
|
-
|
|
208
|
-
return {
|
|
209
|
-
id: entity.id,
|
|
210
|
-
code: entity.code,
|
|
211
|
-
language: entity.language,
|
|
212
|
-
category: entity.category,
|
|
213
|
-
source: entity.source,
|
|
214
|
-
reasoning_json: entity.reasoning ? JSON.stringify(entity.reasoning.toJSON()) : null,
|
|
215
|
-
status: entity.status,
|
|
216
|
-
status_history_json: JSON.stringify(entity.statusHistory),
|
|
217
|
-
created_by: entity.createdBy,
|
|
218
|
-
created_at: entity.createdAt || now,
|
|
219
|
-
updated_at: entity.updatedAt || now,
|
|
220
|
-
approved_by: entity.approvedBy,
|
|
221
|
-
approved_at: entity.approvedAt,
|
|
222
|
-
rejection_reason: entity.rejectionReason,
|
|
223
|
-
rejected_by: entity.rejectedBy,
|
|
224
|
-
applied_recipe_id: entity.appliedRecipeId,
|
|
225
|
-
metadata_json: JSON.stringify(entity.metadata || {}),
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
export default CandidateRepositoryImpl;
|