Improve Word parsing and harden NAS publishing#1
Merged
Conversation
新增写作题库创作能力,产出 NAS 端 NasJsDirectWritingAssetProvider 能识别的
writing-exams/manifest.js + task1.js + task2.js。
== Rust 后端(独立模型,不污染阅读 ImportJob)==
- writing_store.rs: WritingJob 模型 + CRUD(writing-jobs/<jobId>/writing-job.json)
WritingJobStatus(Draft/ExportReady/Exported)、writing_source() 转导出 payload
- export_writing_library.rs: 导出管线
build_writing_wrapper(__WRITING_EXAM_DATA__.register IIFE,key=taskType)
build_writing_manifest(window.__WRITING_EXAM_MANIFEST__ 赋值,key=taskType)
校验 task1+task2 齐全 + promptText 非空;输出到 {exportDir}/writing-exams/
- util.rs: writing_job_dir/safe_writing_job_dir + ensure_app_dirs 加 writing-jobs
- lib.rs: 6 个 #[tauri::command] + generate_handler 注册
(create/list/get/update/delete_writing_job + export_writing_library)
== React 前端 ==
- types/writing.ts: WritingJob/CreateWritingJobInput/WritingJobPatch/WritingJobFilter/
WritingExportResult/ExportWritingLibraryInput/WritingExamSourceV1(与 NAS 契约对齐)
- tauriCommands.ts: 6 个 API 方法
- devFallbackBackend.ts: 6 个 case + Store.writingJobs 内存存储
- WritingStudio.tsx: 创作页(手输 promptText + taskType 切换 + 状态流转)
- ExportPage.tsx: writing-library 导出模式(选 task1+task2 job → 导出)
- router.ts/App.tsx/AppShell.tsx/Dashboard.tsx: 写作路由 + 侧栏入口 + 工作台按钮
- styles.css: 补 writing-create-form/writing-edit-form/writing-prompt-textarea 等 8 类
== 导出契约(与 NAS 端逐字段核对,并发子代理联调验证)==
manifest: window.__WRITING_EXAM_MANIFEST__ = { task1:{taskType,examId,dataKey,script,title}, task2:{...} }
task: __WRITING_EXAM_DATA__.register(taskType, {schemaVersion:"WritingExamSourceV1",examId,taskType,promptText,suggestedWordCount,meta{title,taskType}})
== 验证 ==
- cargo check: 通过(仅既有未使用函数警告)
- tsc --noEmit: 通过
- vite build: 通过(49 模块)
- 联调:NAS E2E 注入 PDF2Test 格式产物,console 不再 404,writingSubmissions:2 正常
- 阅读链路零改动:ImportJob/job_store/export_nas_library/reading_source 一行未动
…pture
显著提升苛刻素材(双栏 PDF、仅 run 级格式 DOCX)的识别与切块准确率,
并确保打包后在无 Python 的目标电脑上全功能可用。
PDF 几何后端 (src-tauri/src/pdf_geometry.rs, 新模块):
- 用 pdfium-render 提取每字符真实坐标,按 y 聚类成行、x 聚类成词,
输出带真实 bbox 的 DocumentIRV1(不再伪造 [72,*,520,*])。
- 双栏 gutter 检测:x 直方图找近空带,正确分左右两列。
- pdfium 同时承担页面渲染(扫描件视觉救援),替代系统 Python+PyMuPDF。
- 库缺失时优雅回退到 pdf-extract 文本层。
- 替换重量级 image crate 为轻量 png crate + as_rgba_bytes()。
DOCX run 级格式捕获 (src-tauri/src/parser.rs):
- 扩展 quick_xml 状态机捕获 w:b/w:i/w:sz/w:jc/sectPr,输出 layoutHints.runFormat。
- infer_run_heading_level:无 Heading 样式时,加粗+短+无句号→子标题(hL3),
居中+加粗→passage 标题(hL2)。验证 The history of lighting→hL2,
Candlelight/Oil lamps/Gas lighting/Electricity→hL3。
栏检测与阅读顺序 (src-tauri/src/authoring_pipeline.rs):
- dynamic_document_blocks 不再丢弃 _epic8PageWidth,让栏检测读到真实页宽。
- 全宽标题行(x 跨度 >75% 页宽)判为 header,不混入栏排序。
打包 (无 Python 也能用):
- scripts/fetch-pdfium.mjs: 从 bblanchon/pdfium-binaries 下载原生库到 lib/pdfium-<platform>/。
- 平台化 tauri.{windows,macos,linux,offline}.conf.json 各只打包本平台库。
- package-audit.mjs: pdfium 白名单(仅 lib/pdfium-*/ 路径),Python/Node/tesseract 仍禁。
- package.json: verify:release:* 前置 fetch:pdfium。
- [profile.release] opt-level=z + LTO + strip:.exe 17.36MB→6.57MB,NSIS 包 5.07MB。
依赖瘦身:
- pdfium-render default-features=false(移除 image_latest 默认特性)
彻底移除 image crate 及 moxcms/zune-jpeg/bytemuck 等传递依赖。
- 改用 png = "0.17" (~50KB) 替代 image crate 编码 PNG。
验证:
- NSIS installer.nsi 第 651 行确认 pdfium.dll 部署到 lib/pdfium-windows/。
- 端用户模拟(无环境变量):release .exe 自动解析 bundled pdfium.dll,
用 rust-parser:pdf:pdfium 正确解析 PDF(3 题组分类正确)。
- 打包审计 exit 0。测试 120 pass / 4 pre-existing env failures,0 新增回归。
- 新增 fixtures: demanding-reading-passage-1.docx, demanding-reading-passage-3.pdf。
- 新增 2 回归测试均通过。
问题:打包后的应用启动时会同时弹出一个控制台终端窗口。 根因:main.rs 未声明 windows_subsystem,release 二进制按默认 console 子系统构建,启动 GUI 时 Windows 会为它分配并显示一个控制台窗口。 修复: - main.rs 添加 #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")], release 构建为 GUI 子系统,启动不再创建控制台窗口。debug 构建仍保留 console 子系统以便开发时查看 println!/eprintln! 输出。 - lib.rs run_cli_or_app 在进入 CLI 模式时调用 attach_parent_console: 通过 AttachConsole + SetStdHandle 重新绑定到父进程控制台,保证 `--generate-reading-source` 等 headless CLI 命令在 release 中仍有输出。 用 GetConsoleMode 检测:若 stdout/stderr 已是控制台或已重定向到文件/管道 则不重绑定,避免破坏 `app.exe ... > out.json` 的 shell 重定向。 纯 FFI 调用 kernel32(AttachConsole/GetStdHandle/SetStdHandle/GetConsoleMode), 未引入 windows-sys/winapi 依赖。 验证: - 打包 .exe subsystem=2 (GUI),NSIS 安装包 5.07 MB 不变。 - CLI 重定向:app.exe --generate-reading-source x.docx > out.json 正确输出 53KB JSON。 - GUI 启动(无参数):干净启动无控制台、无报错(仅 WebView2 常规 class 注销告警)。 - 测试 120 pass / 4 pre-existing env failures,0 新增回归。
后端题库基础设施:
- 新增 db.rs: SQLite 连接管理(WAL+busy_timeout)、exams 表 schema(含
CHECK 约束)、upsert/list/get/search/stats/delete/prune_orphan 查询
- 新增 library_commands.rs: 6 个题库 CRUD 命令 + 双写钩子 + 一次性迁移
(事务化,失败回滚不写标记,下次重试) + DB 孤儿行清理
- job_store/writing_store: 保存时双写 DB;题库元数据编辑回写源 JSON
(避免改了不生效);题库删除同步删源文件(避免双写复活)
- job_commands: 删除任务同步删 DB 行
- lib.rs: AppState 注册、setup 初始化+迁移、6 命令注册、Library* 类型
前端题库管理界面:
- 新增 LibraryPage(列表/筛选/搜索/统计/删除) + LibraryExamDetail
(详情/元数据编辑/payload 渲染,运行时类型守卫防 ImportJob fallback 崩溃)
- 新增 library.ts 类型
- router.ts: 新增 library/libraryExam 路由
- AppShell.tsx: 4 一级导航(工作台/转化工具可展开/题库管理/设置),
折叠态分组标题直达首子项
- Dashboard: 题库统计概览
- tauriCommands + devFallbackBackend: 6 命令封装 + mock
安全加固:
- 新增 sanitizeHtml.ts: DOMParser 白名单净化,应用于 passage HTML 渲染
(LibraryExamDetail/UnifiedPreview),防存储型 XSS
回归修复:
- WritingStudio 死路由 /export -> /packs
- 题库详情「从该题导出」改为带 jobId 上下文(阅读跳 /jobs/{id}/export)
- 删除 3 个废弃页面(GroupEditor/SplitAndAnswers/LlmReview)
- 错误信息脱敏、limit/offset 边界、dev mock Cleaned 语义保留
测试: db 模块 8 个 + library_commands 4 个回归测试(回写不被覆盖、
删除不复活、迁移 prune 孤儿),全部通过
githubSINGLE
marked this pull request as ready for review
July 15, 2026 17:03
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
Why
Customer testing exposed three connected problems: Word question extraction was too brittle for otherwise regular IELTS documents, export review gates were unnecessarily blocking unanswered questions, and NAS subset publishing could replace the existing student manifest or report success against a local placeholder directory.
Impact
Authors can move from Word import to student-ready NAS content with fewer manual corrections and a shorter publish path. Existing NAS questions are retained during subset updates, unsafe destinations are rejected, and students receive readable option labels plus option text.
Root causes
Validation
cargo test --lib: 205 passed, 0 failed, 3 ignorednpm run build: passednpm run e2e:ui-flow: 3 end-to-end flows passed, including one-click force publish and OCR/manual transcriptionnode --check scripts/generate-docx-corpus.mjs: passednode scripts/generate-docx-corpus.mjs --self-test: passedgit diff --check: passed