hololake-system-architecture/product-source/guanghu-knowledge-base/server/index.ts

462 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 光湖知识库 · 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 });
}
});
// ─── ForgejoGit 远端引擎) ───
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<string, ConversationRecord>();
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;