UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

200 lines (199 loc) 7.84 kB
/** * Recipes API 路由 * 提供 Recipe 知识图谱关系发现等操作 * * 说明: Recipe 的 CRUD 已由 knowledge.js 统一提供, * 此路由仅处理 Recipe 特有的批量 AI 操作。 */ import express from 'express'; import { COUNTABLE_LIFECYCLES } from '../../domain/knowledge/Lifecycle.js'; import Logger from '../../infrastructure/logging/Logger.js'; import { getServiceContainer } from '../../injection/ServiceContainer.js'; const router = express.Router(); const logger = Logger.getInstance(); /* ═══ 进程内任务状态(单实例足够) ═══════════════════════ */ let discoverTask = { status: 'idle', // idle | running | done | error startedAt: null, finishedAt: null, discovered: 0, totalPairs: 0, batchErrors: 0, error: null, elapsed: 0, message: null, }; function resetTask() { discoverTask = { status: 'idle', startedAt: null, finishedAt: null, discovered: 0, totalPairs: 0, batchErrors: 0, error: null, elapsed: 0, message: null, }; } /* ═══ POST /api/v1/recipes/discover-relations ═══════════ */ /** * 异步启动 AI 批量发现 Recipe 知识图谱关系 * Body: { batchSize?: number } * * 立即返回 { status: 'started' },后台执行。 * Dashboard 通过 GET /discover-relations/status 轮询进度。 */ router.post('/discover-relations', async (req, res) => { const { batchSize: _batchSize = 20 } = req.body; // 如果已有任务在运行,返回当前状态 if (discoverTask.status === 'running') { const elapsed = Math.round((Date.now() - new Date(discoverTask.startedAt).getTime()) / 1000); return void res.json({ success: true, data: { status: 'running', startedAt: discoverTask.startedAt, elapsed, message: 'AI 分析仍在进行中', }, }); } // 检查 ToolRegistry 是否可用 const container = getServiceContainer(); let agentFactory; try { agentFactory = container.get('agentFactory'); } catch { return void res.json({ success: true, data: { status: 'error', error: 'AgentFactory 不可用,请检查 AI Provider 配置' }, }); } // Mock 模式下跳过 AI 关系发现 if (agentFactory.getAiProviderInfo?.()?.name === 'mock') { return void res.json({ success: true, data: { status: 'error', error: 'AI Provider 未配置,当前为 Mock 模式。请先配置 API Key。' }, }); } // 快速检查:至少需要 2 条可消费 Recipe(active/staging/pending/evolving) try { const knowledgeRepo = container.get('knowledgeRepository'); const count = await knowledgeRepo.countByLifecycles(COUNTABLE_LIFECYCLES); if (count < 2) { return void res.json({ success: true, data: { status: 'empty', message: `只有 ${count} 条活跃 Recipe,至少需要 2 条才能分析关系`, }, }); } } catch { // 如果查询失败,继续尝试(让 runTask 给出具体错误) } // 重置并启动后台任务 resetTask(); discoverTask.status = 'running'; discoverTask.startedAt = new Date().toISOString(); // 异步执行,不 await (async () => { try { const result = await agentFactory.discoverRelations(); const relations = result.relations || []; const analyzed = result.analyzed || 0; // 将 AI 发现的关系写入知识图谱 // AI 返回的 from/to 是 Recipe 标题,需要解析为实际 ID let written = 0; if (relations.length > 0) { try { const graphService = container.get('knowledgeGraphService'); const knowledgeRepo = container.get('knowledgeRepository'); // 缓存标题 → ID 映射,避免重复查询 const titleToId = new Map(); const resolveId = async (title) => { if (titleToId.has(title)) { return titleToId.get(title); } try { const entry = await knowledgeRepo.findByTitle(title); const id = entry?.id ?? null; titleToId.set(title, id); return id; } catch { titleToId.set(title, null); return null; } }; for (const rel of relations) { if (!rel.from || !rel.to || !rel.type) { continue; } const fromId = await resolveId(rel.from); const toId = await resolveId(rel.to); if (!fromId || !toId) { continue; } const res = await graphService.addEdge(fromId, 'recipe', toId, 'recipe', rel.type, { weight: 0.7, source: 'ai-discovery', evidence: rel.evidence || '', }); if (res.success) { written++; } } } catch (graphErr) { logger.warn('Failed to write some discovered edges', { error: graphErr.message, }); } } discoverTask.status = 'done'; discoverTask.finishedAt = new Date().toISOString(); discoverTask.discovered = written; discoverTask.totalPairs = analyzed; discoverTask.batchErrors = relations.length - written; discoverTask.elapsed = Math.round((new Date(discoverTask.finishedAt).getTime() - new Date(discoverTask.startedAt).getTime()) / 1000); logger.info('Discover relations completed', { discovered: discoverTask.discovered, totalPairs: discoverTask.totalPairs, batchErrors: discoverTask.batchErrors, elapsed: discoverTask.elapsed, }); } catch (err) { discoverTask.status = 'error'; discoverTask.finishedAt = new Date().toISOString(); discoverTask.error = err.message; discoverTask.elapsed = Math.round((new Date(discoverTask.finishedAt).getTime() - new Date(discoverTask.startedAt).getTime()) / 1000); logger.error('Discover relations failed', { error: err.message }); } })(); res.json({ success: true, data: { status: 'started', startedAt: discoverTask.startedAt, message: 'AI 分析已启动,正在后台运行', }, }); }); /* ═══ GET /api/v1/recipes/discover-relations/status ═════ */ /** 查询关系发现任务状态 */ router.get('/discover-relations/status', async (req, res) => { const data = { ...discoverTask }; // 计算实时 elapsed if (data.status === 'running' && data.startedAt) { data.elapsed = Math.round((Date.now() - new Date(data.startedAt).getTime()) / 1000); } res.json({ success: true, data }); }); export default router;