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
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Knowledge API 路由 (V3)
3
+ * 统一知识条目的 CRUD + 生命周期操作
4
+ * 替代 recipes.js + candidates.js (旧路由继续保留用于向后兼容)
5
+ */
6
+
7
+ import express from 'express';
8
+ import { asyncHandler } from '../middleware/errorHandler.js';
9
+ import { getServiceContainer } from '../../injection/ServiceContainer.js';
10
+ import { ValidationError } from '../../shared/errors/index.js';
11
+ import Logger from '../../infrastructure/logging/Logger.js';
12
+ import { getContext, safeInt } from '../utils/routeHelpers.js';
13
+
14
+ const logger = Logger.getInstance();
15
+ const router = express.Router();
16
+
17
+ const MAX_BATCH_SIZE = 100;
18
+
19
+ /* ═══ 查询 ═══════════════════════════════════════════════ */
20
+
21
+ /**
22
+ * GET /api/v1/knowledge
23
+ * 获取知识条目列表(支持筛选和分页)
24
+ */
25
+ router.get('/', asyncHandler(async (req, res) => {
26
+ const { lifecycle, kind, category, language, knowledgeType, scope, keyword, tag, source } = req.query;
27
+ const page = safeInt(req.query.page, 1);
28
+ const pageSize = safeInt(req.query.limit, 20, 1, 1000);
29
+
30
+ const container = getServiceContainer();
31
+ const knowledgeService = container.get('knowledgeService');
32
+
33
+ if (keyword) {
34
+ const result = await knowledgeService.search(keyword, { page, pageSize });
35
+ return res.json({ success: true, data: result });
36
+ }
37
+
38
+ const filters = {};
39
+ if (lifecycle) filters.lifecycle = lifecycle;
40
+ if (kind) filters.kind = kind;
41
+ if (category) filters.category = category;
42
+ if (language) filters.language = language;
43
+ if (knowledgeType) filters.knowledgeType = knowledgeType;
44
+ if (scope) filters.scope = scope;
45
+ if (tag) filters.tag = tag;
46
+ if (source) filters.source = source;
47
+
48
+ const result = await knowledgeService.list(filters, { page, pageSize });
49
+ res.json({ success: true, data: result });
50
+ }));
51
+
52
+ /**
53
+ * GET /api/v1/knowledge/stats
54
+ * 获取统计信息
55
+ */
56
+ router.get('/stats', asyncHandler(async (req, res) => {
57
+ const container = getServiceContainer();
58
+ const knowledgeService = container.get('knowledgeService');
59
+ const stats = await knowledgeService.getStats();
60
+ res.json({ success: true, data: stats });
61
+ }));
62
+
63
+ /**
64
+ * GET /api/v1/knowledge/:id
65
+ * 获取知识条目详情
66
+ */
67
+ router.get('/:id', asyncHandler(async (req, res) => {
68
+ const { id } = req.params;
69
+ const container = getServiceContainer();
70
+ const knowledgeService = container.get('knowledgeService');
71
+ const entry = await knowledgeService.get(id);
72
+ res.json({ success: true, data: entry.toJSON() });
73
+ }));
74
+
75
+ /* ═══ CRUD ═══════════════════════════════════════════════ */
76
+
77
+ /**
78
+ * POST /api/v1/knowledge
79
+ * 创建知识条目(wire format 直通)
80
+ */
81
+ router.post('/', asyncHandler(async (req, res) => {
82
+ const data = req.body;
83
+
84
+ if (!data.title || !data.content) {
85
+ throw new ValidationError('title and content are required');
86
+ }
87
+
88
+ const container = getServiceContainer();
89
+ const knowledgeService = container.get('knowledgeService');
90
+ const context = getContext(req);
91
+
92
+ const entry = await knowledgeService.create(data, context);
93
+ res.status(201).json({
94
+ success: true,
95
+ data: entry.toJSON(),
96
+ });
97
+ }));
98
+
99
+ /**
100
+ * PATCH /api/v1/knowledge/:id
101
+ * 更新知识条目(白名单字段)
102
+ */
103
+ router.patch('/:id', asyncHandler(async (req, res) => {
104
+ const { id } = req.params;
105
+ const container = getServiceContainer();
106
+ const knowledgeService = container.get('knowledgeService');
107
+ const context = getContext(req);
108
+
109
+ const entry = await knowledgeService.update(id, req.body, context);
110
+ res.json({ success: true, data: entry.toJSON() });
111
+ }));
112
+
113
+ /**
114
+ * DELETE /api/v1/knowledge/:id
115
+ * 删除知识条目
116
+ */
117
+ router.delete('/:id', asyncHandler(async (req, res) => {
118
+ const { id } = req.params;
119
+ const container = getServiceContainer();
120
+ const knowledgeService = container.get('knowledgeService');
121
+ const context = getContext(req);
122
+
123
+ const result = await knowledgeService.delete(id, context);
124
+ res.json({ success: true, data: result });
125
+ }));
126
+
127
+ /* ═══ 生命周期操作(3 状态: pending / active / deprecated)═══ */
128
+
129
+ /**
130
+ * PATCH /api/v1/knowledge/:id/publish
131
+ * 发布 (pending → active) — 仅开发者
132
+ */
133
+ router.patch('/:id/publish', asyncHandler(async (req, res) => {
134
+ const { id } = req.params;
135
+ const container = getServiceContainer();
136
+ const knowledgeService = container.get('knowledgeService');
137
+ const context = getContext(req);
138
+
139
+ const entry = await knowledgeService.publish(id, context);
140
+ res.json({ success: true, data: entry.toJSON() });
141
+ }));
142
+
143
+ /**
144
+ * PATCH /api/v1/knowledge/:id/deprecate
145
+ * 废弃 (pending|active → deprecated)
146
+ */
147
+ router.patch('/:id/deprecate', asyncHandler(async (req, res) => {
148
+ const { id } = req.params;
149
+ const { reason } = req.body;
150
+
151
+ if (!reason) {
152
+ throw new ValidationError('reason is required for deprecation');
153
+ }
154
+
155
+ const container = getServiceContainer();
156
+ const knowledgeService = container.get('knowledgeService');
157
+ const context = getContext(req);
158
+
159
+ const entry = await knowledgeService.deprecate(id, reason, context);
160
+ res.json({ success: true, data: entry.toJSON() });
161
+ }));
162
+
163
+ /**
164
+ * PATCH /api/v1/knowledge/:id/reactivate
165
+ * 重新激活 (deprecated → pending)
166
+ */
167
+ router.patch('/:id/reactivate', asyncHandler(async (req, res) => {
168
+ const { id } = req.params;
169
+ const container = getServiceContainer();
170
+ const knowledgeService = container.get('knowledgeService');
171
+ const context = getContext(req);
172
+
173
+ const entry = await knowledgeService.reactivate(id, context);
174
+ res.json({ success: true, data: entry.toJSON() });
175
+ }));
176
+
177
+ /* ═══ 批量操作 ═══════════════════════════════════════════ */
178
+
179
+ /**
180
+ * POST /api/v1/knowledge/batch-publish
181
+ * 批量发布 (pending → active)
182
+ * 支持 autoApprovableOnly=true 参数,只发布 auto_approvable 的条目
183
+ */
184
+ router.post('/batch-publish', asyncHandler(async (req, res) => {
185
+ const { ids } = req.body;
186
+
187
+ if (!Array.isArray(ids) || ids.length === 0) {
188
+ throw new ValidationError('ids array is required and must not be empty');
189
+ }
190
+ if (ids.length > MAX_BATCH_SIZE) {
191
+ throw new ValidationError(`Batch size exceeds limit of ${MAX_BATCH_SIZE}`);
192
+ }
193
+
194
+ const container = getServiceContainer();
195
+ const knowledgeService = container.get('knowledgeService');
196
+ const context = getContext(req);
197
+
198
+ const results = await Promise.allSettled(
199
+ ids.map(id => knowledgeService.publish(id, context)),
200
+ );
201
+
202
+ const published = results.filter(r => r.status === 'fulfilled').map(r => r.value.toJSON());
203
+ const failed = results
204
+ .map((r, i) => r.status === 'rejected' ? { id: ids[i], error: r.reason?.message } : null)
205
+ .filter(Boolean);
206
+
207
+ res.json({
208
+ success: true,
209
+ data: { published, failed, total: ids.length, successCount: published.length, failureCount: failed.length },
210
+ });
211
+ }));
212
+
213
+ /* ═══ 使用 / 质量 ═══════════════════════════════════════ */
214
+
215
+ /**
216
+ * POST /api/v1/knowledge/:id/usage
217
+ * 记录使用(adoption / application / guard_hit / view / success)
218
+ */
219
+ router.post('/:id/usage', asyncHandler(async (req, res) => {
220
+ const { id } = req.params;
221
+ const { type = 'adoption', feedback } = req.body;
222
+ const context = getContext(req);
223
+
224
+ const container = getServiceContainer();
225
+ const knowledgeService = container.get('knowledgeService');
226
+
227
+ await knowledgeService.incrementUsage(id, type, { actor: context.userId, feedback });
228
+ res.json({ success: true, message: `${type} recorded` });
229
+ }));
230
+
231
+ /**
232
+ * PATCH /api/v1/knowledge/:id/quality
233
+ * 重新计算质量评分
234
+ */
235
+ router.patch('/:id/quality', asyncHandler(async (req, res) => {
236
+ const { id } = req.params;
237
+ const context = getContext(req);
238
+
239
+ const container = getServiceContainer();
240
+ const knowledgeService = container.get('knowledgeService');
241
+
242
+ const result = await knowledgeService.updateQuality(id, context);
243
+ res.json({ success: true, data: result });
244
+ }));
245
+
246
+ export default router;
@@ -44,13 +44,13 @@ router.get('/', asyncHandler(async (req, res) => {
44
44
  const results = {};
45
45
  const pagination = { page, pageSize: limit };
46
46
 
47
- // 搜索 Recipe(统一模型,包含所有知识类型)
47
+ // 搜索知识条目(V3 统一模型)
48
48
  if (type === 'all' || type === 'recipe' || type === 'solution') {
49
49
  try {
50
- const recipeService = container.get('recipeService');
51
- results.recipes = await recipeService.searchRecipes(q, pagination);
50
+ const knowledgeService = container.get('knowledgeService');
51
+ results.recipes = await knowledgeService.search(q, pagination);
52
52
  } catch (err) {
53
- logger.warn('Recipe 搜索失败', { query: q, error: err.message });
53
+ logger.warn('Knowledge 搜索失败', { query: q, error: err.message });
54
54
  results.recipes = { items: [], total: 0 };
55
55
  }
56
56
  }
@@ -66,11 +66,11 @@ router.get('/', asyncHandler(async (req, res) => {
66
66
  }
67
67
  }
68
68
 
69
- // 搜索 Candidate
69
+ // 搜索候选知识条目 (V3: lifecycle=draft/pending)
70
70
  if (type === 'all' || type === 'candidate') {
71
71
  try {
72
- const candidateService = container.get('candidateService');
73
- results.candidates = await candidateService.searchCandidates(q, pagination);
72
+ const knowledgeService = container.get('knowledgeService');
73
+ results.candidates = await knowledgeService.search(q, pagination);
74
74
  } catch (err) {
75
75
  logger.warn('Candidate 搜索失败', { query: q, error: err.message });
76
76
  results.candidates = { items: [], total: 0 };
@@ -172,18 +172,14 @@ router.get('/graph/all', asyncHandler(async (req, res) => {
172
172
  const nodeTypes = {}; // id → 主要类型(供前端区分渲染)
173
173
  const nodeCategories = {}; // id → category/target 名(供前端分组布局)
174
174
  if (nodeMap.size > 0) {
175
- const recipeService = container.get('recipeService');
176
- // 使用 repository.findById 避免 RecipeService.getRecipe 在找不到时记录 error 日志
177
- const recipeRepo = recipeService?.recipeRepository ?? null;
175
+ const knowledgeRepo = container.get('knowledgeRepository');
178
176
  for (const [id, types] of nodeMap) {
179
- // 记录节点主要类型
180
177
  const primaryType = types.has('recipe') ? 'recipe' : [...types][0];
181
178
  nodeTypes[id] = primaryType;
182
179
 
183
- if (primaryType === 'recipe' && recipeRepo) {
184
- // 仅 recipe 类型才查 DB;直接用 repo 避免 NotFoundError 日志噪音
180
+ if ((primaryType === 'recipe' || primaryType === 'knowledge') && knowledgeRepo) {
185
181
  try {
186
- const r = await recipeRepo.findById(id);
182
+ const r = await knowledgeRepo.findById(id);
187
183
  if (r) {
188
184
  nodeLabels[id] = r.title || r.name || id;
189
185
  nodeCategories[id] = r.category || '';
@@ -191,7 +187,6 @@ router.get('/graph/all', asyncHandler(async (req, res) => {
191
187
  }
192
188
  } catch { /* not found – fall through */ }
193
189
  }
194
- // module / candidate 或查不到的 recipe → 直接用 ID
195
190
  nodeLabels[id] = id;
196
191
  }
197
192
  }
@@ -226,9 +221,9 @@ router.post('/context-aware', asyncHandler(async (req, res) => {
226
221
  throw new ValidationError('keyword is required');
227
222
  }
228
223
  const container = getServiceContainer();
229
- const recipeService = container.get('recipeService');
224
+ const knowledgeService = container.get('knowledgeService');
230
225
  const pageSize = Math.min(limit || 10, 100);
231
- const list = await recipeService.searchRecipes(keyword, { page: 1, pageSize });
226
+ const list = await knowledgeService.search(keyword, { page: 1, pageSize });
232
227
  const items = list.data || list.items || [];
233
228
  const results = items.map(r => ({
234
229
  name: (r.title || r.id) + '.md',
@@ -5,7 +5,7 @@
5
5
 
6
6
  import express from 'express';
7
7
  import { asyncHandler } from '../middleware/errorHandler.js';
8
- import { listSkills, loadSkill, createSkill, suggestSkills } from '../../external/mcp/handlers/skill.js';
8
+ import { listSkills, loadSkill, createSkill, deleteSkill, updateSkill, suggestSkills } from '../../external/mcp/handlers/skill.js';
9
9
  import { ValidationError } from '../../shared/errors/index.js';
10
10
 
11
11
  const router = express.Router();
@@ -107,4 +107,47 @@ router.post('/', asyncHandler(async (req, res) => {
107
107
  res.status(201).json({ success: true, data: parsed.data });
108
108
  }));
109
109
 
110
+ /**
111
+ * PUT /api/v1/skills/:name
112
+ * 更新项目级 Skill(description / content)
113
+ */
114
+ router.put('/:name', asyncHandler(async (req, res) => {
115
+ const { name } = req.params;
116
+ const { description, content } = req.body;
117
+
118
+ if (!description && !content) {
119
+ throw new ValidationError('At least one of description or content must be provided');
120
+ }
121
+
122
+ const raw = updateSkill(null, { name, description, content });
123
+ const parsed = JSON.parse(raw);
124
+
125
+ if (!parsed.success) {
126
+ const status = parsed.error?.code === 'SKILL_NOT_FOUND' ? 404
127
+ : parsed.error?.code === 'BUILTIN_PROTECTED' ? 403 : 500;
128
+ return res.status(status).json(parsed);
129
+ }
130
+
131
+ res.json({ success: true, data: parsed.data });
132
+ }));
133
+
134
+ /**
135
+ * DELETE /api/v1/skills/:name
136
+ * 删除项目级 Skill
137
+ */
138
+ router.delete('/:name', asyncHandler(async (req, res) => {
139
+ const { name } = req.params;
140
+
141
+ const raw = deleteSkill(null, { name });
142
+ const parsed = JSON.parse(raw);
143
+
144
+ if (!parsed.success) {
145
+ const status = parsed.error?.code === 'SKILL_NOT_FOUND' ? 404
146
+ : parsed.error?.code === 'BUILTIN_PROTECTED' ? 403 : 500;
147
+ return res.status(status).json(parsed);
148
+ }
149
+
150
+ res.json({ success: true, data: parsed.data });
151
+ }));
152
+
110
153
  export default router;
@@ -0,0 +1,143 @@
1
+ /**
2
+ * GraphCache — 基于文件的图数据持久化缓存
3
+ *
4
+ * 功能:
5
+ * 1. 将图数据序列化为 JSON 写入磁盘
6
+ * 2. 基于 contentHash 判断缓存是否有效(Package.swift / 源文件)
7
+ * 3. 支持 SPM 依赖图和 AST ProjectGraph 两种场景
8
+ *
9
+ * 缓存位置: {projectRoot}/.autosnippet/cache/
10
+ */
11
+
12
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from 'node:fs';
13
+ import { join, relative } from 'node:path';
14
+ import { createHash } from 'node:crypto';
15
+ import Logger from '../logging/Logger.js';
16
+
17
+ export class GraphCache {
18
+ #cacheDir;
19
+ #logger;
20
+
21
+ /**
22
+ * @param {string} projectRoot 项目根目录
23
+ */
24
+ constructor(projectRoot) {
25
+ this.#cacheDir = join(projectRoot, '.autosnippet', 'cache');
26
+ this.#logger = Logger.getInstance();
27
+ }
28
+
29
+ /**
30
+ * 保存缓存
31
+ * @param {string} key 缓存键名(生成 {key}.json)
32
+ * @param {object} data 要缓存的数据
33
+ * @param {object} meta 元信息(含 hash、timestamp 等)
34
+ */
35
+ save(key, data, meta = {}) {
36
+ try {
37
+ if (!existsSync(this.#cacheDir)) {
38
+ mkdirSync(this.#cacheDir, { recursive: true });
39
+ }
40
+ const payload = {
41
+ version: 1,
42
+ savedAt: new Date().toISOString(),
43
+ ...meta,
44
+ data,
45
+ };
46
+ const filePath = join(this.#cacheDir, `${key}.json`);
47
+ writeFileSync(filePath, JSON.stringify(payload), 'utf-8');
48
+ this.#logger.debug(`[GraphCache] saved: ${key} (${JSON.stringify(payload).length} bytes)`);
49
+ } catch (err) {
50
+ this.#logger.warn(`[GraphCache] save failed for ${key}: ${err.message}`);
51
+ }
52
+ }
53
+
54
+ /**
55
+ * 加载缓存
56
+ * @param {string} key 缓存键名
57
+ * @returns {{ data: object, [key: string]: any } | null}
58
+ */
59
+ load(key) {
60
+ try {
61
+ const filePath = join(this.#cacheDir, `${key}.json`);
62
+ if (!existsSync(filePath)) return null;
63
+ const raw = readFileSync(filePath, 'utf-8');
64
+ return JSON.parse(raw);
65
+ } catch (err) {
66
+ this.#logger.warn(`[GraphCache] load failed for ${key}: ${err.message}`);
67
+ return null;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * 检查缓存是否有效(hash 匹配)
73
+ * @param {string} key 缓存键
74
+ * @param {string} currentHash 当前内容的 hash
75
+ * @returns {boolean}
76
+ */
77
+ isValid(key, currentHash) {
78
+ const cached = this.load(key);
79
+ if (!cached) return false;
80
+ return cached.contentHash === currentHash;
81
+ }
82
+
83
+ /**
84
+ * 删除缓存
85
+ * @param {string} key
86
+ */
87
+ invalidate(key) {
88
+ try {
89
+ const filePath = join(this.#cacheDir, `${key}.json`);
90
+ if (existsSync(filePath)) {
91
+ unlinkSync(filePath);
92
+ this.#logger.debug(`[GraphCache] invalidated: ${key}`);
93
+ }
94
+ } catch (err) {
95
+ this.#logger.warn(`[GraphCache] invalidate failed for ${key}: ${err.message}`);
96
+ }
97
+ }
98
+
99
+ /**
100
+ * 计算文件内容 hash
101
+ * @param {string} filePath 文件绝对路径
102
+ * @returns {string} sha256 hex (前 16 字符)
103
+ */
104
+ computeFileHash(filePath) {
105
+ try {
106
+ const content = readFileSync(filePath, 'utf-8');
107
+ return this.computeContentHash(content);
108
+ } catch {
109
+ return '';
110
+ }
111
+ }
112
+
113
+ /**
114
+ * 计算字符串内容 hash
115
+ * @param {string} content
116
+ * @returns {string} sha256 hex (前 16 字符)
117
+ */
118
+ computeContentHash(content) {
119
+ return createHash('sha256').update(content).digest('hex').substring(0, 16);
120
+ }
121
+
122
+ /**
123
+ * 批量计算文件 hash 映射
124
+ * @param {string[]} filePaths 文件绝对路径数组
125
+ * @param {string} projectRoot 项目根目录
126
+ * @returns {Object<string, string>} { relativePath: hash }
127
+ */
128
+ computeFileHashes(filePaths, projectRoot) {
129
+ const hashes = {};
130
+ for (const fp of filePaths) {
131
+ const rel = relative(projectRoot, fp);
132
+ hashes[rel] = this.computeFileHash(fp);
133
+ }
134
+ return hashes;
135
+ }
136
+
137
+ /**
138
+ * 获取缓存目录路径
139
+ */
140
+ getCacheDir() {
141
+ return this.#cacheDir;
142
+ }
143
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Migration 015: Create token_usage table
3
+ *
4
+ * 持久化 AI 调用的 Token 消耗记录,支持近 7 日消耗趋势查询。
5
+ * 每次 ChatAgent.execute() 完成后写入一条记录。
6
+ */
7
+ export default function migrate(db) {
8
+ db.exec(`
9
+ CREATE TABLE IF NOT EXISTS token_usage (
10
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
11
+ timestamp INTEGER NOT NULL,
12
+ source TEXT NOT NULL DEFAULT 'unknown',
13
+ dimension TEXT,
14
+ provider TEXT,
15
+ model TEXT,
16
+ input_tokens INTEGER NOT NULL DEFAULT 0,
17
+ output_tokens INTEGER NOT NULL DEFAULT 0,
18
+ total_tokens INTEGER NOT NULL DEFAULT 0,
19
+ duration_ms INTEGER,
20
+ tool_calls INTEGER DEFAULT 0,
21
+ session_id TEXT
22
+ );
23
+
24
+ CREATE INDEX IF NOT EXISTS idx_token_usage_timestamp ON token_usage(timestamp);
25
+ CREATE INDEX IF NOT EXISTS idx_token_usage_source ON token_usage(source);
26
+ `);
27
+ }