diff --git a/docs/pr-reports/2026-10-13-p3b-compile-ui.md b/docs/pr-reports/2026-10-13-p3b-compile-ui.md new file mode 100644 index 0000000..c634c48 --- /dev/null +++ b/docs/pr-reports/2026-10-13-p3b-compile-ui.md @@ -0,0 +1,69 @@ +# PR 报告:P3b 编译 PDF Tab(论文重排版面板) + +- 日期:2026-10-13 +- 分支:`feature/p3b-compile-ui`(自最新 main 新开;未 commit,按用户约定统一处理) +- 范围:`ui/` 面板 + store/api 扩展;`review_service/` 仅 restart.sh 调起预热脚本 + 新增 + scripts/prewarm_tectonic.sh。**后端 compile.py / latex_compile.py(P3a)零改动**; + 无 sdk/、agent/、vstore/ 改动;无新增依赖。 + +## 概要 + +P3b:论文重排版面板启用"编译 PDF" Tab,实现"上传 .tex → 服务器编译 → 下载 PDF"的完整闭环。 + +## 修改内容 + +| 文件 | 变更 | +|---|---| +| ui/src/components/edu/panels/ReformatPanel.vue | 编译 Tab 状态机:无 job_id 引导 / 编译按钮 / loading 动画 + 首次 1-3 分钟黄条(可关闭)/ 成功 blob URL iframe 预览 + 下载 + 耗时 + 重新编译 / 失败错误摘要 + log 尾 200 行折叠 + 完整 log 下载;按钮不 await,状态全由 store 驱动(切 Tab/切面板编译继续) | +| ui/src/stores/edu.js | compileStatus/compileResult/compileError/compileLogTail + compilePdfUrl(blob URL,fetch 失败静默回退 pdf_url 直链)+ compileFirstHint;submitCompile(loading 重入保护)/ clearCompile(revoke blob);重新排版自动 clearCompile | +| ui/src/api/review.js | CODE_HINTS 增 QUEUE_FULL 固定文案"编译队列已满(最多 5 个任务),请稍后重试"(后端 message 带动态 pending 数,规范统一口径);compileTex() 为 P3a 已有封装 | +| review_service/restart.sh | 启动后后台调起 prewarm_tectonic.sh(nohup 非阻塞,日志 prewarm.log) | +| review_service/scripts/prewarm_tectonic.sh(新增) | 最小 hello.tex 实编译以缓存 TeX 环境。**注**:tectonic v0.17.0 无 `download` 子命令(实测 `no subcommand download`),改用 tools/README 的最小 .tex 预编译法;失败仅告警不影响服务启动 | +| tests/frontend/unit/edu/reformat.compile.test.js(新增) | 6 个用例(真实 reviewErrorMessage 经 importOriginal 保留,QUEUE_FULL 走 CODE_HINTS) | + +## 关键实现决策 + +1. **无异步轮询**:后端编译同步阻塞返回(单槽队列,常规 10-30s,上限 `compile_queue_limit=5`), + 无 stage 可显示 → loading 只做不确定时长旋转动画 + 文案说明。 +2. **QUEUE_FULL**:HTTP 429 + 同款信封,http.js 抛 ApiError(code=QUEUE_FULL); + 前端 CODE_HINTS 固定文案,报错后"重新编译"按钮立即可用(store finally 解锁)。 +3. **PDF blob URL**:成功后异步 `fetch(pdf_url) → blob → createObjectURL`, + 预览/下载不暴露后端路径;jsdom/网络失败回退 pdf_url 直链,不阻塞渲染。 +4. **失败 log**:信封 detail = log 尾 200 行(折叠 `
`);完整 log 下载兜底拼
+   `/api/review/compile/{job_id}/log`(失败信封不带 log_url,但 job_id 即 anchor_id)。
+5. **首次编译提示条**:`compileFirstHint` 在首次 submitCompile 置位,loading 期间显示,
+   可手动关闭;`clearCompile`(重新排版)复位。
+6. **预热命令修正**:任务原文 `./tools/tectonic -X download default` 在钉死版本
+   v0.17.0 不存在——实测后改为最小 .tex 实编译预热(同 tools/README.md 建议)。
+
+## 测试结果
+
+- 后端 pytest(review_service,后端未动):**113 passed**
+- 前端 vitest 全量:**403 passed / 5 failed**(5F = 既有 labels.js 基线失败
+  benchpanel×4 + Composer pill×1,与本 PR 无关)
+- vitest 新增 reformat.compile.test.js:**6/6 passed**
+  (无 job_id 引导 / 按钮可点 / 编译中+黄条+重入保护 / 成功 iframe+下载+耗时 /
+   失败 log 折叠 / QUEUE_FULL 文案+重试可用)
+- npm run build:✓ 3.12s
+
+## 手工冒烟(8100 真实链路)
+
+- reformat /tmp/opencode/reformat_sample.tex → ok=True job_id=b3631727af68
+- POST /compile {job_id=b3631727af68} → ok=True seconds=0.61 pdf_bytes=3596
+- GET /compile/{cid}/pdf → 200,3596 bytes,application/pdf(file: PDF 1.5)
+- GET /compile/{cid}/log → 200,3533 bytes
+- job_id 不存在 / 格式非法 → VALIDATION 信封
+- 坏 tex(\thisCommandDoesNotExist)→ COMPILE_FAILED + detail 尾 200 行
+  (含 "! Undefined control sequence")
+- QUEUE_FULL → 429 + 信封(后端路径实测于 P3a;前端文案 vitest 6/6 覆盖)
+- scripts/prewarm_tectonic.sh → exit=0 OK,宏包缓存就绪
+
+## 部署验证(部署后执行)
+
+```bash
+bash review_service/restart.sh
+# 观察日志:prewarm_tectonic.sh 后台跑(非阻塞),prewarm.log 出现 "[prewarm] OK"
+# 浏览器 → 教育科研 → 论文自动重排版 → 上传 .tex → 选期刊 → 重排版
+#        → 切"编译 PDF" → 点编译
+# 期望:首次 1-3 分钟(若预热已完成则秒级)、后续秒回;失败可见 log 尾 200 行
+```