UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

939 lines (938 loc) 38.1 kB
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;