feat: add Express API server with chat endpoint and static proxy

This commit is contained in:
z2_cc 2026-05-13 11:44:41 +08:00
parent 11c79472ce
commit cba879b7f3
3 changed files with 1437 additions and 296 deletions

101
api/server.js Normal file
View File

@ -0,0 +1,101 @@
// api/server.js
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const { search } = require('./search/indexer');
const { streamChat } = require('./llm/deepseek');
const API_PORT = parseInt(process.env.API_PORT || '3000', 10);
const SERVE_PORT = parseInt(process.env.SERVE_PORT || '3001', 10);
const MAX_HISTORY = 10;
const SYSTEM_PROMPT = `你是 GitLink 帮助中心的 AI 助手。请基于提供的文档内容回答用户的问题。
要求
1. 回答要简洁明了直接回答用户的问题
2. 如果提供的文档中没有相关内容请诚实说明
3. 回答时引用具体的操作步骤和页面位置`;
function buildMessages(query, history, searchResults) {
const messages = [{ role: 'system', content: SYSTEM_PROMPT }];
if (searchResults.length > 0) {
const context = searchResults.map((doc, i) =>
`[文档${i + 1}] 标题: ${doc.title}\n路径: ${doc.breadcrumb}\nURL: ${doc.url}\n内容:\n${doc.text}`
).join('\n\n---\n\n');
messages.push({
role: 'system',
content: `以下是相关的帮助中心文档内容:\n\n${context}`,
});
}
const recentHistory = history.slice(-MAX_HISTORY);
for (const msg of recentHistory) {
messages.push({ role: msg.role, content: msg.content });
}
messages.push({ role: 'user', content: query });
return messages;
}
const app = express();
app.use(express.json());
// Health check
app.get('/api/health', (req, res) => {
res.json({ status: 'ok' });
});
// Chat endpoint (SSE streaming)
app.post('/api/chat', async (req, res) => {
const { message, history = [] } = req.body;
if (!message || typeof message !== 'string') {
return res.status(400).json({ error: 'message is required' });
}
let searchResults = [];
try {
searchResults = search(message, 3);
} catch (err) {
console.error('Search error:', err.message);
}
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no');
if (searchResults.length > 0) {
const links = searchResults.map(doc => ({
title: doc.title,
path: encodeURI(doc.url),
breadcrumb: doc.breadcrumb,
}));
res.write(`data: ${JSON.stringify({ type: 'sources', links })}\n\n`);
}
try {
const messages = buildMessages(message, history, searchResults);
for await (const chunk of streamChat(messages)) {
res.write(`data: ${JSON.stringify({ type: 'content', text: chunk })}\n\n`);
}
res.write(`data: ${JSON.stringify({ type: 'done' })}\n\n`);
} catch (err) {
console.error('LLM error:', err.message);
res.write(`data: ${JSON.stringify({ type: 'error', message: 'AI 服务暂时不可用,请稍后重试' })}\n\n`);
}
res.end();
});
// Proxy all other requests to Docusaurus serve
app.use('/', createProxyMiddleware({
target: `http://localhost:${SERVE_PORT}`,
changeOrigin: true,
ws: true,
}));
app.listen(API_PORT, () => {
console.log(`API server listening on port ${API_PORT}, proxying to Docusaurus on port ${SERVE_PORT}`);
});

1625
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -11,7 +11,9 @@
"clear": "docusaurus clear",
"serve": "docusaurus serve",
"write-translations": "docusaurus write-translations",
"write-heading-ids": "docusaurus write-heading-ids"
"write-heading-ids": "docusaurus write-heading-ids",
"build:api": "node scripts/generate-doc-index.js",
"serve:api": "node api/server.js"
},
"dependencies": {
"@docusaurus/core": "3.9.2",
@ -21,6 +23,9 @@
"@node-rs/jieba": "^1.7.0",
"clsx": "1.2.1",
"docusaurus-plugin-image-zoom": "1.0.1",
"express": "^5.2.1",
"http-proxy-middleware": "^4.0.0",
"pm2": "^7.0.1",
"prism-react-renderer": "^1.3.5",
"prismjs": "^1.30.0",
"react": "18.2.0",