UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

304 lines (303 loc) 10.7 kB
/** * Wiki API 路由 * * 提供 Repo Wiki 的生成、查询、更新操作。 * 支持异步生成 + Socket.io 进度推送。 * * 端点: * POST /api/v1/wiki/generate — 触发全量生成 * POST /api/v1/wiki/update — 增量更新 * POST /api/v1/wiki/abort — 中止生成 * GET /api/v1/wiki/status — 获取 Wiki 状态 * GET /api/v1/wiki/files — 列出 Wiki 文件 * GET /api/v1/wiki/file/:path — 读取某个 Wiki 文件内容 */ import fs from 'node:fs'; import path from 'node:path'; import express from 'express'; import Logger from '../../infrastructure/logging/Logger.js'; import { getServiceContainer } from '../../injection/ServiceContainer.js'; import { WikiGenerator, } from '../../service/wiki/WikiGenerator.js'; import { DEFAULT_KNOWLEDGE_BASE_DIR } from '../../shared/ProjectMarkers.js'; const router = express.Router(); const logger = Logger.getInstance(); /* ═══ 进程内 Wiki 任务状态 ═══════════════════════════════ */ let wikiTask = { status: 'idle', // idle | running | done | error phase: null, progress: 0, message: null, startedAt: null, finishedAt: null, result: null, error: null, }; let currentGenerator = null; function resetWikiTask() { wikiTask = { status: 'idle', phase: null, progress: 0, message: null, startedAt: null, finishedAt: null, result: null, error: null, }; currentGenerator = null; } /** 外部读取 wikiTask 状态(供 bootstrap orchestrator 等外部流程同步使用) */ export function getWikiTask() { return wikiTask; } /** 外部设置 wikiTask 状态(供 bootstrap orchestrator 等外部流程同步使用) */ export function patchWikiTask(patch) { Object.assign(wikiTask, patch); } /** 创建 WikiGenerator 实例 */ function createGenerator(container) { const projectRoot = container.singletons?._projectRoot || process.env.ASD_PROJECT_DIR || process.cwd(); // 尝试获取可用的服务(非必须的优雅降级) let moduleService = null; let knowledgeService = null; let codeEntityGraph = null; try { moduleService = container.get('moduleService'); } catch { /* ok */ } try { knowledgeService = container.get('knowledgeService'); } catch { /* ok */ } try { codeEntityGraph = container.get('codeEntityGraph'); } catch { /* ok */ } // 尝试获取已缓存的 ProjectGraph(可能在 bootstrap 中构建过) const projectGraph = (container.singletons?.projectGraph || null); // 获取 RealtimeService 用于推送进度 let realtimeService = null; try { realtimeService = container.singletons?.realtimeService || null; } catch { /* ok */ } const generator = new WikiGenerator({ projectRoot, moduleService: moduleService, knowledgeService: knowledgeService, projectGraph: projectGraph, codeEntityGraph: codeEntityGraph, aiProvider: (container.singletons?.aiProvider || null), onProgress: (phase, progress, message) => { wikiTask.phase = phase; wikiTask.progress = progress; wikiTask.message = message; // 通过 Socket.io 推送进度 if (realtimeService) { try { realtimeService.broadcastEvent?.('wiki:progress', { phase, progress, message, timestamp: Date.now(), }); } catch { /* non-critical */ } } }, options: { language: process.env.ASD_WIKI_LANG || 'zh', }, }); return generator; } /* ═══ POST /api/v1/wiki/generate ═══════════════════════ */ router.post('/generate', async (req, res) => { if (wikiTask.status === 'running') { return void res.status(409).json({ success: false, error: { code: 'ALREADY_RUNNING', message: 'Wiki 生成正在进行中' }, data: { progress: wikiTask.progress, phase: wikiTask.phase }, }); } const container = getServiceContainer(); resetWikiTask(); wikiTask.status = 'running'; wikiTask.startedAt = Date.now(); const generator = createGenerator(container); currentGenerator = generator; // 异步执行,立即返回 202 res.status(202).json({ success: true, message: 'Wiki 生成已启动,通过 /api/v1/wiki/status 或 Socket.io wiki:progress 事件追踪进度', }); // 后台执行生成 try { const result = (await generator.generate()); wikiTask.status = result.success ? 'done' : 'error'; wikiTask.finishedAt = Date.now(); wikiTask.result = result; if (!result.success) { wikiTask.error = result.error; } // 推送完成事件 const realtimeService = (container.singletons?.realtimeService || null); if (realtimeService) { realtimeService.broadcastEvent?.('wiki:completed', { success: result.success, filesGenerated: result.filesGenerated, duration: result.duration, }); } } catch (err) { wikiTask.status = 'error'; wikiTask.finishedAt = Date.now(); wikiTask.error = err.message; logger.error('[Wiki Route] Generation failed', { error: err.message }); } currentGenerator = null; }); /* ═══ POST /api/v1/wiki/update ═══════════════════════ */ router.post('/update', async (req, res) => { if (wikiTask.status === 'running') { return void res.status(409).json({ success: false, error: { code: 'ALREADY_RUNNING', message: 'Wiki 生成正在进行中' }, }); } const container = getServiceContainer(); resetWikiTask(); wikiTask.status = 'running'; wikiTask.startedAt = Date.now(); const generator = createGenerator(container); currentGenerator = generator; res.status(202).json({ success: true, message: 'Wiki 增量更新已启动', }); try { const result = (await generator.update()); wikiTask.status = result.success ? 'done' : 'error'; wikiTask.finishedAt = Date.now(); wikiTask.result = result; if (!result.success) { wikiTask.error = result.error; } } catch (err) { wikiTask.status = 'error'; wikiTask.finishedAt = Date.now(); wikiTask.error = err.message; } currentGenerator = null; }); /* ═══ POST /api/v1/wiki/abort ═══════════════════════ */ router.post('/abort', async (req, res) => { if (wikiTask.status !== 'running' || !currentGenerator) { return void res.json({ success: true, message: '没有正在运行的 Wiki 任务' }); } currentGenerator.abort(); wikiTask.status = 'error'; wikiTask.error = 'Aborted by user'; wikiTask.finishedAt = Date.now(); res.json({ success: true, message: 'Wiki 生成已中止' }); }); /* ═══ GET /api/v1/wiki/status ═══════════════════════ */ router.get('/status', async (req, res) => { const container = getServiceContainer(); // 如果没有活跃任务,从磁盘读取元数据 if (wikiTask.status === 'idle') { const generator = createGenerator(container); const diskStatus = generator.getStatus(); return void res.json({ success: true, data: { task: wikiTask, wiki: diskStatus, }, }); } res.json({ success: true, data: { task: { ...wikiTask }, }, }); }); /* ═══ GET /api/v1/wiki/files ═══════════════════════ */ router.get('/files', async (req, res) => { const projectRoot = process.env.ASD_PROJECT_DIR || process.cwd(); const wikiDir = path.join(projectRoot, DEFAULT_KNOWLEDGE_BASE_DIR, 'wiki'); if (!fs.existsSync(wikiDir)) { return void res.json({ success: true, data: { files: [], exists: false }, }); } const files = []; const readDir = (dir, prefix = '') => { for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const rel = prefix ? `${prefix}/${entry.name}` : entry.name; if (entry.isDirectory()) { readDir(path.join(dir, entry.name), rel); } else if (entry.name.endsWith('.md')) { const stat = fs.statSync(path.join(dir, entry.name)); files.push({ path: rel, name: entry.name, size: stat.size, modifiedAt: stat.mtime.toISOString(), }); } } }; readDir(wikiDir); res.json({ success: true, data: { files, exists: true, wikiDir }, }); }); /* ═══ GET /api/v1/wiki/file/:path(*) ═══════════════ */ router.get('/file/{*path}', async (req, res) => { const projectRoot = process.env.ASD_PROJECT_DIR || process.cwd(); const wikiDir = path.join(projectRoot, DEFAULT_KNOWLEDGE_BASE_DIR, 'wiki'); const rawPath = req.params.path; const requestedPath = Array.isArray(rawPath) ? rawPath.join('/') : String(rawPath ?? ''); if (!requestedPath) { return void res.status(400).json({ success: false, error: { message: 'path required' } }); } // 安全检查:防止路径穿越 const fullPath = path.resolve(wikiDir, requestedPath); if (!fullPath.startsWith(wikiDir)) { return void res .status(403) .json({ success: false, error: { message: 'Path traversal not allowed' } }); } if (!fs.existsSync(fullPath)) { return void res.status(404).json({ success: false, error: { message: 'File not found' } }); } const content = fs.readFileSync(fullPath, 'utf-8'); res.json({ success: true, data: { path: requestedPath, content, size: Buffer.byteLength(content), }, }); }); export default router;