autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
939 lines (938 loc) • 38.1 kB
JavaScript
import { KnowledgeEntry } from '../../domain/knowledge/KnowledgeEntry.js';
import { inferKind, Lifecycle } from '../../domain/knowledge/Lifecycle.js';
import Logger from '../../infrastructure/logging/Logger.js';
import { ConflictError, NotFoundError, ValidationError } from '../../shared/errors/index.js';
/**
* KnowledgeService — 统一知识服务
*
* 替代 CandidateService + RecipeService。
* 全链路使用 KnowledgeEntry 实体 + wire format,
* 无需 promote、无需 metadata 袋子、无需打平映射。
*
* 生命周期操作委托给 KnowledgeEntry 实体方法,
* Service 负责编排 Repository / FileWriter / AuditLog / Graph / SkillHooks。
*/
export class KnowledgeService {
_confidenceRouter;
_edgeRepo;
_eventBus;
_fileWriter;
_knowledgeGraphService;
_proposalRepo;
_qualityScorer;
_skillHooks;
auditLogger;
gateway;
logger;
repository;
constructor(repository, auditLogger, gateway, knowledgeGraphService, options = {}) {
this.repository = repository;
this.auditLogger = auditLogger;
this.gateway = gateway;
this._knowledgeGraphService = knowledgeGraphService || null;
this._fileWriter = options.fileWriter || null;
this._skillHooks = options.skillHooks || null;
this._confidenceRouter = options.confidenceRouter || null;
this._qualityScorer = options.qualityScorer || null;
this._eventBus = options.eventBus || null;
this._edgeRepo = options.edgeRepo || null;
this._proposalRepo = options.proposalRepo || null;
this.logger = Logger.getInstance();
}
/* ═══ CRUD ══════════════════════════════════════════════ */
/**
* 创建知识条目
*
* MCP 参数 = wire format → KnowledgeEntry.fromJSON() 直接构造。
* 所有新条目初始状态为 pending(待审核)。
* ConfidenceRouter 仅标记 auto_approvable 标志,不改变 lifecycle。
*
* @param data wire format 数据
* @param context { userId }
*/
async create(data, context) {
try {
this._validateCreateInput(data);
// ── 标题去重:防止跨维度/跨调用创建同名条目 ──
if (data.title) {
const existing = await this.repository.findByTitle(data.title);
if (existing) {
throw new ConflictError(`Knowledge entry with title "${data.title}" already exists (id: ${existing.id})`, { existingId: existing.id, title: data.title });
}
}
const entry = KnowledgeEntry.fromJSON({
...data,
lifecycle: Lifecycle.PENDING,
source: data.source || 'manual',
createdBy: context.userId,
});
if (!entry.isValid()) {
throw new ValidationError('title + content required');
}
// ── SkillHooks: onKnowledgeSubmit ──
if (this._skillHooks) {
const hookResult = await this._skillHooks.run('onKnowledgeSubmit', entry, {
userId: context.userId,
});
if (hookResult?.block) {
throw new ValidationError(`SkillHook blocked: ${hookResult.reason || 'unknown'}`);
}
}
// ── ConfidenceRouter — staging 路由 ──
if (this._confidenceRouter) {
const route = await this._confidenceRouter.route(entry);
if (route.action === 'auto_approve') {
entry.autoApprovable = true;
// 六态状态机:高置信度条目进入 staging
if (route.targetState === 'staging' && route.gracePeriod) {
entry.lifecycle = Lifecycle.STAGING;
entry.stagingDeadline = Date.now() + route.gracePeriod;
}
}
else if (route.action === 'reject' && route.targetState === 'deprecated') {
entry.lifecycle = Lifecycle.DEPRECATED;
}
// pending 保持不变
}
// 注意: staging 条目由 StagingManager.checkAndPromote() 在到期后自动转为 active。
// autoApprovable 标记保留,供前端显示「推荐批准」徽章。
// CursorDelivery 已支持高置信度 staging/pending 条目的交付。
// ── file-first: 先落盘 .md,再写 DB(文件=真相源) ──
// fileWriter.persist() 会设置 entry.sourceFile,
// 后续 repository.create() 自动包含 sourceFile 字段,无需异步回写。
if (this._fileWriter) {
this._fileWriter.persist(entry);
}
const saved = await this.repository.create(entry);
// 同步 relations → knowledge_edges
this._syncRelationsToGraph(saved.id, saved.relations);
// 自动发现同域条目建立 related 边(best effort, 不阻塞)
this._autoDiscoverRelations(saved.id, saved).catch((err) => this.logger.warn('_autoDiscoverRelations error', { id: saved.id, error: err.message }));
// 审计日志
await this._audit('create_knowledge', saved.id, context.userId, {
title: saved.title,
lifecycle: saved.lifecycle,
kind: saved.kind,
});
this.logger.info('Knowledge entry created', {
id: saved.id,
lifecycle: saved.lifecycle,
kind: saved.kind,
createdBy: context.userId,
});
// ── SkillHooks: onKnowledgeCreated (fire-and-forget) ──
if (this._skillHooks) {
this._skillHooks
.run('onKnowledgeCreated', saved, {
userId: context.userId,
})
.catch((err) => this.logger.warn('SkillHook onKnowledgeCreated error', {
error: err instanceof Error ? err.message : String(err),
}));
}
// ── EventBus: 通知 VectorService 同步向量索引 ──
if (this._eventBus) {
this._eventBus.emit('knowledge:changed', {
action: 'create',
entryId: saved.id,
entry: { id: saved.id, title: saved.title, content: saved.content, kind: saved.kind },
});
}
return saved;
}
catch (error) {
this.logger.error('Error creating knowledge entry', {
error: error instanceof Error ? error.message : String(error),
data,
});
throw error;
}
}
/** 获取单个知识条目 */
async get(id) {
const entry = await this.repository.findById(id);
if (!entry) {
throw new NotFoundError('Knowledge entry not found', 'knowledge', id);
}
return entry;
}
/**
* 更新知识条目(仅允许白名单字段)
* @param data 部分字段(camelCase)
* @param context { userId }
*/
async update(id, data, context) {
try {
const _entry = await this._findOrThrow(id);
const UPDATABLE = [
'title',
'description',
'trigger',
'language',
'category',
'knowledgeType',
'complexity',
'scope',
'difficulty',
'content',
'relations',
'constraints',
'reasoning',
'tags',
'headers',
'headerPaths',
'moduleName',
'includeHeaders',
'agentNotes',
'aiInsight',
// Cursor 交付字段
'topicHint',
'whenClause',
'doClause',
'dontClause',
'coreCode',
'usageGuide',
];
const dbUpdates = {};
for (const key of UPDATABLE) {
if (data[key] === undefined) {
continue;
}
switch (key) {
// 标量字段直传
case 'title':
case 'description':
case 'trigger':
case 'language':
case 'category':
case 'complexity':
case 'scope':
case 'difficulty':
case 'agentNotes':
case 'aiInsight':
case 'moduleName':
case 'includeHeaders':
case 'topicHint':
case 'whenClause':
case 'doClause':
case 'dontClause':
case 'coreCode':
dbUpdates[key] = data[key];
break;
case 'knowledgeType':
dbUpdates.knowledgeType = data.knowledgeType;
dbUpdates.kind = inferKind(data.knowledgeType ?? '');
break;
// 值对象 / 数组字段 — 直传原始值,Repository._entityToRow 负责序列化
case 'content':
case 'relations':
case 'constraints':
case 'reasoning':
case 'headers':
case 'headerPaths':
dbUpdates[key] = data[key];
break;
// tags 需要特殊处理:API 返回时已过滤系统标签,保存时需要合并回来
case 'tags': {
const existingSystemTags = (_entry.tags || []).filter((t) => KnowledgeEntry.isSystemTag(t));
const incomingUserTags = (data.tags || []).filter((t) => !KnowledgeEntry.isSystemTag(t));
dbUpdates.tags = [...incomingUserTags, ...existingSystemTags];
break;
}
}
}
if (Object.keys(dbUpdates).length === 0) {
throw new ValidationError('No updatable fields provided');
}
dbUpdates.updatedAt = Math.floor(Date.now() / 1000);
// ── file-first: 先落盘 .md,再写 DB(文件=真相源) ──
if (this._fileWriter) {
Object.assign(_entry, dbUpdates);
this._fileWriter.persist(_entry);
// fileWriter 可能更新 sourceFile,同步到 dbUpdates
if (_entry.sourceFile) {
dbUpdates.sourceFile = _entry.sourceFile;
}
}
const updated = await this.repository.update(id, dbUpdates);
// 若 relations 变更,同步到 knowledge_edges
if (dbUpdates.relations) {
this._syncRelationsToGraph(id, data.relations);
}
await this._audit('update_knowledge', id, context.userId, {
fields: Object.keys(dbUpdates),
});
this.logger.info('Knowledge entry updated', {
id,
updatedBy: context.userId,
fields: Object.keys(dbUpdates),
});
// ── EventBus: 通知 VectorService 同步向量索引 ──
if (this._eventBus) {
this._eventBus.emit('knowledge:changed', {
action: 'update',
entryId: id,
entry: {
id: updated.id,
title: updated.title,
content: updated.content,
kind: updated.kind,
},
});
}
return updated;
}
catch (error) {
this.logger.error('Error updating knowledge entry', {
id,
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
}
/**
* 删除知识条目
* @param context { userId }
* @returns >}
*/
async delete(id, context) {
try {
const entry = await this._findOrThrow(id);
// 删除 .md 文件
this._removeFile(entry);
// 清除 knowledge_edges
this._removeAllEdges(id);
// 清除 evolution_proposals(无 ON DELETE CASCADE,需手动删除)
this._removeRelatedProposals(id);
await this.repository.delete(id);
await this._audit('delete_knowledge', id, context.userId, {
title: entry.title,
});
this.logger.info('Knowledge entry deleted', {
id,
deletedBy: context.userId,
title: entry.title,
});
// ── EventBus: 通知 VectorService 移除向量索引 ──
if (this._eventBus) {
this._eventBus.emit('knowledge:deleted', { entryId: id });
}
return { success: true, id };
}
catch (error) {
this.logger.error('Error deleting knowledge entry', {
id,
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
}
/* ═══ 生命周期操作 ══════════════════════════════════════ */
/** 发布 (pending → active) — 仅开发者可执行 */
async publish(id, context) {
const result = await this._lifecycleTransition(id, 'publish', context, {
entityArgs: [context.userId],
});
// 发布后触发 Cursor Delivery 增量更新(非阻塞)
this._triggerCursorDeliveryAsync();
return result;
}
/**
* 触发 Cursor Delivery Pipeline(非阻塞、容错)
*/
_triggerCursorDeliveryAsync() {
import('../../injection/ServiceContainer.js')
.then(({ getServiceContainer }) => {
const container = getServiceContainer();
if (container.services.cursorDeliveryPipeline) {
const pipeline = container.get('cursorDeliveryPipeline');
pipeline.deliver().catch(() => {
/* ignore */
});
}
})
.catch(() => {
// ServiceContainer 未初始化或服务不可用 — 静默忽略
});
}
/** 弃用 (pending|active → deprecated) */
async deprecate(id, reason, context) {
if (!reason || reason.trim().length === 0) {
throw new ValidationError('Deprecation reason is required');
}
return this._lifecycleTransition(id, 'deprecate', context, {
entityArgs: [reason],
});
}
/** 重新激活 (deprecated|staging → pending) */
async reactivate(id, context) {
return this._lifecycleTransition(id, 'reactivate', context);
}
/** 进入暂存期 (pending → staging) */
async stage(id, context) {
return this._lifecycleTransition(id, 'stage', context);
}
/** 进入进化态 (active → evolving) */
async evolve(id, context) {
return this._lifecycleTransition(id, 'evolve', context);
}
/** 进入衰退观察 (active|evolving → decaying) */
async decay(id, context) {
return this._lifecycleTransition(id, 'decay', context);
}
/** 恢复为已发布 (decaying|evolving → active) */
async restore(id, context) {
return this._lifecycleTransition(id, 'restore', context);
}
// ── 向后兼容别名 ──
/** @deprecated 简化后所有条目直接进 pending */
async submit(id, _context) {
return this.get(id);
}
/** @deprecated 简化后 approve = publish */
async approve(id, context) {
return this.publish(id, context);
}
/** @deprecated 简化后无需 autoApprove */
async autoApprove(id, _context) {
return this.get(id);
}
/** @deprecated 简化后 reject = deprecate */
async reject(id, reason, context) {
return this.deprecate(id, reason, context);
}
/** @deprecated 简化后 toDraft = reactivate */
async toDraft(id, context) {
return this.reactivate(id, context);
}
/** @deprecated 简化后 fastTrack = publish */
async fastTrack(id, context) {
return this.publish(id, context);
}
/* ═══ 查询 ══════════════════════════════════════════════ */
/**
* 查询列表
* @param filters { lifecycle, kind, language, category, knowledgeType, source, tag }
* @param pagination { page, pageSize }
*/
async list(filters = {}, pagination = {}) {
try {
const { lifecycle, kind, language, category, knowledgeType, source, tag, scope } = filters;
const { page = 1, pageSize = 20 } = pagination;
const dbFilters = {};
if (lifecycle) {
dbFilters.lifecycle = lifecycle;
}
if (kind) {
dbFilters.kind = kind;
}
if (language) {
dbFilters.language = language;
}
if (category) {
dbFilters.category = category;
}
if (knowledgeType) {
dbFilters.knowledgeType = knowledgeType;
}
if (source) {
dbFilters.source = source;
}
if (scope) {
dbFilters.scope = scope;
}
if (tag) {
dbFilters._tagLike = tag;
}
return this.repository.findWithPagination(dbFilters, { page, pageSize });
}
catch (error) {
this.logger.error('Error listing knowledge entries', {
error: error instanceof Error ? error.message : String(error),
filters,
});
throw error;
}
}
/** 按 Kind 查询 */
async listByKind(kind, pagination = {}) {
try {
const { page = 1, pageSize = 20 } = pagination;
return this.repository.findByKind(kind, { page, pageSize });
}
catch (error) {
this.logger.error('Error listing by kind', {
kind,
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
}
/** 搜索 */
async search(keyword, pagination = {}) {
try {
const { page = 1, pageSize = 20 } = pagination;
return this.repository.search(keyword, { page, pageSize });
}
catch (error) {
this.logger.error('Error searching knowledge', {
keyword,
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
}
/** 获取统计信息 */
async getStats() {
try {
return this.repository.getStats();
}
catch (error) {
this.logger.error('Error getting knowledge stats', {
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
}
/* ═══ 使用/质量 ═════════════════════════════════════ */
/**
* 增加使用计数
* @param [options] { actor, feedback }
*/
async incrementUsage(id, type = 'adoption', options = {}) {
try {
const entry = await this._findOrThrow(id);
entry.stats.increment(type);
const statsJson = entry.stats.toJSON();
await this.repository.update(id, {
stats: JSON.stringify(statsJson),
updatedAt: Math.floor(Date.now() / 1000),
});
await this._audit(`knowledge_${type}`, id, options.actor || 'system', {
feedback: options.feedback,
});
this.logger.debug(`Knowledge ${type} incremented`, { id, type });
return entry;
}
catch (error) {
this.logger.error(`Error incrementing knowledge ${type}`, {
id,
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
}
/**
* 更新质量评分
* @param [context] { userId }
*/
async updateQuality(id, context = {}) {
try {
const entry = await this._findOrThrow(id);
if (!this._qualityScorer) {
throw new ValidationError('QualityScorer not configured');
}
// 为 QualityScorer 适配输入字段
const scorerInput = this._adaptForScorer(entry);
const result = this._qualityScorer.score(scorerInput);
// 更新 Quality 值对象;同步计算 authority(0‑5)
const qualityJson = {
completeness: result.dimensions.completeness,
adaptation: result.dimensions.deliveryReady,
documentation: result.dimensions.contentDepth,
overall: result.score,
grade: result.grade,
};
// 当 authority 从未手动设置(仍为 0)时,从 quality.overall 自动推导
const currentAuthority = entry.stats?.authority ?? 0;
const updatePayload = {
quality: JSON.stringify(qualityJson),
updatedAt: Math.floor(Date.now() / 1000),
};
if (currentAuthority === 0 && result.score > 0) {
const statsObj = entry.stats?.toJSON?.() ?? (typeof entry.stats === 'object' ? { ...entry.stats } : {});
updatePayload.stats = JSON.stringify({
...statsObj,
authority: Math.round(result.score * 5),
});
}
await this.repository.update(id, updatePayload);
// ── .md 文件同步: quality 更新后重新落盘,保持文件=真相源 ──
if (this._fileWriter) {
try {
const updated = await this.repository.findById(id);
if (updated) {
this._fileWriter.persist(updated);
}
}
catch {
/* best effort — 不阻塞质量更新流程 */
}
}
if (context.userId) {
await this._audit('update_knowledge_quality', id, context.userId, {
score: result.score,
grade: result.grade,
});
}
this.logger.info('Knowledge quality updated', {
id,
score: result.score,
grade: result.grade,
});
return result;
}
catch (error) {
this.logger.error('Error updating knowledge quality', {
id,
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
}
/* ═══ 私有方法 ══════════════════════════════════════════ */
/** 统一生命周期转换编排 */
async _lifecycleTransition(id, method, context, options = {}) {
try {
const entry = await this._findOrThrow(id);
const prevLifecycle = entry.lifecycle;
const entityArgs = options.entityArgs || [];
const result = entry[method](...entityArgs);
if (!result.success) {
throw new ConflictError(result.error || 'Lifecycle transition failed', {
detail: `Lifecycle ${method} failed for ${id}`,
});
}
// 标记操作人到最后一条 lifecycleHistory 条目
entry.stampLastTransition(context.userId);
// 构建 DB 更新
// 注意: 不在此处 JSON.stringify — repository.update() 内部
// 通过 _entityToRow() 统一执行序列化, 传入原始值即可
const dbUpdates = {
lifecycle: entry.lifecycle,
lifecycleHistory: entry.lifecycleHistory,
updatedAt: entry.updatedAt,
};
// 审核字段
if (entry.reviewedBy) {
dbUpdates.reviewedBy = entry.reviewedBy;
}
if (entry.reviewedAt) {
dbUpdates.reviewedAt = entry.reviewedAt;
}
// 驳回原因(含清除:reactivate 后 rejectionReason = null 需写入 DB)
dbUpdates.rejectionReason = entry.rejectionReason;
// 发布字段
if (entry.publishedAt) {
dbUpdates.publishedAt = entry.publishedAt;
}
if (entry.publishedBy) {
dbUpdates.publishedBy = entry.publishedBy;
}
if (entry.autoApprovable !== undefined) {
dbUpdates.autoApprovable = entry.autoApprovable ? 1 : 0;
}
// ── file-first: 先迁移 .md 文件,再更新 DB lifecycle(文件=真相源) ──
if (this._fileWriter) {
this._fileWriter.moveOnLifecycleChange(entry);
}
const updated = await this.repository.update(id, dbUpdates);
await this._audit(`${method}_knowledge`, id, context.userId, {
from: prevLifecycle,
to: entry.lifecycle,
});
this.logger.info(`Knowledge entry ${method}`, {
id,
from: prevLifecycle,
to: entry.lifecycle,
actor: context.userId,
});
// EventBus: 通知生命周期状态转换(Dashboard 实时更新 + SignalBus)
if (this._eventBus) {
this._eventBus.emit('lifecycle:transition', {
entryId: id,
from: prevLifecycle,
to: entry.lifecycle,
method,
actor: context.userId,
});
}
return updated;
}
catch (error) {
this.logger.error(`Error in lifecycle ${method}`, {
id,
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
}
/** 查找或抛出 NotFoundError */
async _findOrThrow(id) {
const entry = await this.repository.findById(id);
if (!entry) {
throw new NotFoundError('Knowledge entry not found', 'knowledge', id);
}
return entry;
}
/** 验证创建输入 */
_validateCreateInput(data) {
if (!data.title || !data.title.trim()) {
throw new ValidationError('Title is required');
}
// 内容至少需要 content 对象有内容
const c = (data.content || {});
if (!c.pattern &&
!c.rationale &&
!(c.steps?.length && c.steps.length > 0) &&
!c.markdown) {
throw new ValidationError('Content is required (pattern, rationale, steps, or markdown)');
}
}
/**
* 为 QualityScorer 适配输入
* QualityScorer v2 needs: title, trigger, description, language, category,
* doClause, dontClause, whenClause, coreCode, usageGuide,
* contentMarkdown, contentRationale, reasoningWhyStandard, reasoningSources,
* reasoningConfidence, source, headers, tags, views, clicks, rating
*/
_adaptForScorer(entry) {
// 从 Stats 值对象提取 engagement 指标
const stats = entry.stats && typeof entry.stats === 'object'
? entry.stats
: {};
// 从 Content 值对象提取深度字段
const content = entry.content && typeof entry.content === 'object'
? entry.content
: {};
// 从 Reasoning 值对象提取溯源字段
const reasoning = entry.reasoning && typeof entry.reasoning === 'object'
? entry.reasoning
: {};
return {
title: entry.title,
trigger: entry.trigger,
description: entry.description || '',
language: entry.language,
category: entry.category,
doClause: entry.doClause || '',
dontClause: entry.dontClause || '',
whenClause: entry.whenClause || '',
coreCode: entry.coreCode || '',
usageGuide: entry.usageGuide || content.markdown || entry.doClause || '',
contentMarkdown: content.markdown || '',
contentRationale: content.rationale || '',
reasoningWhyStandard: reasoning.whyStandard || '',
reasoningSources: reasoning.sources || [],
reasoningConfidence: reasoning.confidence || 0,
source: entry.source || '',
headers: entry.headers || [],
tags: entry.tags || [],
views: (stats.views ?? 0) + (stats.searchHits ?? 0),
clicks: (stats.adoptions ?? 0) + (stats.applications ?? 0) + (stats.guardHits ?? 0),
rating: stats.authority ?? 0,
};
}
/* ═══ Knowledge Graph 同步 ═══════════════════════════ */
/**
* 自动发现同 category/moduleName/tags 的已有条目并建立 'related' 边
* @param id 新创建的条目 ID
* @param entry 条目实体
*/
async _autoDiscoverRelations(id, entry) {
const gs = this._knowledgeGraphService;
if (!gs) {
return;
}
try {
const candidates = [];
// 与可消费 Recipe(active/staging/evolving)建立关联
const consumableFilter = {
lifecycle: [Lifecycle.ACTIVE, Lifecycle.STAGING, Lifecycle.EVOLVING],
};
// 按 moduleName 查同模块可消费条目
if (entry.moduleName) {
const sameModule = await this.repository.findWithPagination({ ...consumableFilter, moduleName: entry.moduleName }, { page: 1, pageSize: 20 });
for (const r of sameModule.data) {
if (r.id !== id) {
candidates.push({ target: r.id, relation: 'related', weight: 0.8 });
}
}
}
// 按 category 查同类可消费条目(弱关联)
if (entry.category && candidates.length < 10) {
const sameCat = await this.repository.findWithPagination({ ...consumableFilter, category: entry.category }, { page: 1, pageSize: 10 });
for (const r of sameCat.data) {
if (r.id !== id && !candidates.some((c) => c.target === r.id)) {
candidates.push({ target: r.id, relation: 'related', weight: 0.4 });
}
}
}
// 写入 edges(限制最多 10 条自动关联)
for (const c of candidates.slice(0, 10)) {
try {
gs.addEdge(id, 'knowledge', c.target, 'knowledge', c.relation, { weight: c.weight });
}
catch {
/* ignore duplicates */
}
}
// 将发现的关系写回 entry 的 relations 字段
if (candidates.length > 0) {
const relatedItems = candidates.slice(0, 10).map((c) => ({
target: c.target,
description: 'auto-discovered',
}));
const existingRelations = (typeof entry.relations?.toJSON === 'function'
? entry.relations.toJSON()
: entry.relations || {});
const merged = {
...existingRelations,
related: [...(existingRelations['related'] || []), ...relatedItems],
};
await this.repository.update(id, {
relations: JSON.stringify(merged),
updatedAt: Math.floor(Date.now() / 1000),
});
}
}
catch (err) {
this.logger.warn('Auto-discover relations failed (non-blocking)', {
id,
error: err instanceof Error ? err.message : String(err),
});
}
}
/** 将 relations 同步到 knowledge_edges 表 */
_syncRelationsToGraph(id, relations) {
const gs = this._knowledgeGraphService;
if (!gs) {
return;
}
try {
if (this._edgeRepo) {
this._edgeRepo.deleteOutgoing(id, 'knowledge');
}
if (!relations || typeof relations !== 'object') {
return;
}
// Relations 可能是 Relations 值对象或普通对象
const relObj = (typeof relations.toJSON === 'function'
? relations.toJSON()
: relations);
for (const [relType, targets] of Object.entries(relObj)) {
if (!Array.isArray(targets)) {
continue;
}
for (const t of targets) {
const item = t;
const targetId = item.target || item.id || (typeof t === 'string' ? t : null);
if (targetId) {
gs.addEdge(id, 'knowledge', targetId, 'knowledge', relType, {
weight: item.weight || 1.0,
});
}
}
}
}
catch (err) {
this.logger.warn('Failed to sync relations to knowledge_edges', {
id,
error: err instanceof Error ? err.message : String(err),
});
}
}
/** 删除所有关联边 */
_removeAllEdges(id) {
if (!this._edgeRepo) {
return;
}
try {
this._edgeRepo.deleteByEntryId(id);
}
catch (err) {
this.logger.warn('Failed to remove edges', {
id,
error: err instanceof Error ? err.message : String(err),
});
}
}
/** 删除关联的 evolution_proposals(target_recipe_id 无 CASCADE) */
_removeRelatedProposals(id) {
if (!this._proposalRepo) {
return;
}
try {
this._proposalRepo.deleteByTargetRecipeId(id);
}
catch (err) {
this.logger.warn('Failed to remove related proposals', {
id,
error: err instanceof Error ? err.message : String(err),
});
}
}
/* ═══ 文件落盘 ═════════════════════════════════ */
/** 落盘到 .md 文件 + 回写 sourceFile */
_persistToFile(entry) {
if (!this._fileWriter) {
return;
}
try {
const oldSourceFile = entry.sourceFile;
this._fileWriter.persist(entry);
if (entry.sourceFile && entry.sourceFile !== oldSourceFile) {
this.repository.update(entry.id, { sourceFile: entry.sourceFile }).catch((err) => {
this.logger.warn('Failed to update sourceFile in DB', {
id: entry.id,
error: err instanceof Error ? err.message : String(err),
});
});
}
}
catch (err) {
this.logger.warn('Knowledge file persist failed (non-blocking)', {
id: entry?.id,
error: err instanceof Error ? err.message : String(err),
});
}
}
/** 删除 .md 文件 */
_removeFile(entry) {
if (!this._fileWriter) {
return;
}
try {
this._fileWriter.remove(entry);
}
catch (err) {
this.logger.warn('Knowledge file remove failed (non-blocking)', {
id: entry?.id,
error: err instanceof Error ? err.message : String(err),
});
}
}
/* ═══ 审计日志 ═══════════════════════════════════════ */
async _audit(action, id, actor, details = {}) {
try {
await this.auditLogger.log({
action,
resourceType: 'knowledge',
resourceId: id,
resource: `knowledge:${id}`,
actor: actor || 'system',
result: 'success',
details: typeof details === 'string' ? details : JSON.stringify(details),
timestamp: Math.floor(Date.now() / 1000),
});
}
catch (err) {
this.logger.warn('Audit log failed (non-blocking)', {
action,
id,
error: err instanceof Error ? err.message : String(err),
});
}
}
}
export default KnowledgeService;