- 流式查询(SSE 实时展示生成过程) - 三维度数据模型:history(查询)/ words(单词档案)/ roots(词根档案) - 词根拆解与发音拼读训练(词根点击揭晓历史词例) - 词义/同义例句跨查询累积,同义项自动合并 - 历史与词库分页、多维度搜索 - 模型:Qwen3.6-35B-A3B(硅基流动,可 LLM_MODEL 切换)
171 lines
5.3 KiB
JavaScript
171 lines
5.3 KiB
JavaScript
import express from 'express';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import {
|
|
saveRecord,
|
|
listRecords,
|
|
getRecord,
|
|
updateRecord,
|
|
deleteRecord,
|
|
getWord,
|
|
listWords,
|
|
upsertWordData,
|
|
getRoot,
|
|
} from './db.js';
|
|
import { queryWords, queryWordsStream } from './llm.js';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const app = express();
|
|
app.use(express.json({ limit: '2mb' }));
|
|
app.use(express.static(path.join(__dirname, 'public')));
|
|
|
|
// 发起查询
|
|
app.post('/api/query', async (req, res) => {
|
|
const sentence = (req.body.sentence || '').trim();
|
|
const words = Array.isArray(req.body.words)
|
|
? req.body.words.map((w) => String(w).trim()).filter(Boolean)
|
|
: [];
|
|
|
|
if (!sentence) return res.status(400).json({ error: '请提供英语句子' });
|
|
if (words.length === 0)
|
|
return res.status(400).json({ error: '请提供至少一个要查询的单词' });
|
|
if (words.length > 5)
|
|
return res.status(400).json({ error: '一次最多查询 5 个单词' });
|
|
|
|
try {
|
|
const { raw, parsed } = await queryWords(sentence, words);
|
|
const record = saveRecord({
|
|
sentence,
|
|
words,
|
|
result: parsed,
|
|
raw_result: raw,
|
|
});
|
|
res.json(record);
|
|
} catch (err) {
|
|
console.error('查询失败:', err.message);
|
|
res.status(502).json({ error: `查询失败:${err.message}` });
|
|
}
|
|
});
|
|
|
|
|
|
// 流式查询:SSE 增量推送 LLM 原始输出,完成后推送已保存的记录
|
|
app.post('/api/query/stream', async (req, res) => {
|
|
const sentence = (req.body.sentence || '').trim();
|
|
const words = Array.isArray(req.body.words)
|
|
? req.body.words.map((w) => String(w).trim()).filter(Boolean)
|
|
: [];
|
|
|
|
// 校验失败直接返回普通 JSON 错误
|
|
if (!sentence) return res.status(400).json({ error: '请提供英语句子' });
|
|
if (words.length === 0)
|
|
return res.status(400).json({ error: '请提供至少一个要查询的单词' });
|
|
if (words.length > 5)
|
|
return res.status(400).json({ error: '一次最多查询 5 个单词' });
|
|
|
|
res.set({
|
|
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
'Cache-Control': 'no-cache',
|
|
Connection: 'keep-alive',
|
|
'X-Accel-Buffering': 'no',
|
|
});
|
|
res.flushHeaders();
|
|
|
|
const send = (obj) => res.write(`data: ${JSON.stringify(obj)}\n\n`);
|
|
|
|
try {
|
|
// 查询已在词库中的单词:把已有释义带给 LLM,用于同义项合并
|
|
const existingWords = words
|
|
.map((w) => getWord(w))
|
|
.filter(Boolean)
|
|
.map((r) => ({ word: r.word, senses: r.senses.map((s) => s.sense), roots: r.roots }));
|
|
|
|
const { raw, parsed } = await queryWordsStream(sentence, words, (delta) => {
|
|
send({ type: 'delta', text: delta });
|
|
}, existingWords);
|
|
|
|
// 按单词维度并入词库(同义项合并、原句计入该单词记录)
|
|
for (const w of parsed.words || []) {
|
|
try {
|
|
upsertWordData(w, sentence, parsed.sentence_translation || '');
|
|
} catch (e) {
|
|
console.error('词库更新失败:', e.message);
|
|
}
|
|
}
|
|
|
|
const record = saveRecord({
|
|
sentence,
|
|
words,
|
|
result: parsed,
|
|
raw_result: raw,
|
|
});
|
|
send({ type: 'done', record });
|
|
} catch (err) {
|
|
console.error('流式查询失败:', err.message);
|
|
send({ type: 'error', message: `查询失败:${err.message}` });
|
|
} finally {
|
|
res.end();
|
|
}
|
|
});
|
|
// 词库列表
|
|
app.get('/api/words', (req, res) => {
|
|
const { page, size } = pagerParams(req);
|
|
res.json(listWords(page, size, req.query.q || ''));
|
|
});
|
|
|
|
// 单词详情(含全部释义与例句)
|
|
app.get('/api/words/:word', (req, res) => {
|
|
const record = getWord(req.params.word.toLowerCase());
|
|
if (!record) return res.status(404).json({ error: '词库中不存在该单词' });
|
|
res.json(record);
|
|
});
|
|
|
|
// 词根详情(该词根下的所有学过的词,按含义变体并列)
|
|
app.get('/api/roots/:root', (req, res) => {
|
|
const record = getRoot(req.params.root.toLowerCase());
|
|
if (!record) return res.status(404).json({ error: '词根库中不存在' });
|
|
res.json(record);
|
|
});
|
|
|
|
// 历史列表
|
|
app.get('/api/history', (req, res) => {
|
|
const { page, size } = pagerParams(req);
|
|
res.json(listRecords(page, size, req.query.q || ''));
|
|
});
|
|
|
|
// 历史详情
|
|
app.get('/api/history/:id', (req, res) => {
|
|
const record = getRecord(req.params.id);
|
|
if (!record) return res.status(404).json({ error: '记录不存在' });
|
|
res.json(record);
|
|
});
|
|
|
|
// 编辑保存
|
|
app.put('/api/history/:id', (req, res) => {
|
|
const { sentence, words, result } = req.body;
|
|
if (!sentence || !Array.isArray(words) || !result) {
|
|
return res.status(400).json({ error: '字段不完整' });
|
|
}
|
|
const record = updateRecord(req.params.id, { sentence, words, result });
|
|
if (!record) return res.status(404).json({ error: '记录不存在' });
|
|
res.json(record);
|
|
});
|
|
|
|
// 删除
|
|
app.delete('/api/history/:id', (req, res) => {
|
|
const ok = deleteRecord(req.params.id);
|
|
if (!ok) return res.status(404).json({ error: '记录不存在' });
|
|
res.json({ ok: true });
|
|
});
|
|
|
|
// 分页参数解析(默认第 1 页、每页 10 条,上限 50)
|
|
function pagerParams(req) {
|
|
const page = Math.max(1, parseInt(req.query.page) || 1);
|
|
const size = Math.min(50, Math.max(1, parseInt(req.query.size) || 10));
|
|
return { page, size };
|
|
}
|
|
|
|
const PORT = process.env.PORT || 3000;
|
|
app.listen(PORT, () => {
|
|
console.log(`✅ 服务已启动: http://localhost:${PORT}`);
|
|
});
|