133 lines
4.2 KiB
JavaScript
133 lines
4.2 KiB
JavaScript
// api/server.js
|
|
const express = require('express');
|
|
const { createProxyMiddleware } = require('http-proxy-middleware');
|
|
const { search } = require('./search/indexer');
|
|
const { streamChat } = require('./llm/deepseek');
|
|
const { addFeedback, getStats, getRecent } = require('./feedback/store');
|
|
|
|
const API_PORT = parseInt(process.env.API_PORT || '3000', 10);
|
|
const SERVE_PORT = parseInt(process.env.SERVE_PORT || '3001', 10);
|
|
const ADMIN_TOKEN = process.env.ADMIN_TOKEN || 'gitlink_admin_2024';
|
|
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();
|
|
});
|
|
|
|
// Feedback: submit
|
|
app.post('/api/feedback', (req, res) => {
|
|
const { docPath, vote, suggestion } = req.body;
|
|
if (!docPath || !vote || !['yes', 'no'].includes(vote)) {
|
|
return res.status(400).json({ error: 'docPath and vote (yes/no) are required' });
|
|
}
|
|
const entry = addFeedback({ docPath, vote, suggestion });
|
|
res.json({ success: true, id: entry.id });
|
|
});
|
|
|
|
// Feedback: stats (admin)
|
|
app.get('/api/feedback/stats', (req, res) => {
|
|
const token = req.query.token || req.headers['x-admin-token'];
|
|
if (token !== ADMIN_TOKEN) {
|
|
return res.status(403).json({ error: 'unauthorized' });
|
|
}
|
|
res.json(getStats());
|
|
});
|
|
|
|
// Feedback: recent entries (admin)
|
|
app.get('/api/feedback/recent', (req, res) => {
|
|
const token = req.query.token || req.headers['x-admin-token'];
|
|
if (token !== ADMIN_TOKEN) {
|
|
return res.status(403).json({ error: 'unauthorized' });
|
|
}
|
|
const limit = Math.min(parseInt(req.query.limit || '50', 10), 200);
|
|
res.json(getRecent(limit));
|
|
});
|
|
|
|
// 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}`);
|
|
});
|