/** * 光湖知识库 · API 服务器 * * Express + Git 引擎,提供知识库全部 REST API。 * Agent 直接调用这些接口,不经过任何第三方服务。 */ import express from 'express'; import cors from 'cors'; import { GitEngine } from './git-engine.js'; import { PersonaAgent, createDefaultPersona, type Message } from './persona-agent.js'; import { projectHumanKnowledgeTree } from './language-protocol-kernel.js'; import path from 'path'; import fs from 'fs'; import crypto from 'crypto'; // __dirname 兼容 CJS 和 ESM const _dirname = typeof __dirname !== 'undefined' ? __dirname : process.cwd(); // ─── 配置 ─── const PORT = parseInt(process.env.KB_PORT || '3890', 10); const REPO_PATH = process.env.KB_REPO_PATH || path.resolve(_dirname, '../kb-data'); const AGENT_STATE_PATH = process.env.HOLOLAKE_AGENT_STATE_PATH || path.resolve(REPO_PATH, '../agent-conversations.json'); // ─── 初始化 ─── const app = express(); const engine = new GitEngine(REPO_PATH); app.use(cors({ origin(origin, callback) { const allowed = !origin || origin === 'null' || /^https?:\/\/(127\.0\.0\.1|localhost)(:\d+)?$/.test(origin); callback(allowed ? null : new Error('仅允许 HoloLake 本机界面访问'), allowed); }, })); app.use(express.json({ limit: '10mb' })); // Express v5 的 {*param} 返回数组,工具函数统一转字符串 const p = (v: unknown): string => Array.isArray(v) ? v.join('/') : String(v); // ─── 健康检查 ─── app.get('/api/health', (_req, res) => { res.json({ module: 'guanghu-knowledge-base', version: '0.1.0', engine: 'git', repo: REPO_PATH, status: 'online', time: new Date().toISOString(), }); }); // ─── 文档树 ─── app.get('/api/tree', async (_req, res) => { try { const tree = await engine.getTree(); res.json({ ok: true, tree: projectHumanKnowledgeTree(tree) }); } catch (err: any) { res.status(500).json({ ok: false, error: err.message }); } }); // ─── 文档 CRUD ─── // 读取文档 app.get('/api/docs/{*docPath}', async (req, res) => { try { const docPath = p(req.params.docPath); const doc = await engine.getDoc(docPath); res.json({ ok: true, doc }); } catch (err: any) { res.status(404).json({ ok: false, error: `文档不存在: ${p(req.params.docPath)}` }); } }); // 创建文档 app.post('/api/docs/{*docPath}', async (req, res) => { try { const docPath = p(req.params.docPath); const { title, body, author } = req.body; if (!title || !body) { return res.status(400).json({ ok: false, error: 'title 和 body 必填' }); } const doc = await engine.createDoc(docPath, title, body, author); res.status(201).json({ ok: true, doc }); } catch (err: any) { res.status(500).json({ ok: false, error: err.message }); } }); // 更新文档 app.put('/api/docs/{*docPath}', async (req, res) => { try { const docPath = p(req.params.docPath); const { title, body, author } = req.body; if (!title || !body) { return res.status(400).json({ ok: false, error: 'title 和 body 必填' }); } const doc = await engine.updateDoc(docPath, title, body, author); res.json({ ok: true, doc }); } catch (err: any) { res.status(500).json({ ok: false, error: err.message }); } }); // 删除文档 app.delete('/api/docs/{*docPath}', async (req, res) => { try { const docPath = p(req.params.docPath); await engine.deleteDoc(docPath); res.json({ ok: true, message: `已删除: ${docPath}` }); } catch (err: any) { res.status(500).json({ ok: false, error: err.message }); } }); // 重命名/移动文档 app.post('/api/move', async (req, res) => { try { const { oldPath, newPath } = req.body; if (!oldPath || !newPath) { return res.status(400).json({ ok: false, error: 'oldPath 和 newPath 必填' }); } await engine.moveDoc(oldPath, newPath); res.json({ ok: true, message: `${oldPath} → ${newPath}` }); } catch (err: any) { res.status(500).json({ ok: false, error: err.message }); } }); // ─── 版本历史 ─── app.get('/api/history/{*docPath}', async (req, res) => { try { const docPath = p(req.params.docPath); const maxCount = parseInt(req.query.max?.toString() || '50', 10); const history = await engine.getHistory(docPath, maxCount); res.json({ ok: true, history }); } catch (err: any) { res.status(500).json({ ok: false, error: err.message }); } }); // 获取某版本内容 app.get('/api/version/:hash/{*docPath}', async (req, res) => { try { const { hash } = req.params; const docPath = p(req.params.docPath); const content = await engine.getDocAtVersion(docPath, hash); res.json({ ok: true, content }); } catch (err: any) { res.status(500).json({ ok: false, error: err.message }); } }); // 版本对比 app.get('/api/diff/{*docPath}', async (req, res) => { try { const docPath = p(req.params.docPath); const { from, to } = req.query; if (!from || !to) { return res.status(400).json({ ok: false, error: 'from 和 to (commit hash) 必填' }); } const diff = await engine.diffVersions(docPath, from.toString(), to.toString()); res.json({ ok: true, diff }); } catch (err: any) { res.status(500).json({ ok: false, error: err.message }); } }); // ─── 搜索 ─── app.get('/api/search', async (req, res) => { try { const query = req.query.q?.toString(); if (!query) { return res.status(400).json({ ok: false, error: 'q 参数必填' }); } const results = await engine.search(query); res.json({ ok: true, results, count: results.length }); } catch (err: any) { res.status(500).json({ ok: false, error: err.message }); } }); // ─── Forgejo(Git 远端引擎) ─── app.get('/api/forgejo/status', async (_req, res) => { try { res.json({ ok: true, status: await engine.getRepositoryStatus() }); } catch (err: any) { res.status(500).json({ ok: false, error: err.message }); } }); app.put('/api/forgejo/remote', async (req, res) => { try { const { url } = req.body; if (!url || typeof url !== 'string') { return res.status(400).json({ ok: false, error: 'url 必填且为字符串' }); } res.json({ ok: true, status: await engine.configureRemote(url) }); } catch (err: any) { res.status(400).json({ ok: false, error: err.message }); } }); app.delete('/api/forgejo/remote', async (_req, res) => { try { res.json({ ok: true, status: await engine.removeRemote() }); } catch (err: any) { res.status(400).json({ ok: false, error: err.message }); } }); app.post('/api/forgejo/fetch', async (_req, res) => { try { res.json({ ok: true, status: await engine.fetchRemote() }); } catch (err: any) { res.status(502).json({ ok: false, error: err.message }); } }); app.post('/api/forgejo/pull', async (_req, res) => { try { res.json({ ok: true, status: await engine.pullRemote() }); } catch (err: any) { res.status(409).json({ ok: false, error: err.message }); } }); app.post('/api/forgejo/push', async (req, res) => { try { if (req.body?.confirm !== true) { return res.status(428).json({ ok: false, error: '推送需要用户明确确认' }); } res.json({ ok: true, status: await engine.pushRemote() }); } catch (err: any) { res.status(409).json({ ok: false, error: err.message }); } }); // ─── Agent(人格体) ─── interface ConversationRecord { id: string; title: string; createdAt: string; updatedAt: string; agent: PersonaAgent; } const conversations = new Map(); let activeConversationId = ''; function persistConversations(): void { fs.mkdirSync(path.dirname(AGENT_STATE_PATH), { recursive: true }); const records = Array.from(conversations.values()).map(record => ({ id: record.id, title: record.title, createdAt: record.createdAt, updatedAt: record.updatedAt, messages: record.agent.getConversation(), })); fs.writeFileSync(AGENT_STATE_PATH, JSON.stringify({ activeConversationId, conversations: records }, null, 2), { mode: 0o600 }); } function createConversation(title = '新对话', restored?: { id: string; createdAt: string; updatedAt: string; messages: Message[] }): ConversationRecord { const timestamp = new Date().toISOString(); const agent = createDefaultPersona(engine); if (restored?.messages) agent.restoreConversation(restored.messages); const record: ConversationRecord = { id: restored?.id || `conversation-${crypto.randomUUID()}`, title, createdAt: restored?.createdAt || timestamp, updatedAt: restored?.updatedAt || timestamp, agent, }; conversations.set(record.id, record); activeConversationId = record.id; return record; } function initializeConversations(): void { if (conversations.size) return; try { const stored = JSON.parse(fs.readFileSync(AGENT_STATE_PATH, 'utf8')) as any; for (const item of Array.isArray(stored.conversations) ? stored.conversations : []) { if (!item?.id || !Array.isArray(item.messages)) continue; createConversation(String(item.title || '历史对话'), { id: String(item.id), createdAt: String(item.createdAt || new Date().toISOString()), updatedAt: String(item.updatedAt || new Date().toISOString()), messages: item.messages, }); } if (stored.activeConversationId && conversations.has(stored.activeConversationId)) { activeConversationId = stored.activeConversationId; } } catch { // 首次启动没有会话文件。 } if (!conversations.size) createConversation(); } function getConversationRecord(requestedId?: unknown): ConversationRecord { initializeConversations(); const id = typeof requestedId === 'string' && requestedId ? requestedId : activeConversationId; const record = conversations.get(id); if (!record) throw new Error('对话不存在或已经删除'); activeConversationId = record.id; return record; } function getAgent(requestedId?: unknown): PersonaAgent { return getConversationRecord(requestedId).agent; } // Agent 状态 app.get('/api/agent/status', (_req, res) => { const record = getConversationRecord(_req.query.conversationId); const agent = record.agent; const def = agent.getDefinition(); const runtime = agent.getRuntimeStatus(); res.json({ ok: true, persona: { id: def.id, name: def.name, role: def.role, model: def.model, permissionMode: def.permissionMode, }, conversationLength: agent.getConversation().length, configured: runtime.configured, operational: runtime.operational, tools: runtime.toolNames, engine: 'git', repo: REPO_PATH, conversationId: record.id, conversationTitle: record.title, }); }); app.get('/api/agent/conversations', (_req, res) => { initializeConversations(); res.json({ ok: true, activeConversationId, conversations: Array.from(conversations.values()) .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)) .map(record => ({ id: record.id, title: record.title, createdAt: record.createdAt, updatedAt: record.updatedAt, messageCount: record.agent.getConversation().length })), }); }); app.post('/api/agent/conversations', (_req, res) => { const record = createConversation(); persistConversations(); res.status(201).json({ ok: true, conversation: { id: record.id, title: record.title, createdAt: record.createdAt, updatedAt: record.updatedAt, messageCount: 0 } }); }); app.delete('/api/agent/conversations/:conversationId', (req, res) => { initializeConversations(); if (!conversations.delete(req.params.conversationId)) return res.status(404).json({ ok: false, error: '对话不存在' }); if (!conversations.size) createConversation(); if (!conversations.has(activeConversationId)) activeConversationId = Array.from(conversations.keys())[0]; persistConversations(); res.json({ ok: true, activeConversationId }); }); // Agent 对话 app.post('/api/agent/chat', async (req, res) => { try { const { message } = req.body; if (!message || typeof message !== 'string') { return res.status(400).json({ ok: false, error: 'message 必填且为字符串' }); } const record = getConversationRecord(req.body?.conversationId); const turn = await record.agent.chat(message); if (record.title === '新对话') record.title = message.trim().slice(0, 28) || '新对话'; record.updatedAt = new Date().toISOString(); persistConversations(); res.json({ ok: true, ...turn, conversationId: record.id, conversationTitle: record.title, conversationLength: record.agent.getConversation().length, pendingActions: record.agent.getPendingActions(), }); } catch (err: any) { res.status(500).json({ ok: false, error: err.message }); } }); app.get('/api/agent/actions', (_req, res) => { res.json({ ok: true, actions: getAgent(_req.query.conversationId).getPendingActions() }); }); app.post('/api/agent/actions/:actionId/confirm', async (req, res) => { try { const record = getConversationRecord(req.body?.conversationId); const result = await record.agent.confirmAction(req.params.actionId); record.updatedAt = new Date().toISOString(); persistConversations(); res.json({ ok: true, result, messages: record.agent.getConversation(), actions: record.agent.getPendingActions() }); } catch (err: any) { res.status(409).json({ ok: false, error: err.message }); } }); app.post('/api/agent/actions/:actionId/reject', (req, res) => { try { const record = getConversationRecord(req.body?.conversationId); record.agent.rejectAction(req.params.actionId); record.updatedAt = new Date().toISOString(); persistConversations(); res.json({ ok: true, messages: record.agent.getConversation(), actions: record.agent.getPendingActions() }); } catch (err: any) { res.status(409).json({ ok: false, error: err.message }); } }); // Agent 对话历史 app.get('/api/agent/conversation', (_req, res) => { const record = getConversationRecord(_req.query.conversationId); res.json({ ok: true, conversationId: record.id, title: record.title, messages: record.agent.getConversation() }); }); // 清空对话 app.post('/api/agent/clear', (_req, res) => { const record = getConversationRecord(_req.body?.conversationId); record.agent.clearConversation(); record.title = '新对话'; record.updatedAt = new Date().toISOString(); persistConversations(); res.json({ ok: true, message: '对话已清空' }); }); // ─── 启动 ─── async function start() { await engine.init(); // 初始化人格体 initializeConversations(); app.listen(PORT, '127.0.0.1', () => { console.log(`光湖知识库 API 已启动: http://localhost:${PORT}`); const runtime = getAgent().getRuntimeStatus(); console.log(`人格体运行状态: ${runtime?.operational ? '已接入模型' : '等待模型配置'}`); console.log(`仓库路径: ${REPO_PATH}`); }); } start().catch(err => { console.error('启动失败:', err); process.exit(1); }); export default app;