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,373 @@
1
+ import { BaseRepository } from '../base/BaseRepository.js';
2
+ import { KnowledgeEntry, Lifecycle, inferKind } from '../../domain/knowledge/index.js';
3
+ import Logger from '../../infrastructure/logging/Logger.js';
4
+
5
+ /**
6
+ * KnowledgeRepositoryImpl — 统一知识实体仓储实现
7
+ *
8
+ * 面向 knowledge_entries 表的 SQLite 持久化。
9
+ * 替代 CandidateRepositoryImpl + RecipeRepositoryImpl。
10
+ */
11
+ export class KnowledgeRepositoryImpl extends BaseRepository {
12
+ constructor(database) {
13
+ super(database, 'knowledge_entries');
14
+ this.logger = Logger.getInstance();
15
+ }
16
+
17
+ /* ═══ CRUD ═══════════════════════════════════════════ */
18
+
19
+ /**
20
+ * 创建 KnowledgeEntry
21
+ * @param {KnowledgeEntry} entry
22
+ * @returns {Promise<KnowledgeEntry>}
23
+ */
24
+ async create(entry) {
25
+ if (!entry || !entry.isValid()) {
26
+ throw new Error('Invalid knowledge entry: title + content required');
27
+ }
28
+
29
+ try {
30
+ const row = this._entityToRow(entry);
31
+ const keys = Object.keys(row);
32
+ const placeholders = keys.map(() => '?').join(', ');
33
+ const query = `INSERT INTO knowledge_entries (${keys.join(', ')}) VALUES (${placeholders})`;
34
+ this.db.prepare(query).run(...Object.values(row));
35
+ return this.findById(entry.id);
36
+ } catch (error) {
37
+ this.logger.error('Error creating knowledge entry', {
38
+ entryId: entry.id,
39
+ error: error.message,
40
+ });
41
+ throw error;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * 更新 KnowledgeEntry(接受完整实体或部分 wire format)
47
+ * @param {string} id
48
+ * @param {Object|KnowledgeEntry} updates
49
+ * @returns {Promise<KnowledgeEntry>}
50
+ */
51
+ async update(id, updates) {
52
+ try {
53
+ const existing = await this.findById(id);
54
+ if (!existing) throw new Error(`Knowledge entry not found: ${id}`);
55
+
56
+ // 如果传入的是完整实体
57
+ if (updates instanceof KnowledgeEntry) {
58
+ const row = this._entityToRow(updates);
59
+ delete row.id;
60
+ delete row.created_at;
61
+ row.updated_at = Math.floor(Date.now() / 1000);
62
+
63
+ const setClauses = Object.keys(row).map(k => `${k} = ?`).join(', ');
64
+ this.db.prepare(`UPDATE knowledge_entries SET ${setClauses} WHERE id = ?`)
65
+ .run(...Object.values(row), id);
66
+ return this.findById(id);
67
+ }
68
+
69
+ // 部分更新 — 合并 wire format 到现有实体
70
+ const merged = KnowledgeEntry.fromJSON({
71
+ ...existing.toJSON(),
72
+ ...updates,
73
+ updated_at: Math.floor(Date.now() / 1000),
74
+ });
75
+ const row = this._entityToRow(merged);
76
+ delete row.id;
77
+ delete row.created_at;
78
+
79
+ const setClauses = Object.keys(row).map(k => `${k} = ?`).join(', ');
80
+ this.db.prepare(`UPDATE knowledge_entries SET ${setClauses} WHERE id = ?`)
81
+ .run(...Object.values(row), id);
82
+ return this.findById(id);
83
+ } catch (error) {
84
+ this.logger.error('Error updating knowledge entry', {
85
+ id,
86
+ error: error.message,
87
+ });
88
+ throw error;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * 删除
94
+ * @param {string} id
95
+ * @returns {Promise<boolean>}
96
+ */
97
+ async delete(id) {
98
+ try {
99
+ const result = this.db.prepare('DELETE FROM knowledge_entries WHERE id = ?').run(id);
100
+ return result.changes > 0;
101
+ } catch (error) {
102
+ this.logger.error('Error deleting knowledge entry', { id, error: error.message });
103
+ throw error;
104
+ }
105
+ }
106
+
107
+ /* ═══ 查询 ═══════════════════════════════════════════ */
108
+
109
+ /**
110
+ * 分页查询
111
+ * @override
112
+ */
113
+ async findWithPagination(filters = {}, options = {}) {
114
+ const { page = 1, pageSize = 20, orderBy = 'created_at', order = 'DESC' } = options;
115
+ const offset = (page - 1) * pageSize;
116
+
117
+ const conditions = [];
118
+ const params = [];
119
+
120
+ // 处理特殊过滤字段
121
+ const { _tagLike, _search, lifecycle: lcFilter, ...normalFilters } = filters;
122
+
123
+ // lifecycle 筛选:将新 3 状态映射到可能存在的旧值
124
+ if (lcFilter) {
125
+ if (lcFilter === 'pending') {
126
+ conditions.push(`lifecycle IN ('pending', 'draft', 'approved', 'auto_approved')`);
127
+ } else if (lcFilter === 'deprecated') {
128
+ conditions.push(`lifecycle IN ('deprecated', 'rejected')`);
129
+ } else {
130
+ conditions.push(`lifecycle = ?`);
131
+ params.push(lcFilter);
132
+ }
133
+ }
134
+
135
+ for (const [key, value] of Object.entries(normalFilters)) {
136
+ if (value == null) continue;
137
+ this._assertSafeColumn(key);
138
+ conditions.push(`${key} = ?`);
139
+ params.push(value);
140
+ }
141
+
142
+ if (_tagLike) {
143
+ conditions.push(`tags LIKE ?`);
144
+ const escaped = _tagLike.replace(/[%_\\]/g, ch => `\\${ch}`);
145
+ params.push(`%"${escaped}"%`);
146
+ }
147
+
148
+ if (_search) {
149
+ const escaped = _search.replace(/[%_\\]/g, ch => `\\${ch}`);
150
+ const like = `%${escaped}%`;
151
+ conditions.push(`(title LIKE ? ESCAPE '\\' OR description LIKE ? ESCAPE '\\' OR trigger_key LIKE ? ESCAPE '\\' OR content LIKE ? ESCAPE '\\' OR tags LIKE ? ESCAPE '\\')`);
152
+ params.push(like, like, like, like, like);
153
+ }
154
+
155
+ const where = conditions.length > 0 ? ` WHERE ${conditions.join(' AND ')}` : '';
156
+
157
+ this._assertSafeColumn(orderBy);
158
+ const orderClause = ` ORDER BY ${orderBy} ${order === 'ASC' ? 'ASC' : 'DESC'}`;
159
+
160
+ const total = this.db.prepare(`SELECT COUNT(*) as count FROM knowledge_entries${where}`).get(...params).count;
161
+ const data = this.db.prepare(`SELECT * FROM knowledge_entries${where}${orderClause} LIMIT ? OFFSET ?`)
162
+ .all(...params, pageSize, offset);
163
+
164
+ return {
165
+ data: data.map(row => this._rowToEntity(row)),
166
+ pagination: { page, pageSize, total, pages: Math.ceil(total / pageSize) },
167
+ };
168
+ }
169
+
170
+ /**
171
+ * 根据生命周期状态查询
172
+ */
173
+ async findByLifecycle(lifecycle, pagination = {}) {
174
+ return this.findWithPagination({ lifecycle }, pagination);
175
+ }
176
+
177
+ /**
178
+ * 根据 kind 查询
179
+ */
180
+ async findByKind(kind, options = {}) {
181
+ const { lifecycle, ...pagination } = options;
182
+ const filters = { kind };
183
+ if (lifecycle) filters.lifecycle = lifecycle;
184
+ return this.findWithPagination(filters, pagination);
185
+ }
186
+
187
+ /**
188
+ * 查询所有 active 的 rule 类型(Guard 消费热路径)
189
+ * @returns {Promise<KnowledgeEntry[]>}
190
+ */
191
+ async findActiveRules() {
192
+ try {
193
+ const rows = this.db.prepare(`
194
+ SELECT * FROM knowledge_entries
195
+ WHERE kind = 'rule' AND lifecycle = 'active'
196
+ `).all();
197
+ return rows.map(row => this._rowToEntity(row));
198
+ } catch (error) {
199
+ this.logger.error('Error finding active rules', { error: error.message });
200
+ throw error;
201
+ }
202
+ }
203
+
204
+ /**
205
+ * 根据语言查询
206
+ */
207
+ async findByLanguage(language, pagination = {}) {
208
+ return this.findWithPagination({ language }, pagination);
209
+ }
210
+
211
+ /**
212
+ * 根据分类查询
213
+ */
214
+ async findByCategory(category, pagination = {}) {
215
+ return this.findWithPagination({ category }, pagination);
216
+ }
217
+
218
+ /**
219
+ * 搜索
220
+ */
221
+ async search(keyword, pagination = {}) {
222
+ return this.findWithPagination({ _search: keyword }, pagination);
223
+ }
224
+
225
+ /**
226
+ * 获取统计信息
227
+ */
228
+ async getStats() {
229
+ try {
230
+ return this.db.prepare(`
231
+ SELECT
232
+ COUNT(*) as total,
233
+ SUM(CASE WHEN lifecycle IN ('pending', 'draft', 'approved', 'auto_approved') THEN 1 ELSE 0 END) as pending,
234
+ SUM(CASE WHEN lifecycle = 'active' THEN 1 ELSE 0 END) as active,
235
+ SUM(CASE WHEN lifecycle IN ('deprecated', 'rejected') THEN 1 ELSE 0 END) as deprecated,
236
+ SUM(CASE WHEN kind = 'rule' THEN 1 ELSE 0 END) as rules,
237
+ SUM(CASE WHEN kind = 'pattern' THEN 1 ELSE 0 END) as patterns,
238
+ SUM(CASE WHEN kind = 'fact' THEN 1 ELSE 0 END) as facts
239
+ FROM knowledge_entries
240
+ `).get();
241
+ } catch (error) {
242
+ this.logger.error('Error getting knowledge stats', { error: error.message });
243
+ throw error;
244
+ }
245
+ }
246
+
247
+ /* ═══ 行 ↔ 实体 映射 ═══════════════════════════════ */
248
+
249
+ /**
250
+ * DB Row → KnowledgeEntry
251
+ * @param {Object} row
252
+ * @returns {KnowledgeEntry}
253
+ */
254
+ _rowToEntity(row) {
255
+ if (!row) return null;
256
+
257
+ return new KnowledgeEntry({
258
+ id: row.id,
259
+ title: row.title,
260
+ trigger: row.trigger_key,
261
+ description: row.description,
262
+ lifecycle: row.lifecycle,
263
+ lifecycleHistory: this._parseJson(row.lifecycle_history, []),
264
+ autoApprovable: !!row.probation,
265
+ language: row.language,
266
+ category: row.category,
267
+ kind: row.kind || inferKind(row.knowledge_type),
268
+ knowledgeType: row.knowledge_type,
269
+ complexity: row.complexity,
270
+ scope: row.scope,
271
+ difficulty: row.difficulty,
272
+ tags: this._parseJson(row.tags, []),
273
+ summaryCn: row.summary_cn || '',
274
+ summaryEn: row.summary_en || '',
275
+ usageGuideCn: row.usage_guide_cn || '',
276
+ usageGuideEn: row.usage_guide_en || '',
277
+ content: this._parseJson(row.content, {}),
278
+ relations: this._parseJson(row.relations, {}),
279
+ constraints: this._parseJson(row.constraints, {}),
280
+ reasoning: this._parseJson(row.reasoning, {}),
281
+ quality: this._parseJson(row.quality, {}),
282
+ stats: this._parseJson(row.stats, {}),
283
+ headers: this._parseJson(row.headers, []),
284
+ headerPaths: this._parseJson(row.header_paths, []),
285
+ moduleName: row.module_name || '',
286
+ includeHeaders: !!row.include_headers,
287
+ agentNotes: this._parseJson(row.agent_notes, null),
288
+ aiInsight: row.ai_insight || null,
289
+ reviewedBy: row.reviewed_by || null,
290
+ reviewedAt: row.reviewed_at || null,
291
+ rejectionReason: row.rejection_reason || null,
292
+ source: row.source || 'manual',
293
+ sourceFile: row.source_file || null,
294
+ sourceCandidateId: row.source_candidate_id || null,
295
+ createdBy: row.created_by || 'system',
296
+ createdAt: row.created_at,
297
+ updatedAt: row.updated_at,
298
+ publishedAt: row.published_at || null,
299
+ publishedBy: row.published_by || null,
300
+ });
301
+ }
302
+
303
+ /**
304
+ * KnowledgeEntry → DB Row
305
+ * @param {KnowledgeEntry} e
306
+ * @returns {Object}
307
+ */
308
+ _entityToRow(e) {
309
+ const now = Math.floor(Date.now() / 1000);
310
+ return {
311
+ id: e.id,
312
+ title: e.title,
313
+ trigger_key: e.trigger || '',
314
+ description: e.description || '',
315
+ lifecycle: e.lifecycle,
316
+ lifecycle_history: JSON.stringify(e.lifecycleHistory || []),
317
+ probation: e.autoApprovable ? 1 : 0,
318
+ language: e.language,
319
+ category: e.category,
320
+ kind: e.kind || inferKind(e.knowledgeType),
321
+ knowledge_type: e.knowledgeType || 'code-pattern',
322
+ complexity: e.complexity || 'intermediate',
323
+ scope: e.scope || null,
324
+ difficulty: e.difficulty || null,
325
+ tags: JSON.stringify(e.tags || []),
326
+ summary_cn: e.summaryCn || null,
327
+ summary_en: e.summaryEn || null,
328
+ usage_guide_cn: e.usageGuideCn || null,
329
+ usage_guide_en: e.usageGuideEn || null,
330
+ content: JSON.stringify(e.content.toJSON()),
331
+ relations: JSON.stringify(e.relations.toJSON()),
332
+ constraints: JSON.stringify(e.constraints.toJSON()),
333
+ reasoning: JSON.stringify(e.reasoning.toJSON()),
334
+ quality: JSON.stringify(e.quality.toJSON()),
335
+ stats: JSON.stringify(e.stats.toJSON()),
336
+ headers: JSON.stringify(e.headers || []),
337
+ header_paths: JSON.stringify(e.headerPaths || []),
338
+ module_name: e.moduleName || null,
339
+ include_headers: e.includeHeaders ? 1 : 0,
340
+ agent_notes: e.agentNotes ? JSON.stringify(e.agentNotes) : null,
341
+ ai_insight: e.aiInsight || null,
342
+ reviewed_by: e.reviewedBy || null,
343
+ reviewed_at: e.reviewedAt || null,
344
+ rejection_reason: e.rejectionReason || null,
345
+ source: e.source || 'manual',
346
+ source_file: e.sourceFile || null,
347
+ source_candidate_id: e.sourceCandidateId || null,
348
+ created_by: e.createdBy || 'system',
349
+ created_at: e.createdAt || now,
350
+ updated_at: e.updatedAt || now,
351
+ published_at: e.publishedAt || null,
352
+ published_by: e.publishedBy || null,
353
+ content_hash: null,
354
+ };
355
+ }
356
+
357
+ /**
358
+ * 覆写 BaseRepository 的 _mapRowToEntity
359
+ * @override
360
+ */
361
+ _mapRowToEntity(row) {
362
+ return this._rowToEntity(row);
363
+ }
364
+
365
+ /** @private 安全解析 JSON */
366
+ _parseJson(value, fallback) {
367
+ if (!value || value === 'null') return fallback;
368
+ if (typeof value === 'object') return value;
369
+ try { return JSON.parse(value); } catch { return fallback; }
370
+ }
371
+ }
372
+
373
+ export default KnowledgeRepositoryImpl;
@@ -0,0 +1,162 @@
1
+ /**
2
+ * TokenUsageStore — Token 消耗持久化存储
3
+ * 写入 AI 调用的 token 用量记录到 SQLite token_usage 表。
4
+ * 提供近 7 日按日/按来源的聚合查询。
5
+ */
6
+
7
+ import Logger from '../../infrastructure/logging/Logger.js';
8
+
9
+ const MAX_ROWS = 10000; // 自动清理: 保留最近 10000 条
10
+
11
+ export class TokenUsageStore {
12
+ #db;
13
+ #logger;
14
+ #insertStmt;
15
+ #pruneStmt;
16
+ #dailyStmt;
17
+ #bySourceStmt;
18
+ #summaryStmt;
19
+ /** @type {{ data: object, expireAt: number } | null} */
20
+ #reportCache = null;
21
+
22
+ /**
23
+ * @param {import('better-sqlite3').Database} db
24
+ */
25
+ constructor(db) {
26
+ this.#db = db;
27
+ this.#logger = Logger.getInstance();
28
+
29
+ // 预编译常用语句
30
+ this.#insertStmt = this.#db.prepare(`
31
+ INSERT INTO token_usage (timestamp, source, dimension, provider, model, input_tokens, output_tokens, total_tokens, duration_ms, tool_calls, session_id)
32
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
33
+ `);
34
+ this.#pruneStmt = this.#db.prepare(`
35
+ DELETE FROM token_usage WHERE id NOT IN (
36
+ SELECT id FROM token_usage ORDER BY timestamp DESC LIMIT ?
37
+ )
38
+ `);
39
+ this.#dailyStmt = this.#db.prepare(`
40
+ SELECT
41
+ DATE(timestamp / 1000, 'unixepoch', 'localtime') AS date,
42
+ SUM(input_tokens) AS input_tokens,
43
+ SUM(output_tokens) AS output_tokens,
44
+ SUM(total_tokens) AS total_tokens,
45
+ COUNT(*) AS call_count
46
+ FROM token_usage
47
+ WHERE timestamp >= ?
48
+ GROUP BY date
49
+ ORDER BY date ASC
50
+ `);
51
+ this.#bySourceStmt = this.#db.prepare(`
52
+ SELECT
53
+ source,
54
+ SUM(input_tokens) AS input_tokens,
55
+ SUM(output_tokens) AS output_tokens,
56
+ SUM(total_tokens) AS total_tokens,
57
+ COUNT(*) AS call_count
58
+ FROM token_usage
59
+ WHERE timestamp >= ?
60
+ GROUP BY source
61
+ ORDER BY total_tokens DESC
62
+ `);
63
+ this.#summaryStmt = this.#db.prepare(`
64
+ SELECT
65
+ COALESCE(SUM(input_tokens), 0) AS input_tokens,
66
+ COALESCE(SUM(output_tokens), 0) AS output_tokens,
67
+ COALESCE(SUM(total_tokens), 0) AS total_tokens,
68
+ COUNT(*) AS call_count
69
+ FROM token_usage
70
+ WHERE timestamp >= ?
71
+ `);
72
+ }
73
+
74
+ // ─── 写入 ─────────────────────────────────────────
75
+
76
+ /**
77
+ * 记录一次 AI 调用的 token 消耗
78
+ * @param {{ source: string, dimension?: string, provider?: string, model?: string, inputTokens: number, outputTokens: number, durationMs?: number, toolCalls?: number, sessionId?: string }} record
79
+ */
80
+ record(record) {
81
+ try {
82
+ const now = Date.now();
83
+ const total = (record.inputTokens || 0) + (record.outputTokens || 0);
84
+ if (total === 0) return; // 跳过无消耗的调用
85
+
86
+ this.#insertStmt.run(
87
+ now,
88
+ record.source || 'unknown',
89
+ record.dimension || null,
90
+ record.provider || null,
91
+ record.model || null,
92
+ record.inputTokens || 0,
93
+ record.outputTokens || 0,
94
+ total,
95
+ record.durationMs || null,
96
+ record.toolCalls || 0,
97
+ record.sessionId || null,
98
+ );
99
+
100
+ // 写入后使缓存失效
101
+ this.#reportCache = null;
102
+
103
+ // 定期清理(每 100 次写入检查一次)
104
+ if (Math.random() < 0.01) {
105
+ this.#pruneStmt.run(MAX_ROWS);
106
+ }
107
+ } catch (err) {
108
+ this.#logger.debug('[TokenUsageStore] record failed', { error: err.message });
109
+ }
110
+ }
111
+
112
+ // ─── 查询 ─────────────────────────────────────────
113
+
114
+ /**
115
+ * 近 7 日按日聚合统计
116
+ * @returns {Array<{ date: string, input_tokens: number, output_tokens: number, total_tokens: number, call_count: number }>}
117
+ */
118
+ getLast7DaysDaily() {
119
+ const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
120
+ return this.#dailyStmt.all(sevenDaysAgo);
121
+ }
122
+
123
+ /**
124
+ * 近 7 日按来源 (source) 聚合统计
125
+ * @returns {Array<{ source: string, input_tokens: number, output_tokens: number, total_tokens: number, call_count: number }>}
126
+ */
127
+ getLast7DaysBySource() {
128
+ const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
129
+ return this.#bySourceStmt.all(sevenDaysAgo);
130
+ }
131
+
132
+ /**
133
+ * 近 7 日总计
134
+ * @returns {{ input_tokens: number, output_tokens: number, total_tokens: number, call_count: number, avg_per_call: number }}
135
+ */
136
+ getLast7DaysSummary() {
137
+ const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
138
+ const row = this.#summaryStmt.get(sevenDaysAgo);
139
+ return {
140
+ ...row,
141
+ avg_per_call: row.call_count > 0 ? Math.round(row.total_tokens / row.call_count) : 0,
142
+ };
143
+ }
144
+
145
+ /**
146
+ * 获取完整的 7 日报告(前端一次拉取)
147
+ * 带 10s 内存缓存,避免高频请求重复查询
148
+ */
149
+ getLast7DaysReport() {
150
+ const now = Date.now();
151
+ if (this.#reportCache && now < this.#reportCache.expireAt) {
152
+ return this.#reportCache.data;
153
+ }
154
+ const data = {
155
+ daily: this.getLast7DaysDaily(),
156
+ bySource: this.getLast7DaysBySource(),
157
+ summary: this.getLast7DaysSummary(),
158
+ };
159
+ this.#reportCache = { data, expireAt: now + 10_000 }; // 10s 缓存
160
+ return data;
161
+ }
162
+ }
@@ -123,9 +123,8 @@ function _isHeaderDirective(line) {
123
123
  }
124
124
 
125
125
  function _isGuardDirective(line) {
126
- return (
127
- line.startsWith(MARKS.AUDIT_SHORT) || line.startsWith(MARKS.AUDIT_ALIAS)
128
- );
126
+ // 精确匹配: // as:audit 或 // as:a,后面只能是空格或行尾
127
+ return /^\/\/\s*as:(?:audit|a)(?:\s|$)/.test(line);
129
128
  }
130
129
 
131
130
  function _isSearchDirective(line) {
@@ -187,7 +187,7 @@ export class FileWatcher {
187
187
 
188
188
  // // as:a — Guard 检查
189
189
  if (triggers.guardLine) {
190
- await handleGuard(fullPath, data, triggers.guardLine);
190
+ await handleGuard(this, fullPath, data, triggers.guardLine);
191
191
  }
192
192
 
193
193
  // // as:s — 搜索
@@ -230,46 +230,85 @@ export class FileWatcher {
230
230
  * 追加候选项(通过 ServiceContainer 或 HTTP API)
231
231
  */
232
232
  async _appendCandidates(items, source) {
233
+ // 过滤空 title / 空 code 的无效条目
234
+ const validItems = items.filter(item => {
235
+ const title = (item.title || '').trim();
236
+ const code = (item.code || '').trim();
237
+ if (!title || !code) {
238
+ console.warn(`[Watcher] 跳过无效候选: title=${JSON.stringify(title)}, code length=${code.length}`);
239
+ return false;
240
+ }
241
+ return true;
242
+ });
243
+ if (validItems.length === 0) {
244
+ throw new Error('所有候选条目缺少 title 或 code,无法提交');
245
+ }
246
+
233
247
  // 优先 ServiceContainer
248
+ let serviceError = null;
234
249
  try {
235
250
  const { ServiceContainer } = await import('../../injection/ServiceContainer.js');
236
251
  const container = ServiceContainer.getInstance();
237
- const candidateService = container.get('candidateService');
238
- for (const item of items) {
239
- await candidateService.createCandidate(
240
- {
241
- code: item.code || '',
242
- language: item.language || 'objc',
243
- category: item.category || 'Utility',
244
- source: source || 'watch',
245
- metadata: {
246
- title: item.title,
247
- summary: item.summary,
248
- trigger: item.trigger,
249
- usageGuide: item.usageGuide,
250
- headers: item.headers,
251
- },
252
+ const knowledgeService = container.get('knowledgeService');
253
+ const context = { userId: 'filewatcher' };
254
+ for (const item of validItems) {
255
+ await knowledgeService.create({
256
+ content: {
257
+ pattern: item.code || '',
252
258
  },
253
- { userId: 'file-watcher' }
254
- );
259
+ language: item.language || 'objc',
260
+ category: item.category || 'Utility',
261
+ source: source || 'watch',
262
+ title: item.title,
263
+ summary_cn: item.summary || item.summary_cn || '',
264
+ summary_en: item.summary_en || '',
265
+ usage_guide_cn: item.usageGuide || item.usageGuide_cn || '',
266
+ usage_guide_en: item.usageGuide_en || '',
267
+ module_name: item.moduleName || 'watch-create',
268
+ trigger: item.trigger || '',
269
+ headers: item.headers || [],
270
+ tags: item.tags || [],
271
+ metadata: {
272
+ trigger: item.trigger,
273
+ usageGuide: item.usageGuide,
274
+ headers: item.headers,
275
+ },
276
+ }, context);
255
277
  }
256
278
  return;
257
- } catch {
258
- // ServiceContainer 未初始化
279
+ } catch (err) {
280
+ serviceError = err;
281
+ console.warn('[Watcher] KnowledgeService 创建失败,尝试 HTTP 回退:', err.message);
259
282
  }
260
283
 
261
- // 回退:HTTP API
284
+ // 回退:HTTP API(使用 knowledge 端点而非 candidates)
262
285
  const dashboardUrl = process.env.ASD_DASHBOARD_URL || 'http://localhost:3000';
263
286
  try {
264
- const resp = await fetch(`${dashboardUrl}/api/v1/candidates`, {
265
- method: 'POST',
266
- headers: { 'Content-Type': 'application/json' },
267
- body: JSON.stringify({ items, source }),
268
- });
269
- if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
287
+ for (const item of validItems) {
288
+ const resp = await fetch(`${dashboardUrl}/api/v1/knowledge`, {
289
+ method: 'POST',
290
+ headers: { 'Content-Type': 'application/json' },
291
+ body: JSON.stringify({
292
+ title: item.title,
293
+ content: { pattern: item.code || '' },
294
+ language: item.language || 'objc',
295
+ category: item.category || 'Utility',
296
+ source: source || 'watch',
297
+ summary_cn: item.summary || item.summary_cn || '',
298
+ module_name: item.moduleName || 'watch-create',
299
+ trigger: item.trigger || '',
300
+ headers: item.headers || [],
301
+ }),
302
+ });
303
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
304
+ }
305
+ return;
270
306
  } catch (err) {
271
- console.warn(`[Watcher] 候选提交失败: ${err.message}`);
307
+ console.warn(`[Watcher] HTTP 候选提交也失败: ${err.message}`);
272
308
  }
309
+
310
+ // 两条路径都失败 → 抛出原始错误
311
+ throw serviceError || new Error('候选提交失败:ServiceContainer 和 HTTP 均不可用');
273
312
  }
274
313
 
275
314
  /**