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,725 @@
|
|
|
1
|
+
import { KnowledgeEntry } from '../../domain/knowledge/KnowledgeEntry.js';
|
|
2
|
+
import { Lifecycle, inferKind } from '../../domain/knowledge/Lifecycle.js';
|
|
3
|
+
import Logger from '../../infrastructure/logging/Logger.js';
|
|
4
|
+
import { ValidationError, ConflictError, NotFoundError } from '../../shared/errors/index.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* KnowledgeService — 统一知识服务
|
|
8
|
+
*
|
|
9
|
+
* 替代 CandidateService + RecipeService。
|
|
10
|
+
* 全链路使用 KnowledgeEntry 实体 + wire format,
|
|
11
|
+
* 无需 promote、无需 metadata 袋子、无需打平映射。
|
|
12
|
+
*
|
|
13
|
+
* 生命周期操作委托给 KnowledgeEntry 实体方法,
|
|
14
|
+
* Service 负责编排 Repository / FileWriter / AuditLog / Graph / SkillHooks。
|
|
15
|
+
*/
|
|
16
|
+
export class KnowledgeService {
|
|
17
|
+
/**
|
|
18
|
+
* @param {import('../../domain/knowledge/KnowledgeRepository.js').KnowledgeRepository} repository
|
|
19
|
+
* @param {object} auditLogger
|
|
20
|
+
* @param {object} gateway
|
|
21
|
+
* @param {object} knowledgeGraphService
|
|
22
|
+
* @param {object} [options]
|
|
23
|
+
* @param {import('./KnowledgeFileWriter.js').KnowledgeFileWriter} [options.fileWriter]
|
|
24
|
+
* @param {import('../skills/SkillHooks.js').SkillHooks} [options.skillHooks]
|
|
25
|
+
* @param {import('./ConfidenceRouter.js').ConfidenceRouter} [options.confidenceRouter]
|
|
26
|
+
* @param {import('../quality/QualityScorer.js').QualityScorer} [options.qualityScorer]
|
|
27
|
+
*/
|
|
28
|
+
constructor(repository, auditLogger, gateway, knowledgeGraphService, options = {}) {
|
|
29
|
+
this.repository = repository;
|
|
30
|
+
this.auditLogger = auditLogger;
|
|
31
|
+
this.gateway = gateway;
|
|
32
|
+
this._knowledgeGraphService = knowledgeGraphService || null;
|
|
33
|
+
this._fileWriter = options.fileWriter || null;
|
|
34
|
+
this._skillHooks = options.skillHooks || null;
|
|
35
|
+
this._confidenceRouter = options.confidenceRouter || null;
|
|
36
|
+
this._qualityScorer = options.qualityScorer || null;
|
|
37
|
+
this.logger = Logger.getInstance();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* ═══ CRUD ══════════════════════════════════════════════ */
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 创建知识条目
|
|
44
|
+
*
|
|
45
|
+
* MCP 参数 = wire format → KnowledgeEntry.fromJSON() 直接构造。
|
|
46
|
+
* 所有新条目初始状态为 pending(待审核)。
|
|
47
|
+
* ConfidenceRouter 仅标记 auto_approvable 标志,不改变 lifecycle。
|
|
48
|
+
*
|
|
49
|
+
* @param {Object} data - wire format 数据
|
|
50
|
+
* @param {Object} context - { userId }
|
|
51
|
+
* @returns {Promise<KnowledgeEntry>}
|
|
52
|
+
*/
|
|
53
|
+
async create(data, context) {
|
|
54
|
+
try {
|
|
55
|
+
this._validateCreateInput(data);
|
|
56
|
+
|
|
57
|
+
const entry = KnowledgeEntry.fromJSON({
|
|
58
|
+
...data,
|
|
59
|
+
lifecycle: Lifecycle.PENDING,
|
|
60
|
+
source: data.source || 'manual',
|
|
61
|
+
created_by: context.userId,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (!entry.isValid()) {
|
|
65
|
+
throw new ValidationError('title + content required');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── SkillHooks: onKnowledgeSubmit ──
|
|
69
|
+
if (this._skillHooks) {
|
|
70
|
+
const hookResult = await this._skillHooks.run('onKnowledgeSubmit', entry, {
|
|
71
|
+
userId: context.userId,
|
|
72
|
+
});
|
|
73
|
+
if (hookResult?.block) {
|
|
74
|
+
throw new ValidationError(`SkillHook blocked: ${hookResult.reason || 'unknown'}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── ConfidenceRouter — 仅标记 auto_approvable,不改变 lifecycle ──
|
|
79
|
+
if (this._confidenceRouter) {
|
|
80
|
+
const route = await this._confidenceRouter.route(entry);
|
|
81
|
+
if (route.action === 'auto_approve') {
|
|
82
|
+
entry.autoApprovable = true;
|
|
83
|
+
}
|
|
84
|
+
// reject / pending 都保持 pending 状态,等待人工审核
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const saved = await this.repository.create(entry);
|
|
88
|
+
|
|
89
|
+
// 同步 relations → knowledge_edges
|
|
90
|
+
this._syncRelationsToGraph(saved.id, saved.relations);
|
|
91
|
+
|
|
92
|
+
// 落盘 .md 文件
|
|
93
|
+
this._persistToFile(saved);
|
|
94
|
+
|
|
95
|
+
// 审计日志
|
|
96
|
+
await this._audit('create_knowledge', saved.id, context.userId, {
|
|
97
|
+
title: saved.title,
|
|
98
|
+
lifecycle: saved.lifecycle,
|
|
99
|
+
kind: saved.kind,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
this.logger.info('Knowledge entry created', {
|
|
103
|
+
id: saved.id,
|
|
104
|
+
lifecycle: saved.lifecycle,
|
|
105
|
+
kind: saved.kind,
|
|
106
|
+
createdBy: context.userId,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// ── SkillHooks: onKnowledgeCreated (fire-and-forget) ──
|
|
110
|
+
if (this._skillHooks) {
|
|
111
|
+
this._skillHooks.run('onKnowledgeCreated', saved, {
|
|
112
|
+
userId: context.userId,
|
|
113
|
+
}).catch(err => this.logger.warn('SkillHook onKnowledgeCreated error', { error: err.message }));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return saved;
|
|
117
|
+
} catch (error) {
|
|
118
|
+
this.logger.error('Error creating knowledge entry', {
|
|
119
|
+
error: error.message,
|
|
120
|
+
data,
|
|
121
|
+
});
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 获取单个知识条目
|
|
128
|
+
* @param {string} id
|
|
129
|
+
* @returns {Promise<KnowledgeEntry>}
|
|
130
|
+
*/
|
|
131
|
+
async get(id) {
|
|
132
|
+
const entry = await this.repository.findById(id);
|
|
133
|
+
if (!entry) {
|
|
134
|
+
throw new NotFoundError('Knowledge entry not found', 'knowledge', id);
|
|
135
|
+
}
|
|
136
|
+
return entry;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 更新知识条目(仅允许白名单字段)
|
|
141
|
+
* @param {string} id
|
|
142
|
+
* @param {Object} data - wire format 部分字段
|
|
143
|
+
* @param {Object} context - { userId }
|
|
144
|
+
* @returns {Promise<KnowledgeEntry>}
|
|
145
|
+
*/
|
|
146
|
+
async update(id, data, context) {
|
|
147
|
+
try {
|
|
148
|
+
const entry = await this._findOrThrow(id);
|
|
149
|
+
|
|
150
|
+
const UPDATABLE = [
|
|
151
|
+
'title', 'description', 'trigger', 'language', 'category',
|
|
152
|
+
'knowledge_type', 'complexity', 'scope', 'difficulty',
|
|
153
|
+
'summary_cn', 'summary_en', 'usage_guide_cn', 'usage_guide_en',
|
|
154
|
+
'content', 'relations', 'constraints', 'reasoning',
|
|
155
|
+
'tags', 'headers', 'header_paths', 'module_name', 'include_headers',
|
|
156
|
+
'agent_notes', 'ai_insight',
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
const dbUpdates = {};
|
|
160
|
+
|
|
161
|
+
for (const key of UPDATABLE) {
|
|
162
|
+
if (data[key] === undefined) continue;
|
|
163
|
+
|
|
164
|
+
switch (key) {
|
|
165
|
+
// 标量字段直接映射
|
|
166
|
+
case 'title':
|
|
167
|
+
case 'description':
|
|
168
|
+
case 'trigger':
|
|
169
|
+
case 'language':
|
|
170
|
+
case 'category':
|
|
171
|
+
case 'complexity':
|
|
172
|
+
case 'scope':
|
|
173
|
+
case 'difficulty':
|
|
174
|
+
case 'agent_notes':
|
|
175
|
+
case 'ai_insight':
|
|
176
|
+
dbUpdates[key === 'trigger' ? 'trigger_key' : key] = data[key];
|
|
177
|
+
break;
|
|
178
|
+
|
|
179
|
+
case 'summary_cn':
|
|
180
|
+
case 'summary_en':
|
|
181
|
+
case 'usage_guide_cn':
|
|
182
|
+
case 'usage_guide_en':
|
|
183
|
+
dbUpdates[key] = data[key];
|
|
184
|
+
break;
|
|
185
|
+
|
|
186
|
+
case 'knowledge_type':
|
|
187
|
+
dbUpdates.knowledge_type = data.knowledge_type;
|
|
188
|
+
// 联动更新 kind
|
|
189
|
+
dbUpdates.kind = inferKind(data.knowledge_type);
|
|
190
|
+
break;
|
|
191
|
+
|
|
192
|
+
// 值对象字段 → JSON 列(V3 列名无 _json 后缀)
|
|
193
|
+
case 'content':
|
|
194
|
+
dbUpdates.content = JSON.stringify(data.content);
|
|
195
|
+
break;
|
|
196
|
+
case 'relations':
|
|
197
|
+
dbUpdates.relations = JSON.stringify(data.relations);
|
|
198
|
+
break;
|
|
199
|
+
case 'constraints':
|
|
200
|
+
dbUpdates.constraints = JSON.stringify(data.constraints);
|
|
201
|
+
break;
|
|
202
|
+
case 'reasoning':
|
|
203
|
+
dbUpdates.reasoning = JSON.stringify(data.reasoning);
|
|
204
|
+
break;
|
|
205
|
+
|
|
206
|
+
// 数组 → JSON
|
|
207
|
+
case 'tags':
|
|
208
|
+
dbUpdates.tags = JSON.stringify(data.tags);
|
|
209
|
+
break;
|
|
210
|
+
case 'headers':
|
|
211
|
+
dbUpdates.headers = JSON.stringify(data.headers);
|
|
212
|
+
break;
|
|
213
|
+
case 'header_paths':
|
|
214
|
+
dbUpdates.header_paths = JSON.stringify(data.header_paths);
|
|
215
|
+
break;
|
|
216
|
+
|
|
217
|
+
// 布尔/标量
|
|
218
|
+
case 'module_name':
|
|
219
|
+
case 'include_headers':
|
|
220
|
+
dbUpdates[key] = data[key];
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (Object.keys(dbUpdates).length === 0) {
|
|
226
|
+
throw new ValidationError('No updatable fields provided');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
dbUpdates.updated_at = Math.floor(Date.now() / 1000);
|
|
230
|
+
|
|
231
|
+
const updated = await this.repository.update(id, dbUpdates);
|
|
232
|
+
|
|
233
|
+
// 若 relations 变更,同步到 knowledge_edges
|
|
234
|
+
if (dbUpdates.relations) {
|
|
235
|
+
this._syncRelationsToGraph(id, data.relations);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// 落盘
|
|
239
|
+
this._persistToFile(updated);
|
|
240
|
+
|
|
241
|
+
await this._audit('update_knowledge', id, context.userId, {
|
|
242
|
+
fields: Object.keys(dbUpdates),
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
this.logger.info('Knowledge entry updated', {
|
|
246
|
+
id, updatedBy: context.userId, fields: Object.keys(dbUpdates),
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
return updated;
|
|
250
|
+
} catch (error) {
|
|
251
|
+
this.logger.error('Error updating knowledge entry', {
|
|
252
|
+
id, error: error.message,
|
|
253
|
+
});
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* 删除知识条目
|
|
260
|
+
* @param {string} id
|
|
261
|
+
* @param {Object} context - { userId }
|
|
262
|
+
* @returns {Promise<{ success: boolean, id: string }>}
|
|
263
|
+
*/
|
|
264
|
+
async delete(id, context) {
|
|
265
|
+
try {
|
|
266
|
+
const entry = await this._findOrThrow(id);
|
|
267
|
+
|
|
268
|
+
// 删除 .md 文件
|
|
269
|
+
this._removeFile(entry);
|
|
270
|
+
|
|
271
|
+
// 清除 knowledge_edges
|
|
272
|
+
this._removeAllEdges(id);
|
|
273
|
+
|
|
274
|
+
await this.repository.delete(id);
|
|
275
|
+
|
|
276
|
+
await this._audit('delete_knowledge', id, context.userId, {
|
|
277
|
+
title: entry.title,
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
this.logger.info('Knowledge entry deleted', {
|
|
281
|
+
id, deletedBy: context.userId, title: entry.title,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
return { success: true, id };
|
|
285
|
+
} catch (error) {
|
|
286
|
+
this.logger.error('Error deleting knowledge entry', {
|
|
287
|
+
id, error: error.message,
|
|
288
|
+
});
|
|
289
|
+
throw error;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/* ═══ 生命周期操作 ══════════════════════════════════════ */
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* 发布 (pending → active) — 仅开发者可执行
|
|
297
|
+
*/
|
|
298
|
+
async publish(id, context) {
|
|
299
|
+
return this._lifecycleTransition(id, 'publish', context, {
|
|
300
|
+
entityArgs: [context.userId],
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* 弃用 (pending|active → deprecated)
|
|
306
|
+
*/
|
|
307
|
+
async deprecate(id, reason, context) {
|
|
308
|
+
if (!reason || reason.trim().length === 0) {
|
|
309
|
+
throw new ValidationError('Deprecation reason is required');
|
|
310
|
+
}
|
|
311
|
+
return this._lifecycleTransition(id, 'deprecate', context, {
|
|
312
|
+
entityArgs: [reason],
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* 重新激活 (deprecated → pending)
|
|
318
|
+
*/
|
|
319
|
+
async reactivate(id, context) {
|
|
320
|
+
return this._lifecycleTransition(id, 'reactivate', context);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ── 向后兼容别名 ──
|
|
324
|
+
|
|
325
|
+
/** @deprecated 简化后所有条目直接进 pending */
|
|
326
|
+
async submit(id, context) { return this.get(id); }
|
|
327
|
+
|
|
328
|
+
/** @deprecated 简化后 approve = publish */
|
|
329
|
+
async approve(id, context) { return this.publish(id, context); }
|
|
330
|
+
|
|
331
|
+
/** @deprecated 简化后无需 autoApprove */
|
|
332
|
+
async autoApprove(id, context) { return this.get(id); }
|
|
333
|
+
|
|
334
|
+
/** @deprecated 简化后 reject = deprecate */
|
|
335
|
+
async reject(id, reason, context) { return this.deprecate(id, reason, context); }
|
|
336
|
+
|
|
337
|
+
/** @deprecated 简化后 toDraft = reactivate */
|
|
338
|
+
async toDraft(id, context) { return this.reactivate(id, context); }
|
|
339
|
+
|
|
340
|
+
/** @deprecated 简化后 fastTrack = publish */
|
|
341
|
+
async fastTrack(id, context) { return this.publish(id, context); }
|
|
342
|
+
|
|
343
|
+
/* ═══ 查询 ══════════════════════════════════════════════ */
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* 查询列表
|
|
347
|
+
* @param {Object} filters - { lifecycle, kind, language, category, knowledge_type, source, tag }
|
|
348
|
+
* @param {Object} pagination - { page, pageSize }
|
|
349
|
+
*/
|
|
350
|
+
async list(filters = {}, pagination = {}) {
|
|
351
|
+
try {
|
|
352
|
+
const { lifecycle, kind, language, category, knowledgeType, source, tag, scope } = filters;
|
|
353
|
+
const { page = 1, pageSize = 20 } = pagination;
|
|
354
|
+
|
|
355
|
+
const dbFilters = {};
|
|
356
|
+
if (lifecycle) dbFilters.lifecycle = lifecycle;
|
|
357
|
+
if (kind) dbFilters.kind = kind;
|
|
358
|
+
if (language) dbFilters.language = language;
|
|
359
|
+
if (category) dbFilters.category = category;
|
|
360
|
+
if (knowledgeType) dbFilters.knowledge_type = knowledgeType;
|
|
361
|
+
if (source) dbFilters.source = source;
|
|
362
|
+
if (scope) dbFilters.scope = scope;
|
|
363
|
+
if (tag) dbFilters._tagLike = tag;
|
|
364
|
+
|
|
365
|
+
return this.repository.findWithPagination(dbFilters, { page, pageSize });
|
|
366
|
+
} catch (error) {
|
|
367
|
+
this.logger.error('Error listing knowledge entries', {
|
|
368
|
+
error: error.message, filters,
|
|
369
|
+
});
|
|
370
|
+
throw error;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* 按 Kind 查询
|
|
376
|
+
*/
|
|
377
|
+
async listByKind(kind, pagination = {}) {
|
|
378
|
+
try {
|
|
379
|
+
const { page = 1, pageSize = 20 } = pagination;
|
|
380
|
+
return this.repository.findByKind(kind, { page, pageSize });
|
|
381
|
+
} catch (error) {
|
|
382
|
+
this.logger.error('Error listing by kind', { kind, error: error.message });
|
|
383
|
+
throw error;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* 搜索
|
|
389
|
+
*/
|
|
390
|
+
async search(keyword, pagination = {}) {
|
|
391
|
+
try {
|
|
392
|
+
const { page = 1, pageSize = 20 } = pagination;
|
|
393
|
+
return this.repository.search(keyword, { page, pageSize });
|
|
394
|
+
} catch (error) {
|
|
395
|
+
this.logger.error('Error searching knowledge', {
|
|
396
|
+
keyword, error: error.message,
|
|
397
|
+
});
|
|
398
|
+
throw error;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* 获取统计信息
|
|
404
|
+
*/
|
|
405
|
+
async getStats() {
|
|
406
|
+
try {
|
|
407
|
+
return this.repository.getStats();
|
|
408
|
+
} catch (error) {
|
|
409
|
+
this.logger.error('Error getting knowledge stats', {
|
|
410
|
+
error: error.message,
|
|
411
|
+
});
|
|
412
|
+
throw error;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/* ═══ 使用/质量 ═════════════════════════════════════════ */
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* 增加使用计数
|
|
420
|
+
* @param {string} id
|
|
421
|
+
* @param {'adoption'|'application'|'guard_hit'|'view'|'success'} type
|
|
422
|
+
* @param {Object} [options] - { actor, feedback }
|
|
423
|
+
*/
|
|
424
|
+
async incrementUsage(id, type = 'adoption', options = {}) {
|
|
425
|
+
try {
|
|
426
|
+
const entry = await this._findOrThrow(id);
|
|
427
|
+
entry.stats.increment(type);
|
|
428
|
+
|
|
429
|
+
const statsJson = entry.stats.toJSON();
|
|
430
|
+
await this.repository.update(id, {
|
|
431
|
+
stats_json: JSON.stringify(statsJson),
|
|
432
|
+
updated_at: Math.floor(Date.now() / 1000),
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
await this._audit(`knowledge_${type}`, id, options.actor || 'system', {
|
|
436
|
+
feedback: options.feedback,
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
this.logger.debug(`Knowledge ${type} incremented`, { id, type });
|
|
440
|
+
|
|
441
|
+
return entry;
|
|
442
|
+
} catch (error) {
|
|
443
|
+
this.logger.error(`Error incrementing knowledge ${type}`, {
|
|
444
|
+
id, error: error.message,
|
|
445
|
+
});
|
|
446
|
+
throw error;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* 更新质量评分
|
|
452
|
+
* @param {string} id
|
|
453
|
+
* @param {Object} [context] - { userId }
|
|
454
|
+
*/
|
|
455
|
+
async updateQuality(id, context = {}) {
|
|
456
|
+
try {
|
|
457
|
+
const entry = await this._findOrThrow(id);
|
|
458
|
+
|
|
459
|
+
if (!this._qualityScorer) {
|
|
460
|
+
throw new ValidationError('QualityScorer not configured');
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// 为 QualityScorer 适配输入字段
|
|
464
|
+
const scorerInput = this._adaptForScorer(entry);
|
|
465
|
+
const result = this._qualityScorer.score(scorerInput);
|
|
466
|
+
|
|
467
|
+
// 更新 Quality 值对象
|
|
468
|
+
await this.repository.update(id, {
|
|
469
|
+
quality_json: JSON.stringify({
|
|
470
|
+
completeness: result.dimensions.completeness,
|
|
471
|
+
project_adaptation: result.dimensions.metadata,
|
|
472
|
+
documentation_clarity: result.dimensions.format,
|
|
473
|
+
overall: result.score,
|
|
474
|
+
grade: result.grade,
|
|
475
|
+
}),
|
|
476
|
+
updated_at: Math.floor(Date.now() / 1000),
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
if (context.userId) {
|
|
480
|
+
await this._audit('update_knowledge_quality', id, context.userId, {
|
|
481
|
+
score: result.score,
|
|
482
|
+
grade: result.grade,
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
this.logger.info('Knowledge quality updated', {
|
|
487
|
+
id, score: result.score, grade: result.grade,
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
return result;
|
|
491
|
+
} catch (error) {
|
|
492
|
+
this.logger.error('Error updating knowledge quality', {
|
|
493
|
+
id, error: error.message,
|
|
494
|
+
});
|
|
495
|
+
throw error;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/* ═══ 私有方法 ══════════════════════════════════════════ */
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* 统一生命周期转换编排
|
|
503
|
+
*/
|
|
504
|
+
async _lifecycleTransition(id, method, context, options = {}) {
|
|
505
|
+
try {
|
|
506
|
+
const entry = await this._findOrThrow(id);
|
|
507
|
+
const prevLifecycle = entry.lifecycle;
|
|
508
|
+
|
|
509
|
+
const entityArgs = options.entityArgs || [];
|
|
510
|
+
const result = entry[method](...entityArgs);
|
|
511
|
+
|
|
512
|
+
if (!result.success) {
|
|
513
|
+
throw new ConflictError(
|
|
514
|
+
result.error,
|
|
515
|
+
`Lifecycle ${method} failed for ${id}`,
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// 构建 DB 更新
|
|
520
|
+
const dbUpdates = {
|
|
521
|
+
lifecycle: entry.lifecycle,
|
|
522
|
+
lifecycle_history_json: JSON.stringify(entry.lifecycleHistory),
|
|
523
|
+
updated_at: entry.updatedAt,
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
// 审核字段
|
|
527
|
+
if (entry.reviewedBy) dbUpdates.reviewed_by = entry.reviewedBy;
|
|
528
|
+
if (entry.reviewedAt) dbUpdates.reviewed_at = entry.reviewedAt;
|
|
529
|
+
if (entry.rejectionReason !== null) dbUpdates.rejection_reason = entry.rejectionReason;
|
|
530
|
+
|
|
531
|
+
// 发布字段
|
|
532
|
+
if (entry.publishedAt) dbUpdates.published_at = entry.publishedAt;
|
|
533
|
+
if (entry.publishedBy) dbUpdates.published_by = entry.publishedBy;
|
|
534
|
+
if (entry.autoApprovable !== undefined) dbUpdates.probation = entry.autoApprovable ? 1 : 0;
|
|
535
|
+
|
|
536
|
+
const updated = await this.repository.update(id, dbUpdates);
|
|
537
|
+
|
|
538
|
+
// 文件位置迁移(candidate ↔ recipe 目录)
|
|
539
|
+
if (this._fileWriter) {
|
|
540
|
+
try {
|
|
541
|
+
this._fileWriter.moveOnLifecycleChange(updated);
|
|
542
|
+
} catch (err) {
|
|
543
|
+
this.logger.warn('moveOnLifecycleChange failed (non-blocking)', {
|
|
544
|
+
id, error: err.message,
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
await this._audit(`${method}_knowledge`, id, context.userId, {
|
|
550
|
+
from: prevLifecycle,
|
|
551
|
+
to: entry.lifecycle,
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
this.logger.info(`Knowledge entry ${method}`, {
|
|
555
|
+
id, from: prevLifecycle, to: entry.lifecycle,
|
|
556
|
+
actor: context.userId,
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
return updated;
|
|
560
|
+
} catch (error) {
|
|
561
|
+
this.logger.error(`Error in lifecycle ${method}`, {
|
|
562
|
+
id, error: error.message,
|
|
563
|
+
});
|
|
564
|
+
throw error;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* 查找或抛出 NotFoundError
|
|
570
|
+
*/
|
|
571
|
+
async _findOrThrow(id) {
|
|
572
|
+
const entry = await this.repository.findById(id);
|
|
573
|
+
if (!entry) {
|
|
574
|
+
throw new NotFoundError('Knowledge entry not found', 'knowledge', id);
|
|
575
|
+
}
|
|
576
|
+
return entry;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* 验证创建输入
|
|
581
|
+
*/
|
|
582
|
+
_validateCreateInput(data) {
|
|
583
|
+
if (!data.title || !data.title.trim()) {
|
|
584
|
+
throw new ValidationError('Title is required');
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// 内容至少需要 content 对象有内容
|
|
588
|
+
const c = data.content || {};
|
|
589
|
+
if (!c.pattern && !c.rationale && !(c.steps?.length > 0) && !c.markdown) {
|
|
590
|
+
throw new ValidationError('Content is required (pattern, rationale, steps, or markdown)');
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* 为 QualityScorer 适配输入
|
|
596
|
+
* QualityScorer 需要: title, trigger, code, language, category, summary, usageGuide, headers, tags
|
|
597
|
+
*/
|
|
598
|
+
_adaptForScorer(entry) {
|
|
599
|
+
return {
|
|
600
|
+
title: entry.title,
|
|
601
|
+
trigger: entry.trigger,
|
|
602
|
+
code: entry.content?.pattern || entry.content?.markdown || '',
|
|
603
|
+
language: entry.language,
|
|
604
|
+
category: entry.category,
|
|
605
|
+
summary: entry.summaryCn || entry.summaryEn || '',
|
|
606
|
+
usageGuide: entry.usageGuideCn || entry.usageGuideEn || '',
|
|
607
|
+
headers: entry.headers || [],
|
|
608
|
+
tags: entry.tags || [],
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/* ═══ Knowledge Graph 同步 ═══════════════════════════ */
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* 将 relations 同步到 knowledge_edges 表
|
|
616
|
+
*/
|
|
617
|
+
_syncRelationsToGraph(id, relations) {
|
|
618
|
+
const gs = this._knowledgeGraphService;
|
|
619
|
+
if (!gs) return;
|
|
620
|
+
|
|
621
|
+
try {
|
|
622
|
+
gs.db.prepare(
|
|
623
|
+
`DELETE FROM knowledge_edges WHERE from_id = ? AND from_type = 'knowledge'`
|
|
624
|
+
).run(id);
|
|
625
|
+
|
|
626
|
+
if (!relations || typeof relations !== 'object') return;
|
|
627
|
+
|
|
628
|
+
// Relations 可能是 Relations 值对象或普通对象
|
|
629
|
+
const relObj = typeof relations.toJSON === 'function' ? relations.toJSON() : relations;
|
|
630
|
+
|
|
631
|
+
for (const [relType, targets] of Object.entries(relObj)) {
|
|
632
|
+
if (!Array.isArray(targets)) continue;
|
|
633
|
+
for (const t of targets) {
|
|
634
|
+
const targetId = t.target || t.id || (typeof t === 'string' ? t : null);
|
|
635
|
+
if (targetId) {
|
|
636
|
+
gs.addEdge(id, 'knowledge', targetId, 'knowledge', relType, {
|
|
637
|
+
weight: t.weight || 1.0,
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
} catch (err) {
|
|
643
|
+
this.logger.warn('Failed to sync relations to knowledge_edges', {
|
|
644
|
+
id, error: err.message,
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* 删除所有关联边
|
|
651
|
+
*/
|
|
652
|
+
_removeAllEdges(id) {
|
|
653
|
+
const gs = this._knowledgeGraphService;
|
|
654
|
+
if (!gs) return;
|
|
655
|
+
|
|
656
|
+
try {
|
|
657
|
+
gs.db.prepare(
|
|
658
|
+
`DELETE FROM knowledge_edges WHERE from_id = ? OR to_id = ?`
|
|
659
|
+
).run(id, id);
|
|
660
|
+
} catch (err) {
|
|
661
|
+
this.logger.warn('Failed to remove edges', { id, error: err.message });
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/* ═══ 文件落盘 ═══════════════════════════════════════ */
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* 落盘到 .md 文件 + 回写 source_file
|
|
669
|
+
*/
|
|
670
|
+
_persistToFile(entry) {
|
|
671
|
+
if (!this._fileWriter) return;
|
|
672
|
+
try {
|
|
673
|
+
const oldSourceFile = entry.sourceFile;
|
|
674
|
+
this._fileWriter.persist(entry);
|
|
675
|
+
if (entry.sourceFile && entry.sourceFile !== oldSourceFile) {
|
|
676
|
+
this.repository.update(entry.id, { source_file: entry.sourceFile }).catch(err => {
|
|
677
|
+
this.logger.warn('Failed to update source_file in DB', {
|
|
678
|
+
id: entry.id, error: err.message,
|
|
679
|
+
});
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
} catch (err) {
|
|
683
|
+
this.logger.warn('Knowledge file persist failed (non-blocking)', {
|
|
684
|
+
id: entry?.id, error: err.message,
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* 删除 .md 文件
|
|
691
|
+
*/
|
|
692
|
+
_removeFile(entry) {
|
|
693
|
+
if (!this._fileWriter) return;
|
|
694
|
+
try {
|
|
695
|
+
this._fileWriter.remove(entry);
|
|
696
|
+
} catch (err) {
|
|
697
|
+
this.logger.warn('Knowledge file remove failed (non-blocking)', {
|
|
698
|
+
id: entry?.id, error: err.message,
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/* ═══ 审计日志 ═══════════════════════════════════════ */
|
|
704
|
+
|
|
705
|
+
async _audit(action, id, actor, details = {}) {
|
|
706
|
+
try {
|
|
707
|
+
await this.auditLogger.log({
|
|
708
|
+
action,
|
|
709
|
+
resourceType: 'knowledge',
|
|
710
|
+
resourceId: id,
|
|
711
|
+
resource: `knowledge:${id}`,
|
|
712
|
+
actor: actor || 'system',
|
|
713
|
+
result: 'success',
|
|
714
|
+
details: typeof details === 'string' ? details : JSON.stringify(details),
|
|
715
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
716
|
+
});
|
|
717
|
+
} catch (err) {
|
|
718
|
+
this.logger.warn('Audit log failed (non-blocking)', {
|
|
719
|
+
action, id, error: err.message,
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
export default KnowledgeService;
|