词汇站 kaoyan-vocab · 单词记忆与复习算法
一句话:词汇站是一个 Next.js 14 的单词记忆应用,跑在
127.0.0.1:3001(Next 实际监听::3001),basePath 为/vocab,由门户反代到https://cag.quest/vocab/;它自己不做登录,是一个「全局单用户」的词库。
一、定位与入口
| 项 | 实测值 |
|---|---|
| 公网地址 | https://cag.quest/vocab/(经 portal 3000 反代) |
| 监听端口 | 3001(实测 Get-NetTCPConnection 显示 :: 3001,PID 21496) |
| 进程命令 | node node_modules\next\dist\bin\next start -p 3001 |
| basePath | /vocab(next.config.js) |
| 计划任务 | KaoyanNextServer,Running;动作 ops\launch.ps1 -Name vocab |
| 框架 | Next.js 14.2.15 App Router、React 18、Prisma 5.20、SQLite |
| 数据库 | kaoyan-vocab/prisma/dev.db(实测 16 张表,528 个单词) |
| 样式 | Tailwind CSS,暗色模式用 html.dark 类切换 |
next.config.js 同时开启 trailingSlash: true 和 serverComponentsExternalPackages: ["@prisma/client"]。前者意味着所有页面和 API 都要带尾斜杠;后者避免 Prisma 引擎被打包进 server bundle。
二、技术栈与目录结构
kaoyan-vocab/
app/
page.tsx 首页:搜索框 + 统计卡 + 白皮书知识库入口
layout.tsx 全局布局、主题脚本、导航、工具栏
word/[word]/ 单词详情(主版本正文 + 版本/收藏/掌握度)
review/ 复习卡片页
history/ 搜索历史
roots/ 词根词缀库
confusables/ 易混词组
associate/ 联想气泡
associate/[id]/ 联想结果详情
api/
search/ POST 检索
words/[word]/ 单词详情
words/[word]/favorite
words/[word]/mastery 掌握度 0-5
words/[word]/regenerate
review/ 待复习列表
random/ 随机复习一个词
history/ stats/ roots/ confusables/ compare/ notes/ association/
components/ SearchBox、Toolbar、WordClickProvider、MarkdownView 等
lib/
db.ts Prisma 单例
session.ts 仅生成 localStorage 匿名 sessionId
deepseek.ts AI 调用
generate.ts AI 生成单词材料
formats.ts 生成格式模板
whitebook.ts / whitebook-parse.ts / whitebook-updater.ts
ratelimit.ts 进程内限流
prisma/
schema.prisma 16 个模型
seed.ts 从 whitebook.md 3.1–3.7 灌库
dev.db
目录里没有 middleware.ts,也没有任何鉴权模块。lib/session.ts 的注释写得很清楚:它生成的是「客户端匿名会话 ID,用于搜索历史归组」,与门户的 pf_sid 毫无关系。
三、数据模型
dev.db 实测 16 张表。核心模型如下:
| 表 | 用途 | 关键字段 | 关系 |
|---|---|---|---|
Word | 单词主表 | word 唯一、normalized 唯一、phonetic、coreMeaning、mastery(0-5)、favorite、inWhitebook、frequencyRank | 一对多 WordSense、WordVersion、WordRelation、WordMorpheme |
WordSense | 义项 / 熟词僻义 | meaningZh、label、isRare、prosody、order | wordId → Word,级联删除 |
WordVersion | 单词内容的版本 | formatType(standard/brief/root/confuse/example/translation/custom)、contentMarkdown、isPrimary、model、tokens | 多对一 Word;一个词只有一个 isPrimary |
Morpheme | 词根 / 前缀 / 后缀 | slug 唯一、form、type、meaning | 多对多 Word,经 WordMorpheme 带 slot |
WordMorpheme | 词与词根关联 | slot(prefix/root/suffix)、note | 复合唯一 (wordId, morphemeId, slot) |
ConfusableGroup | 易混词组 | title 唯一、keyPoint、category | 一对多 ConfusableMember |
ConfusableMember | 组内成员 | wordText、normalized、distinction、order | groupId → 组 |
PronunciationItem | 音标辨析 | kind(symbol/word)、symbol、ipa、note | 唯一 (kind, symbol) |
TranslationNote | 翻译 / 语义支线 | keyText、body | 唯一 (keyText, body) |
WordRelation | 词间关系 | relationType(同根/易混/反义/搭配/相关)、toWordText | 自关联 Word |
SearchHistory | 搜索流水 | wordText、normalized、hit、sessionId | 可关联 Word |
Note | 工具栏临时笔记 | content、pinned、wordText | 可关联 Word |
CompareItem | 对比工具收藏 | normalized 唯一、order | 可关联 Word |
Association / AssociationItem | AI 联想批次与条目 | seedWords、similarity、reason | 一对多 |
实测行数:Word 528、WordSense 145、Morpheme 23、WordMorpheme 85、ConfusableGroup 52、ConfusableMember 390、PronunciationItem 20、TranslationNote 19、WordVersion 41、SearchHistory 39、Association 13 / AssociationItem 138。词库规模不大,属于个人精读型,不是背单词大厂的全量词表。
四、请求链路
- 浏览器访问
https://cag.quest/vocab/。 - Cloudflare →
cloudflared→127.0.0.1:3000。 - portal 命中
/vocab前缀,proxy(req,res,3001,...),原样转发,只把裸/vocab补成/vocab/。 - Next 以
basePath=/vocab匹配页面,服务端组件直接prisma查 SQLite,渲染 HTML。 - 浏览器里的客户端组件发
fetch("/api/search")。注意这是绝对根路径,不是/vocab/api/search。它必须再经过 portal:portal 的/api/*规则把它重写成/vocab/api/*再转给 3001。 - 因此词汇站不能脱离门户直接使用:直连
http://127.0.0.1:3001/vocab/虽然能打开页面,但页面里的/api/*请求会 404。这是 basePath 与前端绝对路径共同造成的耦合。
五、关键机制
1)所谓「复习算法」其实是掌握度队列,不是间隔重复。
app/api/review/route.ts 的完整筛选条件是:
where: { mastery: { lt: 4 } }
orderBy: { updatedAt: "desc" }
take: 100
没有 dueAt、没有 interval、没有 easeFactor、没有 SM-2 / FSRS 的遗忘曲线。前端 app/review/page.tsx 的行为是:
- 点「不会」→
POST /api/words/:word/mastery,值0; - 点「模糊」→ 值
3; - 点「已掌握」→ 值
5; mastery >= 4的卡片从当前队列移除并计入「本轮已掌握」;< 4的卡片被splice出来push到队尾,本轮再来一遍;- 全部过完或用户点「重新加载列表」时,重新拉一次
mastery < 4的词。
所以它是一个「按最近更新倒序、最多 100 张、答对即出队、答错排队尾」的 Leitner 式简化队列。优点是实现极简、用户理解成本低;缺点是同一批词每次顺序固定、没有时间维度、无法表达「三天后再复习」。文中必须明确:这不是间隔重复算法,只是掌握度驱动的循环队列。
2)掌握度是唯一记忆状态。 Word.mastery 是 0-5 的整数,只由用户点按钮写入,没有自动衰减。/api/stats 把 mastery < 4 的数量直接当作「待复习」。这意味着系统的「记忆模型」实际上是一个手工标签,而不是可计算的记忆强度。
3)内容生成与版本。 搜索一个不存在的词会走 deepseek 生成,结果写入 WordVersion,isPrimary=true 的那一版作为详情页主内容。一个词可以有多版(标准/简版/词根版/易混版/例句版/翻译版/自定义),旧版永不覆盖。生成是同步请求,受 lib/ratelimit.ts 的进程内限流约束。
4)白皮书驱动的知识库。 prisma/seed.ts 解析项目根 whitebook.md 的第 3.1–3.7 节,灌入 Morpheme、WordSense、ConfusableGroup、PronunciationItem、TranslationNote。首页的「白皮书知识库」四张卡直接读这些表的计数。更新白皮书后要手动 npm run db:seed,没有增量同步机制。
5)与门户的鉴权衔接:几乎没有。 实测对 app/ 与 lib/ 全量搜索 x-portal-secret、x-user-、getViewer,零命中。词汇站不读门户注入的身份头,也不校验共享密钥。门户对它只是「网络层入口」:所有公网流量必须经过 3000,但站内没有「谁在访问」的概念。任何能访问 /vocab/ 的人都能改掌握度、收藏、生成内容。这是当前架构里权限最弱的一站。
六、发布与运维
标准流程:
cd C:\Users\cag\WorkBuddy\2026-10-11-06-00-16\kaoyan-vocab
npm run build
Stop-ScheduledTask -TaskName KaoyanNextServer
Start-ScheduledTask -TaskName KaoyanNextServer
launch.ps1 -Name vocab 会先杀掉 3001 的监听进程,再以隐藏窗口执行 next start -p 3001,日志写 next-server.log 与 .err。手工调试可以用 start-next.bat,它同样先 taskkill 占用 3001 的旧进程——脚本注释里明确写了「3000 留给 portal」。
常见故障:
- 页面能开但所有操作报 404 / 405:八成是绕过了门户直连 3001,
/api/*没有被重写。改用http://127.0.0.1:3000/vocab/。 ChunkLoadError或静态资源 404:.next构建产物与当前进程不匹配,重新next build后重启任务。- Prisma 报无法打开数据库:确认
DATABASE_URL="file:./dev.db"是相对prisma/解析的,实际文件在prisma/dev.db。 - 端口被占:
launch.ps1会处理;若是 SYSTEM 孤儿进程,需要管理员权限。
七、现状与改进点
结构债一:没有用户维度。 全站共享一份 mastery、favorite、Note、CompareItem。多人使用会互相覆盖,这与门户已经有的 5 个账号完全脱节。
结构债二:复习模型名不副实。 如果产品目标是「抗遗忘」,需要引入 dueAt、intervalDays、ease 或至少 lastReviewedAt,并按到期日取词;当前只按 updatedAt 倒序,复习顺序会随任何写操作漂移。
结构债三:前端绝对路径依赖门户。 fetch("/api/...") 与 basePath 不一致,是历史兼容写法。要独立部署,应统一改成 /vocab/api/... 或走 lib/api.ts 常量。
风险:写接口无鉴权。 公网任何人可清空/覆盖掌握度,也可触发 AI 生成消耗 token。至少应校验 x-portal-secret 并要求登录,写操作再加一层 viewer 判断。
建议:先做「最小身份接入」——读 X-User-*,给 Word 增加 ownerId,把掌握度与收藏按用户隔离;再把复习队列升级为「到期优先 + 新词配额」;最后把前端 API 前缀统一,去掉对门户 /api/* 重写的隐式依赖。