# 📘 LLM 论文图书馆 — 代码全景 & 维护手册
> 最后更新:2026-06-09 | 馆长必读。每次改代码前先看这里。
> 本文档是代码的"说明书"——解释每个文件做什么、怎么改、踩过什么坑。
---
## 🗺️ 整体架构
```
用户浏览器 → Nginx (443, OpenResty) → Docker (127.0.0.1:8741→8000)
└── FastAPI (uvicorn)
├── /api/* → REST API
├── /papers/* → PDF 本地代理
└── / → 静态前端
```
**数据流:** `papers.json` 是唯一数据源。前端通过 `/api/modules/{id}` 拉取完整模块数据(含论文列表),在浏览器端渲染。
---
## 📁 文件清单 & 职责
### 后端 — `api/`
| 文件 | 行数 | 职责 | 改这里时注意 |
|------|------|------|-------------|
| `server.py` | ~600 | FastAPI 主程序:路由、鉴权、PDF 代理、翻译触发 | **核心文件。** 所有 API 在此。修改路由/鉴权/PDF 逻辑都在这里 |
| `downloader.py` | ~200 | 批量下载 arXiv + HuggingFace PDF 到本地缓存 | 调用 `httpx`,有重试机制。下载到 `papers/arxiv/` 和 `papers/hf/` |
| `backfill.py` | ~50 | 快速补全缺失的 arXiv PDF | 比 downloader 轻量,用 `wget`。适合 cron 定期跑 |
| `batch_translate.py` | ~70 | 用 pdf2zh + DeepSeek 批量翻译 | 从 `papers.json` 读取论文,跳过已有译文。输出到 `papers/translated/` |
| `parse_papers.py` | ~110 | 从 `llm_library.html` 解析论文数据 → `papers.json` | 旧版工具,用于从原始 HTML 模板提取数据 |
| `extract_data.py` | ~105 | 备用提取脚本(与 parse_papers 功能相似) | 路径硬编码了 `/app/working/workspaces/default/` |
| `check_trans.py` | ~15 | 测试翻译 API 连通性 | 仅开发调试用 |
### 前端 — `static/`
| 文件 | 行数 | 职责 | 改这里时注意 |
|------|------|------|-------------|
| `index.html` | ~330 | HTML 壳(header + search + grid + overlay modal) | **注意:** `index_template.html` 和它内容相同。修改时两个都要同步?→ 实际上只有 `index.html` 被服务。`index_template.html` 是旧版模板 |
| `app.js` | ~285 | 全部前端逻辑 | ES6 模块风格(但不是 module)。**无框架依赖**,纯 vanilla JS。 |
| `style.css` | ~315 | 暗色主题 CSS | 9 种模块配色 + 卡片光效 + PDF 阅读器样式 |
| `pdf.min.js` | ~40K | PDF.js 库 | 外部依赖,不要手动改 |
### 数据 — `data/`
| 文件 | 说明 |
|------|------|
| `papers.json` | **唯一数据源。** 结构:`{ module_id: { name, icon, desc, areas: [{ id, name, mainline[], branches[], forward[] }] } }` |
### 配置 & 部署
| 文件 | 说明 |
|------|------|
| `Dockerfile` | Python 3.11-slim,安装 poppler-utils + torch CPU + pdf2zh |
| `start.sh` | 容器启动脚本:检查 API Key、安装依赖、启动 uvicorn |
| `requirements.txt` | FastAPI + uvicorn + httpx + pydantic + tqdm |
| `nginx.conf` | 参考配置(非容器内使用,宿主机 Nginx 反向代理) |
| `.env.example` | 环境变量模板 |
| `proxy_conf.txt` | 代理配置备注 |
---
## 🔌 API 路由速查
### 读操作(无需鉴权)
| 方法 | 路径 | 返回 | 备注 |
|------|------|------|------|
| GET | `/api/health` | `{"status":"ok"}` | 健康检查 |
| GET | `/api/stats` | 模块数/领域数/论文总数 | 首页统计 |
| GET | `/api/modules` | 模块列表(含 paper_count) | 用于渲染首页卡片 |
| GET | `/api/modules/{id}` | 完整模块数据(含所有论文) | 点击卡片后调用 |
| GET | `/api/papers?q=...&module=...&tag=...` | 搜索结果列表 | 搜索栏 / 标签过滤 |
| GET | `/papers/arxiv/{id}.pdf` | PDF 文件 | 本地缓存代理。也支持无 `.pdf` 后缀 |
| GET | `/papers/hf/{name}.pdf` | PDF 文件 | HuggingFace 缓存代理 |
| GET | `/papers/translated/{id}.pdf` | 翻译 PDF | 中文译文 |
| GET | `/api/translated/{id}` | `{"exists":bool}` | 检查译文是否存在 |
| GET | `/api/translate/{id}` | 段落级翻译 JSON | MyMemory 免费 API,文本翻译 |
| GET | `/api/translate/{id}/status` | 缓存状态 | 检查段落翻译缓存 |
| GET | `/api/translate/status` | 正在翻译的论文列表 | pdf2zh 翻译队列 |
### 写操作(需 API Key:Bearer 头 或 `?api_key=` 参数)
| 方法 | 路径 | 说明 |
|------|------|------|
| POST | `/api/papers` | 新增论文 |
| PUT | `/api/papers?module_id=...&area_id=...&title=...` | 修改论文 |
| DELETE | `/api/papers?module_id=...&area_id=...&title=...` | 删除论文 |
| POST | `/api/translate/{paper_id}` | 触发 pdf2zh 翻译 |
| POST | `/api/download/{arxiv_id}` | 按需下载单篇 PDF |
---
## 🧩 前端逻辑流程
```
1. init()
├── buildStatusBar() → 底部连通性检测条
├── fetch('/api/modules') → 获取模块列表
├── renderCards(mods) → 渲染 9 张卡片
├── attachGlowTracking() → 鼠标光效
└── checkSources() → ping arxiv + hf
2. 用户点击卡片
└── openModule(modId)
├── fetch('/api/modules/{id}') → 获取完整数据(缓存到 moduleData)
└── renderPapers(area)
├── section-label: 主线论文 / 支线论文 / 前瞻探索
└── paper-item: 年份 | 标题 | 作者 | venue | tags | 按钮
3. 用户点击「📄 阅读」
└── openPdfBtn(btn)
└── openPdf(url, title)
├── 创建/复用 #pdfOverlay
├── getLocalPdfUrl() → 优先走本地缓存 /papers/arxiv/{id}.pdf
└── iframe 加载 PDF,失败时回退到 arXiv 直链
4. 用户点击「📖 译文」
└── 同上,URL 指向 /papers/translated/{id}.pdf
```
---
## 🔒 安全机制
| 机制 | 位置 | 说明 |
|------|------|------|
| API Key 鉴权 | `server.py:verify_api_key()` | 所有 POST/PUT/DELETE 需 Bearer token 或 `?api_key=` |
| 路径遍历防护 | `server.py:safe_paper_id()` | 正则 `^[a-zA-Z0-9_.\-]+$`,禁止 `..` |
| PDF 路径校验 | `server.py:safe_pdf_path()` | resolve 后检查是否在 base 目录内 |
| CORS | 全局 `allow_origins=["*"]` | 当前宽松,如需收紧改这里 |
| 速率限制 | `nginx.conf` | 宿主机 Nginx 层 `limit_req zone=api:10m rate=10r/s` |
---
## ⚠️ 已知问题 & 注意事项
### 1. `index.html` vs `index_template.html` (2026-06-09 已修复)
**已修复:** `index.html` 原来包含 286 行内联 JS,导致 `app.js` 的修改完全不生效。已改为 ``,HTML 和 JS 完全分离。`index_template.html` 保持为早期模板(也引用外部 script),用于备用。
### 2. MyMemory 免费翻译 vs pdf2zh
`GET /api/translate/{arxiv_id}` 用的是 MyMemory 免费 API(质量一般),而真正的翻译是通过 `POST /api/translate/{paper_id}` 触发 pdf2zh + DeepSeek(生成中文 PDF)。两者是不同的翻译通道,不要混淆。
### 3. 按需下载实际跑全量
`POST /api/download/{arxiv_id}` 目前的实现是运行 `downloader.py` 下载**所有**未缓存的论文,而不是只下载请求的那一篇。这是个已知的粗糙实现。
### 4. 翻译覆盖率
翻译 PDF 存储在容器内 `/app/papers/translated/`,通过 volume 挂载持久化。翻译 API (`POST /api/translate/{paper_id}`) 会检查是否已翻译,避免重复。
### 5. 搜索实现(2026-06-09 已修复)
搜索已在 2026-06-09 升级为**实时下拉结果面板**。输入 ≥2 字符后,200ms debounce 后调用 `/api/papers?q=`,在下拉面板中显示匹配论文(标题高亮、模块/领域、年份、标签)。卡片同步绿框高亮。点击结果项跳转到对应模块弹窗。点外部或 Escape 关闭。
### 6. OpenResty 缓存问题
宿主机 Nginx/OpenResty(1Panel 管理,容器名 `1Panel-openresty-mHac`)可能缓存静态文件响应。已通过 FastAPI 中间件添加 `Cache-Control: no-cache, no-store, must-revalidate` 头来阻止。如果仍有缓存问题,运行:
```bash
docker restart 1Panel-openresty-mHac
```
另外,`app.js` 和 `style.css` 的引用使用了版本号查询参数(`?v=20260609`),修改后递增版本号即可绕过所有缓存。
`papers.json` 是唯一数据源。新增论文通过 API 写操作 → 自动更新 JSON → 前端下次请求时自动获取最新数据。**不要手动编辑 production 上的 papers.json 绕过 API**,可能导致并发写入问题。
---
## 🔄 修改代码后的部署流程
```
1. 本地修改代码(在 paper_librarian/llm-library/ 下)
2. git add . && git commit -m "..." && git push origin main
3. scp 到宿主机:scp -i hk_server_key -P 16844 root@103.112.185.16:/opt/llm-library/
4. docker cp 到容器:ssh ... "docker cp /opt/llm-library/ llm-library:/app/"
5. 重启容器:ssh ... "docker restart llm-library"
6. 用无头浏览器验证网站各功能正常
```
**注意:** API 文件(`server.py`)改动后需要重启容器。静态文件(`*.html, *.js, *.css`)也需要重启才能生效(因为被 FastAPI StaticFiles 挂载)。
---
## 🧪 无头浏览器验证清单
每次部署后,用 headless browser 验证以下功能:
- [ ] 首页加载:9 张模块卡片正常渲染
- [ ] 连通性检测条:arXiv 和 HF 状态显示正常
- [ ] 点击任意模块卡片 → 弹窗打开,tabs 切换正常
- [ ] 论文列表:主线/支线/前瞻 三个 section 显示正确
- [ ] 「📄 阅读」按钮:PDF iframe 能加载(至少一篇测试)
- [ ] 「📖 译文」按钮:翻译 PDF 能加载(如果存在)
- [ ] 搜索功能:输入关键词能返回结果
- [ ] Esc 键关闭弹窗 / PDF
- [ ] 底部栏链接:spdis.space 可点击
---
## 📊 性能基线
| 指标 | 当前值 | 备注 |
|------|--------|------|
| 论文总数 | ~189 | `papers.json` ~200KB |
| API 响应时间 | <50ms | 纯内存 JSON 读取 |
| PDF 代理延迟 | <100ms | 本地文件系统读取 |
| 翻译耗时 | 3-8 min/篇 | pdf2zh + DeepSeek,取决于论文长度 |
| 容器镜像 | ~4.3GB | 含 PyTorch CPU + ONNX 模型 |
---
## 🔧 常见维护操作
### 新增论文
```bash
curl -X POST https://llmlibrary.spdis.space/api/papers?api_key=KEY \
-H "Content-Type: application/json" \
-d '{"module_id":"arch","area_id":"attention","section":"mainline","title":"...","authors":"...","year":2026,"venue":"arXiv","arxiv":"2601.01234","tags":["前沿"]}'
```
### 修改论文标签
```bash
curl -X PUT "https://llmlibrary.spdis.space/api/papers?api_key=KEY&module_id=arch&area_id=attention&title=Attention Is All You Need" \
-H "Content-Type: application/json" \
-d '{"tags":["起点","关键节点"]}'
```
### 触发翻译
```bash
curl -X POST "https://llmlibrary.spdis.space/api/translate/1706.03762?api_key=KEY"
```
### 检查翻译状态
```bash
# 查看所有翻译 PDF 数量
ssh ... "docker exec llm-library ls /app/papers/translated/ | wc -l"
# 查看翻译队列
curl https://llmlibrary.spdis.space/api/translate/status
```