2026-08-08 07:09:54 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 光湖知识库 · API 服务器
|
|
|
|
|
|
*
|
|
|
|
|
|
* Express + Git 引擎,提供知识库全部 REST API。
|
|
|
|
|
|
* Agent 直接调用这些接口,不经过任何第三方服务。
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
import express from 'express';
|
|
|
|
|
|
import cors from 'cors';
|
|
|
|
|
|
import { GitEngine } from './git-engine.js';
|
2026-08-08 07:38:09 +08:00
|
|
|
|
import { PersonaAgent, createDefaultPersona } from './persona-agent.js';
|
2026-08-08 07:09:54 +08:00
|
|
|
|
import path from 'path';
|
|
|
|
|
|
import { fileURLToPath } from 'url';
|
|
|
|
|
|
|
2026-08-08 09:46:43 +08:00
|
|
|
|
// __dirname 兼容 CJS 和 ESM
|
|
|
|
|
|
const _dirname = typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url));
|
2026-08-08 07:09:54 +08:00
|
|
|
|
|
|
|
|
|
|
// ─── 配置 ───
|
|
|
|
|
|
|
|
|
|
|
|
const PORT = parseInt(process.env.KB_PORT || '3890', 10);
|
2026-08-08 09:46:43 +08:00
|
|
|
|
const REPO_PATH = process.env.KB_REPO_PATH || path.resolve(_dirname, '../kb-data');
|
2026-08-08 07:09:54 +08:00
|
|
|
|
|
|
|
|
|
|
// ─── 初始化 ───
|
|
|
|
|
|
|
|
|
|
|
|
const app = express();
|
|
|
|
|
|
const engine = new GitEngine(REPO_PATH);
|
|
|
|
|
|
|
|
|
|
|
|
app.use(cors());
|
|
|
|
|
|
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 });
|
|
|
|
|
|
} 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 });
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-08-08 07:38:09 +08:00
|
|
|
|
// ─── Agent(人格体) ───
|
|
|
|
|
|
|
|
|
|
|
|
let personaAgent: PersonaAgent | null = null;
|
|
|
|
|
|
|
|
|
|
|
|
function getAgent(): PersonaAgent {
|
|
|
|
|
|
if (!personaAgent) {
|
|
|
|
|
|
personaAgent = createDefaultPersona(engine);
|
|
|
|
|
|
console.log('人格体已创建:', personaAgent.getDefinition().name);
|
|
|
|
|
|
}
|
|
|
|
|
|
return personaAgent;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Agent 状态
|
|
|
|
|
|
app.get('/api/agent/status', (_req, res) => {
|
|
|
|
|
|
const agent = getAgent();
|
|
|
|
|
|
const def = agent.getDefinition();
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
ok: true,
|
|
|
|
|
|
persona: {
|
|
|
|
|
|
id: def.id,
|
|
|
|
|
|
name: def.name,
|
|
|
|
|
|
role: def.role,
|
|
|
|
|
|
model: def.model,
|
|
|
|
|
|
permissionMode: def.permissionMode,
|
|
|
|
|
|
},
|
|
|
|
|
|
conversationLength: agent.getConversation().length,
|
|
|
|
|
|
engine: 'git',
|
|
|
|
|
|
repo: REPO_PATH,
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 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 agent = getAgent();
|
|
|
|
|
|
const reply = await agent.chat(message);
|
|
|
|
|
|
res.json({
|
|
|
|
|
|
ok: true,
|
|
|
|
|
|
reply,
|
|
|
|
|
|
conversationLength: agent.getConversation().length,
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
|
res.status(500).json({ ok: false, error: err.message });
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Agent 对话历史
|
|
|
|
|
|
app.get('/api/agent/conversation', (_req, res) => {
|
|
|
|
|
|
const agent = getAgent();
|
|
|
|
|
|
res.json({ ok: true, messages: agent.getConversation() });
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 清空对话
|
|
|
|
|
|
app.post('/api/agent/clear', (_req, res) => {
|
|
|
|
|
|
const agent = getAgent();
|
|
|
|
|
|
agent.clearConversation();
|
|
|
|
|
|
res.json({ ok: true, message: '对话已清空' });
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-08-08 07:09:54 +08:00
|
|
|
|
// ─── 启动 ───
|
|
|
|
|
|
|
|
|
|
|
|
async function start() {
|
|
|
|
|
|
await engine.init();
|
2026-08-08 07:38:09 +08:00
|
|
|
|
// 初始化人格体
|
|
|
|
|
|
getAgent();
|
2026-08-08 07:09:54 +08:00
|
|
|
|
app.listen(PORT, () => {
|
|
|
|
|
|
console.log(`光湖知识库 API 已启动: http://localhost:${PORT}`);
|
2026-08-08 07:38:09 +08:00
|
|
|
|
console.log(`人格体已激活: ${personaAgent?.getDefinition().name}`);
|
2026-08-08 07:09:54 +08:00
|
|
|
|
console.log(`仓库路径: ${REPO_PATH}`);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
start().catch(err => {
|
|
|
|
|
|
console.error('启动失败:', err);
|
|
|
|
|
|
process.exit(1);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
export default app;
|
|
|
|
|
|
|