Skip to content

Improve Word parsing and harden NAS publishing#1

Merged
githubSINGLE merged 13 commits into
mainfrom
feat/writing-exam-authoring
Jul 16, 2026
Merged

Improve Word parsing and harden NAS publishing#1
githubSINGLE merged 13 commits into
mainfrom
feat/writing-exam-authoring

Conversation

@githubSINGLE

Copy link
Copy Markdown
Collaborator

What changed

  • Upgraded DOCX/IELTS recognition for native numbering, non-1 question starts, A-J and Roman option banks, tables, wrapped and inline choices, prompt boundaries, and passage-only sources.
  • Preserved option text through AuthoringIR and student rendering while keeping submission values stable.
  • Made missing answers optional and exposed one-click force publishing without requiring a failed strict attempt first.
  • Simplified the visible publish workflow around NAS export and removed Pack/batch-JS/full-export entry points.
  • Required a real NAS/shared directory, merged existing manifests by examId for GUI and CLI publishing, and added staging, rollback, and manifest-last commit safety.
  • Added a deterministic PDF-derived DOCX corpus generator with explicit/native numbering variants and self-tests.
  • Kept the browser fallback aligned on key numbering, passage-only, and terminal-boundary behavior.

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

  • OOXML numbering/style inheritance and mixed table/paragraph option layouts were only partially interpreted.
  • Inline option segmentation did not safely handle wrapped or multi-label cells.
  • NAS publishing built the manifest from only the current selection and wrote it directly.
  • Browser fallback rules had drifted from the Rust desktop path.

Validation

  • cargo test --lib: 205 passed, 0 failed, 3 ignored
  • npm run build: passed
  • npm run e2e:ui-flow: 3 end-to-end flows passed, including one-click force publish and OCR/manual transcription
  • node --check scripts/generate-docx-corpus.mjs: passed
  • node scripts/generate-docx-corpus.mjs --self-test: passed
  • 1,128 generated DOCX entries passed file/hash/ZIP/core-OOXML integrity checks
  • git diff --check: passed

新增写作题库创作能力,产出 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
githubSINGLE marked this pull request as ready for review July 15, 2026 17:03
@githubSINGLE
githubSINGLE merged commit ab137c1 into main Jul 16, 2026
1 of 2 checks passed
@githubSINGLE
githubSINGLE deleted the feat/writing-exam-authoring branch July 16, 2026 14:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants