autosnippet 2.8.3 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/bin/cli.js +5 -33
- package/config/constitution.yaml +9 -2
- package/dashboard/dist/assets/{icons-B_Xg4B-s.js → icons-BkT3XrKf.js} +105 -100
- package/dashboard/dist/assets/index-BsB7DzW4.css +1 -0
- package/dashboard/dist/assets/index-DdmQMrJJ.js +155 -0
- package/dashboard/dist/index.html +3 -3
- package/lib/cli/AiScanService.js +13 -11
- package/lib/cli/KnowledgeSyncService.js +343 -0
- package/lib/cli/SetupService.js +9 -27
- package/lib/core/ast/ProjectGraph.js +160 -0
- package/lib/core/gateway/GatewayActionRegistry.js +48 -58
- package/lib/domain/index.js +16 -11
- package/lib/domain/knowledge/KnowledgeEntry.js +351 -0
- package/lib/domain/knowledge/KnowledgeRepository.js +123 -0
- package/lib/domain/knowledge/Lifecycle.js +109 -0
- package/lib/domain/knowledge/index.js +27 -0
- package/lib/domain/knowledge/values/Constraints.js +125 -0
- package/lib/domain/knowledge/values/Content.js +86 -0
- package/lib/domain/knowledge/values/Quality.js +93 -0
- package/lib/domain/knowledge/values/Reasoning.js +69 -0
- package/lib/domain/knowledge/values/Relations.js +168 -0
- package/lib/domain/knowledge/values/Stats.js +87 -0
- package/lib/domain/knowledge/values/index.js +9 -0
- package/lib/external/ai/AiProvider.js +48 -0
- package/lib/external/ai/providers/GoogleGeminiProvider.js +12 -3
- package/lib/external/mcp/McpServer.js +7 -5
- package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +3 -2
- package/lib/external/mcp/handlers/bootstrap.js +121 -12
- package/lib/external/mcp/handlers/browse.js +77 -73
- package/lib/external/mcp/handlers/candidate.js +29 -276
- package/lib/external/mcp/handlers/guard.js +2 -0
- package/lib/external/mcp/handlers/knowledge.js +205 -0
- package/lib/external/mcp/handlers/skill.js +4 -2
- package/lib/external/mcp/handlers/structure.js +25 -23
- package/lib/external/mcp/handlers/system.js +10 -12
- package/lib/external/mcp/tools.js +125 -138
- package/lib/http/HttpServer.js +4 -8
- package/lib/http/middleware/requestLogger.js +3 -3
- package/lib/http/routes/ai.js +17 -1
- package/lib/http/routes/extract.js +48 -4
- package/lib/http/routes/knowledge.js +246 -0
- package/lib/http/routes/search.js +12 -17
- package/lib/http/routes/skills.js +44 -1
- package/lib/infrastructure/cache/GraphCache.js +143 -0
- package/lib/infrastructure/database/migrations/015_create_token_usage.js +27 -0
- package/lib/infrastructure/database/migrations/016_unified_knowledge_entries.js +395 -0
- package/lib/infrastructure/external/XcodeAutomation.js +187 -103
- package/lib/infrastructure/realtime/RealtimeService.js +14 -2
- package/lib/injection/ServiceContainer.js +164 -63
- package/lib/repository/knowledge/KnowledgeRepository.impl.js +373 -0
- package/lib/repository/token/TokenUsageStore.js +162 -0
- package/lib/service/automation/DirectiveDetector.js +2 -3
- package/lib/service/automation/FileWatcher.js +67 -28
- package/lib/service/automation/XcodeIntegration.js +931 -156
- package/lib/service/automation/handlers/AlinkHandler.js +6 -4
- package/lib/service/automation/handlers/CreateHandler.js +53 -18
- package/lib/service/automation/handlers/GuardHandler.js +183 -20
- package/lib/service/automation/handlers/SearchHandler.js +35 -17
- package/lib/service/chat/AnalystAgent.js +25 -14
- package/lib/service/chat/CandidateGuardrail.js +1 -1
- package/lib/service/chat/ChatAgent.js +280 -48
- package/lib/service/chat/ContextWindow.js +92 -8
- package/lib/service/chat/HandoffProtocol.js +26 -1
- package/lib/service/chat/ProducerAgent.js +11 -9
- package/lib/service/chat/tools.js +298 -194
- package/lib/service/guard/GuardCheckEngine.js +114 -10
- package/lib/service/guard/GuardService.js +59 -48
- package/lib/service/knowledge/ConfidenceRouter.js +159 -0
- package/lib/service/knowledge/KnowledgeFileWriter.js +602 -0
- package/lib/service/knowledge/KnowledgeService.js +725 -0
- package/lib/service/search/SearchEngine.js +92 -19
- package/lib/service/skills/SignalCollector.js +15 -9
- package/lib/service/skills/SkillAdvisor.js +13 -11
- package/lib/service/snippet/SnippetFactory.js +5 -5
- package/lib/service/spm/SpmService.js +119 -18
- package/package.json +1 -1
- package/scripts/install-cursor-skill.js +0 -6
- package/scripts/migrate-md-to-knowledge.mjs +364 -0
- package/skills/autosnippet-analysis/SKILL.md +15 -7
- package/skills/autosnippet-candidates/SKILL.md +6 -6
- package/skills/autosnippet-coldstart/SKILL.md +7 -3
- package/skills/autosnippet-concepts/SKILL.md +7 -6
- package/skills/autosnippet-create/SKILL.md +13 -13
- package/skills/autosnippet-intent/SKILL.md +3 -2
- package/skills/autosnippet-lifecycle/SKILL.md +5 -5
- package/skills/autosnippet-recipes/SKILL.md +16 -4
- package/templates/constitution.yaml +1 -1
- package/templates/copilot-instructions.md +6 -6
- package/templates/recipes-setup/README.md +3 -3
- package/dashboard/dist/assets/index-CkIih2CC.css +0 -1
- package/dashboard/dist/assets/index-Duc8Qk-c.js +0 -197
- package/lib/cli/CandidateSyncService.js +0 -261
- package/lib/cli/SyncService.js +0 -356
- package/lib/domain/candidate/Candidate.js +0 -196
- package/lib/domain/candidate/CandidateRepository.js +0 -107
- package/lib/domain/candidate/Reasoning.js +0 -52
- package/lib/domain/recipe/Recipe.js +0 -421
- package/lib/domain/recipe/RecipeRepository.js +0 -54
- package/lib/domain/types/CandidateStatus.js +0 -52
- package/lib/http/routes/candidates.js +0 -559
- package/lib/http/routes/recipes.js +0 -397
- package/lib/repository/candidate/CandidateRepository.impl.js +0 -230
- package/lib/repository/recipe/RecipeRepository.impl.js +0 -498
- package/lib/service/candidate/CandidateAggregator.js +0 -52
- package/lib/service/candidate/CandidateFileWriter.js +0 -383
- package/lib/service/candidate/CandidateService.js +0 -973
- package/lib/service/recipe/RecipeFileWriter.js +0 -514
- package/lib/service/recipe/RecipeService.js +0 -786
- package/lib/service/recipe/RecipeStatsTracker.js +0 -148
|
@@ -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
|
-
|
|
127
|
-
|
|
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
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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]
|
|
307
|
+
console.warn(`[Watcher] HTTP 候选提交也失败: ${err.message}`);
|
|
272
308
|
}
|
|
309
|
+
|
|
310
|
+
// 两条路径都失败 → 抛出原始错误
|
|
311
|
+
throw serviceError || new Error('候选提交失败:ServiceContainer 和 HTTP 均不可用');
|
|
273
312
|
}
|
|
274
313
|
|
|
275
314
|
/**
|