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,786 +0,0 @@
1
- import { Recipe, RecipeStatus, KnowledgeType, Complexity, Kind, inferKind } from '../../domain/index.js';
2
- import Logger from '../../infrastructure/logging/Logger.js';
3
- import { ValidationError, ConflictError, NotFoundError } from '../../shared/errors/index.js';
4
- import { v4 as uuidv4 } from 'uuid';
5
-
6
- /**
7
- * RecipeService
8
- * 管理代码模式和最佳实践的发布与生命周期
9
- * 包括创建、发布、质量管理和推荐等业务逻辑
10
- *
11
- * V2 唯一数据源策略:
12
- * DB 写入成功后自动落盘到 AutoSnippet/recipes/{category}/ 目录
13
- * .md 文件 = Source of Truth,DB = 索引缓存
14
- */
15
- export class RecipeService {
16
- /**
17
- * @param {object} recipeRepository
18
- * @param {object} auditLogger
19
- * @param {object} gateway
20
- * @param {object} knowledgeGraphService
21
- * @param {object} [options]
22
- * @param {import('./RecipeFileWriter.js').RecipeFileWriter} [options.fileWriter]
23
- */
24
- constructor(recipeRepository, auditLogger, gateway, knowledgeGraphService, options = {}) {
25
- this.recipeRepository = recipeRepository;
26
- this.auditLogger = auditLogger;
27
- this.gateway = gateway;
28
- this._knowledgeGraphService = knowledgeGraphService || null;
29
- this._fileWriter = options.fileWriter || null;
30
- this._skillHooks = options.skillHooks || null;
31
- this.logger = Logger.getInstance();
32
- }
33
-
34
- /**
35
- * 创建新 Recipe(草稿状态)
36
- */
37
- async createRecipe(data, context) {
38
- try {
39
- this._validateCreateInput(data);
40
-
41
- const recipe = new Recipe({
42
- id: uuidv4(),
43
- title: data.title,
44
- description: data.description || '',
45
- language: data.language,
46
- category: data.category,
47
- summaryCn: data.summaryCn || '',
48
- summaryEn: data.summaryEn || '',
49
- usageGuideCn: data.usageGuideCn || '',
50
- usageGuideEn: data.usageGuideEn || '',
51
- knowledgeType: data.knowledgeType || KnowledgeType.CODE_PATTERN,
52
- kind: data.kind || inferKind(data.knowledgeType || KnowledgeType.CODE_PATTERN),
53
- complexity: data.complexity || Complexity.INTERMEDIATE,
54
- scope: data.scope || null,
55
- content: data.content || {
56
- pattern: data.pattern || '',
57
- rationale: data.rationale || '',
58
- steps: data.steps || [],
59
- codeChanges: data.codeChanges || [],
60
- verification: data.verification || null,
61
- markdown: data.markdown || '',
62
- },
63
- relations: data.relations || {},
64
- constraints: data.constraints || {},
65
- dimensions: data.dimensions || {},
66
- trigger: data.trigger || '',
67
- tags: data.tags || [],
68
- quality: {
69
- codeCompleteness: 0,
70
- projectAdaptation: 0,
71
- documentationClarity: 0,
72
- overall: 0,
73
- },
74
- statistics: {
75
- adoptionCount: 0,
76
- applicationCount: 0,
77
- guardHitCount: 0,
78
- viewCount: 0,
79
- successCount: 0,
80
- feedbackScore: 0,
81
- },
82
- status: RecipeStatus.DRAFT,
83
- createdBy: context.userId,
84
- sourceCandidate: data.sourceCandidate || null,
85
- });
86
-
87
- if (!recipe.isValid()) {
88
- throw new ValidationError('Invalid recipe data');
89
- }
90
-
91
- const created = await this.recipeRepository.create(recipe);
92
-
93
- // 同步 relations → knowledge_edges
94
- this._syncRelationsToGraph(created.id, created.relations);
95
-
96
- // V2: 落盘到 AutoSnippet/recipes/{category}/ (.md = Source of Truth)
97
- this._persistToFile(created);
98
-
99
- await this.auditLogger.log({
100
- action: 'create_recipe',
101
- resourceType: 'recipe',
102
- resourceId: created.id,
103
- actor: context.userId,
104
- details: `Created recipe: ${created.title}`,
105
- timestamp: Math.floor(Date.now() / 1000),
106
- });
107
-
108
- this.logger.info('Recipe created', {
109
- recipeId: created.id,
110
- createdBy: context.userId,
111
- title: created.title,
112
- });
113
-
114
- // ── SkillHooks: onRecipeCreated (fire-and-forget) ──
115
- if (this._skillHooks) {
116
- this._skillHooks.run('onRecipeCreated', created, {
117
- userId: context.userId,
118
- }).catch(err => this.logger.warn('SkillHook onRecipeCreated error', { error: err.message }));
119
- }
120
-
121
- return created;
122
- } catch (error) {
123
- this.logger.error('Error creating recipe', {
124
- error: error.message,
125
- data,
126
- });
127
- throw error;
128
- }
129
- }
130
-
131
- /**
132
- * 发布 Recipe(从 DRAFT 变为 ACTIVE)
133
- */
134
- async publishRecipe(recipeId, context) {
135
- try {
136
- const recipe = await this.recipeRepository.findById(recipeId);
137
-
138
- if (!recipe) {
139
- throw new NotFoundError('Recipe not found', 'recipe', recipeId);
140
- }
141
-
142
- if (recipe.status !== RecipeStatus.DRAFT) {
143
- throw new ConflictError(
144
- `Cannot publish recipe in ${recipe.status} status`,
145
- `Must be in DRAFT status to publish`
146
- );
147
- }
148
-
149
- const publishResult = recipe.publish(context.userId);
150
- if (!publishResult.success) {
151
- throw new ValidationError(publishResult.error || 'Cannot publish recipe');
152
- }
153
-
154
- const updated = await this.recipeRepository.update(recipeId, {
155
- status: recipe.status,
156
- published_by: recipe.publishedBy,
157
- published_at: recipe.publishedAt,
158
- });
159
-
160
- // V2: 发布后落盘/更新 .md 文件(status → active)
161
- this._persistToFile(updated);
162
-
163
- await this.auditLogger.log({
164
- action: 'publish_recipe',
165
- resourceType: 'recipe',
166
- resourceId: recipeId,
167
- actor: context.userId,
168
- details: `Published recipe: ${recipe.title}`,
169
- timestamp: Math.floor(Date.now() / 1000),
170
- });
171
-
172
- this.logger.info('Recipe published', {
173
- recipeId,
174
- publishedBy: context.userId,
175
- });
176
-
177
- return updated;
178
- } catch (error) {
179
- this.logger.error('Error publishing recipe', {
180
- recipeId,
181
- error: error.message,
182
- });
183
- throw error;
184
- }
185
- }
186
-
187
- /**
188
- * 废弃 Recipe(从 ACTIVE 变为 DEPRECATED)
189
- */
190
- async deprecateRecipe(recipeId, reason, context) {
191
- try {
192
- const recipe = await this.recipeRepository.findById(recipeId);
193
-
194
- if (!recipe) {
195
- throw new NotFoundError('Recipe not found', 'recipe', recipeId);
196
- }
197
-
198
- if (recipe.status !== RecipeStatus.ACTIVE) {
199
- throw new ConflictError(
200
- `Cannot deprecate recipe in ${recipe.status} status`,
201
- `Must be in ACTIVE status to deprecate`
202
- );
203
- }
204
-
205
- if (!reason || reason.trim().length === 0) {
206
- throw new ValidationError('Deprecation reason is required');
207
- }
208
-
209
- recipe.deprecate(reason);
210
-
211
- const updated = await this.recipeRepository.update(recipeId, {
212
- status: recipe.status,
213
- deprecation_reason: recipe.deprecation?.reason,
214
- deprecated_at: recipe.deprecation?.deprecatedAt,
215
- });
216
-
217
- // V2: 废弃时更新 .md 文件的 status(不删除,保留 Git 历史)
218
- this._persistToFile(updated);
219
-
220
- await this.auditLogger.log({
221
- action: 'deprecate_recipe',
222
- resourceType: 'recipe',
223
- resourceId: recipeId,
224
- actor: context.userId,
225
- details: `Deprecated recipe: ${reason}`,
226
- timestamp: Math.floor(Date.now() / 1000),
227
- });
228
-
229
- this.logger.info('Recipe deprecated', {
230
- recipeId,
231
- deprecatedBy: context.userId,
232
- reason,
233
- });
234
-
235
- return updated;
236
- } catch (error) {
237
- this.logger.error('Error deprecating recipe', {
238
- recipeId,
239
- error: error.message,
240
- });
241
- throw error;
242
- }
243
- }
244
-
245
- /**
246
- * 更新质量指标
247
- */
248
- async updateQuality(recipeId, metrics, context) {
249
- try {
250
- const recipe = await this.recipeRepository.findById(recipeId);
251
-
252
- if (!recipe) {
253
- throw new NotFoundError('Recipe not found', 'recipe', recipeId);
254
- }
255
-
256
- // 验证指标
257
- if (
258
- typeof metrics.codeCompleteness === 'number' &&
259
- (metrics.codeCompleteness < 0 || metrics.codeCompleteness > 1)
260
- ) {
261
- throw new ValidationError('Code completeness must be between 0 and 1');
262
- }
263
-
264
- if (
265
- typeof metrics.projectAdaptation === 'number' &&
266
- (metrics.projectAdaptation < 0 || metrics.projectAdaptation > 1)
267
- ) {
268
- throw new ValidationError('Project adaptation must be between 0 and 1');
269
- }
270
-
271
- if (
272
- typeof metrics.documentationClarity === 'number' &&
273
- (metrics.documentationClarity < 0 || metrics.documentationClarity > 1)
274
- ) {
275
- throw new ValidationError('Documentation clarity must be between 0 and 1');
276
- }
277
-
278
- recipe.updateQuality(metrics);
279
-
280
- const updated = await this.recipeRepository.update(recipeId, {
281
- quality_code_completeness: recipe.quality.codeCompleteness,
282
- quality_project_adaptation: recipe.quality.projectAdaptation,
283
- quality_documentation_clarity: recipe.quality.documentationClarity,
284
- quality_overall: recipe.quality.overall,
285
- });
286
-
287
- await this.auditLogger.log({
288
- action: 'update_recipe_quality',
289
- resourceType: 'recipe',
290
- resourceId: recipeId,
291
- actor: context.userId,
292
- details: `Updated quality scores: overall=${recipe.quality.overall}`,
293
- timestamp: Math.floor(Date.now() / 1000),
294
- });
295
-
296
- this.logger.info('Recipe quality updated', {
297
- recipeId,
298
- qualityOverall: recipe.quality.overall,
299
- });
300
-
301
- return updated;
302
- } catch (error) {
303
- this.logger.error('Error updating recipe quality', {
304
- recipeId,
305
- error: error.message,
306
- });
307
- throw error;
308
- }
309
- }
310
-
311
- /**
312
- * 通用使用计数递增(含审计日志)
313
- * @param {string} recipeId
314
- * @param {'adoption'|'application'} type
315
- * @param {{ feedback?: string, actor?: string }} options
316
- */
317
- async incrementUsage(recipeId, type = 'adoption', options = {}) {
318
- try {
319
- const recipe = await this.recipeRepository.findById(recipeId);
320
-
321
- if (!recipe) {
322
- throw new NotFoundError('Recipe not found', 'recipe', recipeId);
323
- }
324
-
325
- recipe.incrementUsage(type);
326
-
327
- const column = type === 'application' ? 'application_count' : 'adoption_count';
328
- const count = type === 'application'
329
- ? recipe.statistics.applicationCount
330
- : recipe.statistics.adoptionCount;
331
-
332
- const updated = await this.recipeRepository.update(recipeId, {
333
- [column]: count,
334
- });
335
-
336
- // 审计日志
337
- await this.auditLogger.log({
338
- action: type === 'application' ? 'apply_recipe' : 'adopt_recipe',
339
- resourceType: 'recipe',
340
- resourceId: recipeId,
341
- actor: options.actor || 'user',
342
- details: `Recipe ${type}: ${recipe.title}${options.feedback ? ` | feedback: ${options.feedback}` : ''}`,
343
- timestamp: Math.floor(Date.now() / 1000),
344
- });
345
-
346
- this.logger.debug(`Recipe ${type} incremented`, {
347
- recipeId,
348
- [column]: count,
349
- });
350
-
351
- return updated;
352
- } catch (error) {
353
- this.logger.error(`Error incrementing recipe ${type}`, {
354
- recipeId,
355
- error: error.message,
356
- });
357
- throw error;
358
- }
359
- }
360
-
361
- /**
362
- * 记录采用(向后兼容)
363
- */
364
- async incrementAdoption(recipeId) {
365
- return this.incrementUsage(recipeId, 'adoption');
366
- }
367
-
368
- /**
369
- * 记录应用(向后兼容)
370
- */
371
- async incrementApplication(recipeId) {
372
- return this.incrementUsage(recipeId, 'application');
373
- }
374
-
375
- /**
376
- * 通用更新 Recipe(仅允许更新白名单字段)
377
- */
378
- async updateRecipe(recipeId, data, context) {
379
- try {
380
- const recipe = await this.recipeRepository.findById(recipeId);
381
-
382
- if (!recipe) {
383
- throw new NotFoundError('Recipe not found', 'recipe', recipeId);
384
- }
385
-
386
- // 白名单字段
387
- const UPDATABLE = [
388
- 'title', 'description', 'language', 'category', 'trigger',
389
- 'summaryCn', 'summaryEn', 'usageGuideCn', 'usageGuideEn',
390
- 'knowledgeType', 'complexity', 'scope',
391
- 'content', 'relations', 'constraints',
392
- 'dimensions', 'tags',
393
- ];
394
-
395
- // 构建 DB 列更新映射
396
- const dbUpdates = {};
397
-
398
- for (const key of UPDATABLE) {
399
- if (data[key] === undefined) continue;
400
-
401
- switch (key) {
402
- case 'title':
403
- case 'description':
404
- case 'language':
405
- case 'category':
406
- case 'trigger':
407
- case 'scope':
408
- dbUpdates[key] = data[key];
409
- // 同步实体属性以便 kind 推导
410
- recipe[key] = data[key];
411
- break;
412
-
413
- case 'summaryCn':
414
- dbUpdates.summary_cn = data.summaryCn;
415
- recipe.summaryCn = data.summaryCn;
416
- break;
417
- case 'summaryEn':
418
- dbUpdates.summary_en = data.summaryEn;
419
- recipe.summaryEn = data.summaryEn;
420
- break;
421
- case 'usageGuideCn':
422
- dbUpdates.usage_guide_cn = data.usageGuideCn;
423
- recipe.usageGuideCn = data.usageGuideCn;
424
- break;
425
- case 'usageGuideEn':
426
- dbUpdates.usage_guide_en = data.usageGuideEn;
427
- recipe.usageGuideEn = data.usageGuideEn;
428
- break;
429
-
430
- case 'knowledgeType':
431
- dbUpdates.knowledge_type = data.knowledgeType;
432
- recipe.knowledgeType = data.knowledgeType;
433
- // 联动更新 kind
434
- dbUpdates.kind = inferKind(data.knowledgeType);
435
- recipe.kind = dbUpdates.kind;
436
- break;
437
-
438
- case 'complexity':
439
- dbUpdates.complexity = data.complexity;
440
- recipe.complexity = data.complexity;
441
- break;
442
-
443
- case 'content':
444
- // 深合并: 保留已有字段,覆盖传入字段
445
- recipe.content = { ...recipe.content, ...data.content };
446
- dbUpdates.content_json = JSON.stringify(recipe.content);
447
- break;
448
-
449
- case 'relations':
450
- recipe.relations = { ...recipe.relations, ...data.relations };
451
- dbUpdates.relations_json = JSON.stringify(recipe.relations);
452
- break;
453
-
454
- case 'constraints':
455
- recipe.constraints = { ...recipe.constraints, ...data.constraints };
456
- dbUpdates.constraints_json = JSON.stringify(recipe.constraints);
457
- break;
458
-
459
- case 'dimensions':
460
- recipe.dimensions = { ...recipe.dimensions, ...data.dimensions };
461
- dbUpdates.dimensions_json = JSON.stringify(recipe.dimensions);
462
- break;
463
-
464
- case 'tags':
465
- recipe.tags = data.tags;
466
- dbUpdates.tags_json = JSON.stringify(data.tags);
467
- break;
468
- }
469
- }
470
-
471
- if (Object.keys(dbUpdates).length === 0) {
472
- throw new ValidationError('No updatable fields provided');
473
- }
474
-
475
- const updated = await this.recipeRepository.update(recipeId, dbUpdates);
476
-
477
- // 若 relations 发生变更,同步到 knowledge_edges
478
- if (dbUpdates.relations_json) {
479
- this._syncRelationsToGraph(recipeId, recipe.relations);
480
- }
481
-
482
- // V2: 更新后同步落盘
483
- this._persistToFile(updated);
484
-
485
- await this.auditLogger.log({
486
- action: 'update_recipe',
487
- resourceType: 'recipe',
488
- resourceId: recipeId,
489
- actor: context.userId,
490
- details: `Updated recipe fields: ${Object.keys(dbUpdates).join(', ')}`,
491
- timestamp: Math.floor(Date.now() / 1000),
492
- });
493
-
494
- this.logger.info('Recipe updated', {
495
- recipeId,
496
- updatedBy: context.userId,
497
- fields: Object.keys(dbUpdates),
498
- });
499
-
500
- return updated;
501
- } catch (error) {
502
- this.logger.error('Error updating recipe', {
503
- recipeId,
504
- error: error.message,
505
- });
506
- throw error;
507
- }
508
- }
509
-
510
- /**
511
- * 删除 Recipe
512
- */
513
- async deleteRecipe(recipeId, context) {
514
- try {
515
- const recipe = await this.recipeRepository.findById(recipeId);
516
-
517
- if (!recipe) {
518
- throw new NotFoundError('Recipe not found', 'recipe', recipeId);
519
- }
520
-
521
- // V2: 删除 .md 文件
522
- this._removeFile(recipe);
523
-
524
- // 清除 knowledge_edges 中所有关联边
525
- this._removeAllEdges(recipeId);
526
-
527
- const deleted = await this.recipeRepository.delete(recipeId);
528
-
529
- await this.auditLogger.log({
530
- action: 'delete_recipe',
531
- resourceType: 'recipe',
532
- resourceId: recipeId,
533
- actor: context.userId,
534
- details: `Deleted recipe: ${recipe.title}`,
535
- timestamp: Math.floor(Date.now() / 1000),
536
- });
537
-
538
- this.logger.info('Recipe deleted', {
539
- recipeId,
540
- deletedBy: context.userId,
541
- title: recipe.title,
542
- });
543
-
544
- return { success: true, id: recipeId };
545
- } catch (error) {
546
- this.logger.error('Error deleting recipe', {
547
- recipeId,
548
- error: error.message,
549
- });
550
- throw error;
551
- }
552
- }
553
-
554
- /**
555
- * 按 Kind 查询 Recipe 列表
556
- */
557
- async listByKind(kind, pagination = {}) {
558
- try {
559
- const { page = 1, pageSize = 20 } = pagination;
560
- return this.recipeRepository.findByKind(kind, { page, pageSize });
561
- } catch (error) {
562
- this.logger.error('Error listing recipes by kind', {
563
- kind,
564
- error: error.message,
565
- });
566
- throw error;
567
- }
568
- }
569
-
570
- /**
571
- * 查询 Recipe 列表(支持多条件组合过滤)
572
- */
573
- async listRecipes(filters = {}, pagination = {}) {
574
- try {
575
- const { status, language, category, knowledgeType, kind, scope, tag } = filters;
576
- const { page = 1, pageSize = 20 } = pagination;
577
-
578
- // 构建组合过滤条件(DB 列名 → 值)
579
- const dbFilters = {};
580
- if (status) dbFilters.status = status;
581
- if (language) dbFilters.language = language;
582
- if (category) dbFilters.category = category;
583
- if (knowledgeType) dbFilters.knowledge_type = knowledgeType;
584
- if (kind) dbFilters.kind = kind;
585
- if (scope) dbFilters.scope = scope;
586
-
587
- // tag 过滤通过 tags_json LIKE 实现
588
- if (tag) dbFilters._tagLike = tag;
589
-
590
- return this.recipeRepository.findWithPagination(dbFilters, { page, pageSize });
591
- } catch (error) {
592
- this.logger.error('Error listing recipes', {
593
- error: error.message,
594
- filters,
595
- });
596
- throw error;
597
- }
598
- }
599
-
600
- /**
601
- * 搜索 Recipe
602
- */
603
- async searchRecipes(keyword, pagination = {}) {
604
- try {
605
- const { page = 1, pageSize = 20 } = pagination;
606
- return this.recipeRepository.search(keyword, { page, pageSize });
607
- } catch (error) {
608
- this.logger.error('Error searching recipes', {
609
- keyword,
610
- error: error.message,
611
- });
612
- throw error;
613
- }
614
- }
615
-
616
- /**
617
- * 获取推荐 Recipe
618
- */
619
- async getRecommendations(limit = 10) {
620
- try {
621
- return this.recipeRepository.getRecommendations(limit);
622
- } catch (error) {
623
- this.logger.error('Error getting recommendations', {
624
- error: error.message,
625
- });
626
- throw error;
627
- }
628
- }
629
-
630
- /**
631
- * 获取单个 Recipe
632
- */
633
- async getRecipe(recipeId) {
634
- try {
635
- const recipe = await this.recipeRepository.findById(recipeId);
636
- if (!recipe) {
637
- throw new NotFoundError('Recipe not found', 'recipe', recipeId);
638
- }
639
- return recipe;
640
- } catch (error) {
641
- this.logger.error('Error getting recipe', {
642
- recipeId,
643
- error: error.message,
644
- });
645
- throw error;
646
- }
647
- }
648
-
649
- /**
650
- * 获取 Recipe 统计
651
- */
652
- async getRecipeStats() {
653
- try {
654
- return this.recipeRepository.getStats();
655
- } catch (error) {
656
- this.logger.error('Error getting recipe stats', {
657
- error: error.message,
658
- });
659
- throw error;
660
- }
661
- }
662
-
663
- // ─── Knowledge Graph 同步 ─────────────────────────────────────
664
-
665
- /**
666
- * 将 Recipe 的 relations 同步到 knowledge_edges 表
667
- * 策略:先删旧边,再批量写入新边(保证幂等)
668
- */
669
- _syncRelationsToGraph(recipeId, relations) {
670
- const gs = this._knowledgeGraphService;
671
- if (!gs) return;
672
-
673
- try {
674
- // 1. 删除当前 Recipe 的所有出边
675
- gs.db.prepare(
676
- `DELETE FROM knowledge_edges WHERE from_id = ? AND from_type = 'recipe'`
677
- ).run(recipeId);
678
-
679
- // 2. 写入新边
680
- if (!relations || typeof relations !== 'object') return;
681
-
682
- for (const [relType, targets] of Object.entries(relations)) {
683
- if (!Array.isArray(targets)) continue;
684
- for (const t of targets) {
685
- const targetId = t.target || t.id || (typeof t === 'string' ? t : null);
686
- if (targetId) {
687
- gs.addEdge(recipeId, 'recipe', targetId, 'recipe', relType, {
688
- weight: t.weight || 1.0,
689
- });
690
- }
691
- }
692
- }
693
- } catch (err) {
694
- // 同步失败不应阻断主流程(表可能不存在)
695
- this.logger.warn('Failed to sync relations to knowledge_edges', {
696
- recipeId, error: err.message,
697
- });
698
- }
699
- }
700
-
701
- /**
702
- * 删除 Recipe 关联的所有 knowledge_edges(出边 + 入边)
703
- */
704
- _removeAllEdges(recipeId) {
705
- const gs = this._knowledgeGraphService;
706
- if (!gs) return;
707
-
708
- try {
709
- gs.db.prepare(
710
- `DELETE FROM knowledge_edges WHERE from_id = ? OR to_id = ?`
711
- ).run(recipeId, recipeId);
712
- } catch (err) {
713
- this.logger.warn('Failed to remove edges from knowledge_edges', {
714
- recipeId, error: err.message,
715
- });
716
- }
717
- }
718
-
719
- /**
720
- * 验证创建输入
721
- */
722
- _validateCreateInput(data) {
723
- if (!data.title || data.title.trim().length === 0) {
724
- throw new ValidationError('Title is required');
725
- }
726
-
727
- if (!data.language || data.language.trim().length === 0) {
728
- throw new ValidationError('Language is required');
729
- }
730
-
731
- if (!data.category || data.category.trim().length === 0) {
732
- throw new ValidationError('Category is required');
733
- }
734
-
735
- // 内容至少需要 pattern 或 rationale 或 steps 或 markdown
736
- const c = data.content || {};
737
- if (!c.pattern && !c.rationale && !(c.steps?.length > 0) && !c.markdown && !data.pattern) {
738
- throw new ValidationError('Content is required (pattern, rationale, steps, or markdown)');
739
- }
740
- }
741
-
742
- /* ═══ V2 文件落盘 ═══════════════════════════════════════ */
743
-
744
- /**
745
- * 将 Recipe 落盘到 AutoSnippet/recipes/{category}/ 目录
746
- * 落盘后回写 source_file 到 DB(保证源文件路径可追溯)
747
- * 失败不阻断主流程(DB 写入已成功)
748
- */
749
- _persistToFile(recipe) {
750
- if (!this._fileWriter) return;
751
- try {
752
- const oldSourceFile = recipe.sourceFile;
753
- this._fileWriter.persistRecipe(recipe);
754
- // 回写 source_file 到 DB(新建或路径变更时)
755
- if (recipe.sourceFile && recipe.sourceFile !== oldSourceFile) {
756
- this.recipeRepository.update(recipe.id, { source_file: recipe.sourceFile }).catch(err => {
757
- this.logger.warn('Failed to update source_file in DB', {
758
- recipeId: recipe.id, error: err.message,
759
- });
760
- });
761
- }
762
- } catch (err) {
763
- this.logger.warn('Recipe file persist failed (non-blocking)', {
764
- recipeId: recipe?.id,
765
- error: err.message,
766
- });
767
- }
768
- }
769
-
770
- /**
771
- * 删除 Recipe 对应的 .md 文件
772
- */
773
- _removeFile(recipe) {
774
- if (!this._fileWriter) return;
775
- try {
776
- this._fileWriter.removeRecipe(recipe);
777
- } catch (err) {
778
- this.logger.warn('Recipe file remove failed (non-blocking)', {
779
- recipeId: recipe?.id,
780
- error: err.message,
781
- });
782
- }
783
- }
784
- }
785
-
786
- export default RecipeService;