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,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;