Files
cat-shark 37b75c2d21 Initial commit: 查词学英语 MVP
- 流式查询(SSE 实时展示生成过程)
- 三维度数据模型:history(查询)/ words(单词档案)/ roots(词根档案)
- 词根拆解与发音拼读训练(词根点击揭晓历史词例)
- 词义/同义例句跨查询累积,同义项自动合并
- 历史与词库分页、多维度搜索
- 模型:Qwen3.6-35B-A3B(硅基流动,可 LLM_MODEL 切换)
2026-08-29 08:44:43 +08:00

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}`);
});