Skip to content

feat(ocr): 图片/扫描件可检索 + 评审阻断项修复(含端到端测试) - #4

Merged
FPSZ merged 5 commits into
FPSZ:collabfrom
wjs480:feat/ocr-hardening
Sep 16, 2026
Merged

FPSZ merged 5 commits into
FPSZ:collabfrom
wjs480:feat/ocr-hardening

Conversation

@wjs480

@wjs480 wjs480 commented Sep 14, 2026

Copy link
Copy Markdown

背景

原 PR 的 OCR 部分被评审打回("OCR 部分目前端到端不工作")。本 PR 保留 OCR 功能提交,逐条修复评审提出的问题,并补上评审要求的"走真实索引链路的端到端测试"。

评审问题 → 修复对照

# 评审意见 修复
阻断 1 图片没接进真实索引链路(read_document_text 白名单缺图片 → 被当 UTF-8 读,OCR 永不执行,且每张图都写 last_error 白名单补 png/jpg/jpeg;无 OCR/无文字时返回空串走"空文档"正常分支,不再污染 last_error
阻断 2 tesseract stdout 管道死锁(只轮询 try_wait 不读管道,文字密集页必挂且白烧 30s) stdout/stderr 由独立线程持续消费 + 超时看门狗
严重 3 DOCX 内嵌图临时目录未创建(干净机器静默失败) create_dir_all + 失败告警
严重 4 ask 链路同步跑 OCR 阻塞 tokio worker 新增 extract_document_text_without_ocr();ask 引用摘要与桌面预览一律不触发 OCR(OCR 只在索引期)
严重 5 解压无上限可 OOM take(limit + 1) 限制解压产物,并按声明尺寸设上限
严重 6 忽略 /BitsPerComponent,会把解码噪声当文档内容索引 只接受 8bit/通道,跳过 ImageMask 与不支持的色彩空间
严重 7 无文本层 PDF 由 Some("") 改成 None,误导为"文件读取失败(可能被占用)" 恢复 Some("") 语义
次要 tesseract 路径进 OnceLock + env 不覆盖 → 改配置要重启 探测缓存按配置值做键(改配置自动重探),并区分外部预设 env 与自身写入
次要 read_to_end 失败绕过尺寸护栏 读取失败直接跳过,不再对截断字节做 OCR
次要 perf-scale artifact / resume 计数 见压测 PR

自查中另修(同类"静默失效")

  • /Filter 解析失败会退化成"无过滤器" → 把压缩字节按 raw 像素解码成乱码 PNG,OCR 噪声可能写进知识库(评审 #6 警告的类型)。现改为严格解析:任一元素解析不出名称即整张跳过,并支持间接引用。
  • 支持 ICCBased / 间接引用色彩空间(扫描仪导出 PDF 常用,之前整张被跳过)
  • 支持 ASCII85 包裹的 JPEG[/ASCII85Decode /DCTDecode])与仅 ASCII85/ASCIIHex 的 raw 图
  • 不支持的过滤器(CCITTFax / JPXDecode)加 debug! 留痕,不再"静默什么都没发生"

新增配置入口

  • 桌面:「设置 → 模型」新增「OCR 引擎(tesseract)」行(选择可执行文件 / 清除 / 显示当前路径),保存后立即生效(无需重启)
  • 服务端:新增 POST /api/settings/ocr-path(operator 角色),已登记 OpenAPI 与路由计数同步
  • 解析顺序:MEMORI_OCR_TESSERACT_PATH > settings.jsonocr_tesseract_path > PATH 自动探测

测试与验证

  • 新增真实扫描件 OCR 端到端测试(scanned_pdf_is_indexed_through_ocr_when_available;无 tesseract 时自动跳过;CI 的 Linux job 安装 tesseract-ocr + tesseract-ocr-chi-sim 保证真实执行)
  • 新增:图片路由回归(去掉白名单修复即失败,报错正是 stream did not contain valid UTF-8)、真实扫描件解码为合法 PNG、解压上限、位深校验、ICCBased、ASCII85+DCT
  • 本机用真实 tesseract 跑通:提取成功,40 字符,耗时 755ms → 识别出 …项目的对账窗口为每月 8 号…
  • 诚实提示:tesseract 会把实体名误读(苍岭苑岭/苔岭),因此端到端测试只做结构性断言(中文数量 + 索引链路),不锁定措辞;OCR 文本适合做召回辅助,不宜当作精确引用口径
  • cargo test --workspace --all-targets 全绿(0 失败);clippy -- -D warningsfmt --check、UI build 均通过

文档同步

README.md / README.en.md(OCR 从"设计中"移到"已实现但仍在优化",写清边界)、plan.md(Q6 移到已做)、IMPROVEMENTS.mdRETRIEVAL_BASELINE_V2.md(新增「OCR 接入与边界」章节:能力 / 不做 OCR 的时机 / 配置路径 / 改配置需重建 / 实测 / 已知边界)

已知边界(本 PR 未解决)

  • 混合型 PDF(有文本层 + 扫描页)不对扫描页 OCR
  • ppt/xlsx 内嵌图未接入;CCITTFaxDecode(G4 传真压缩)/ JPXDecode(JPEG2000)不支持
  • 改 OCR 路径后,已入库文件需重建索引才会重跑 OCR

Sourcery 摘要

启用可靠的索引时 OCR,支持图像和扫描文档,同时加强提取安全性、配置管理和端到端验证。

新功能:

  • 为独立图像、无文本层的扫描 PDF 以及 DOCX 中嵌入的图像添加索引时 tesseract OCR,使其内容进入正常的检索流程。
  • 通过桌面端设置和仅限操作员使用的服务器 API 配置 OCR 可执行文件路径,并支持立即应用和基于环境的回退解析。

错误修复:

  • 修复图像路由、PDF 图像解码、DOCX 临时目录处理、OCR 进程管道死锁、提取限制以及误导性的空文档错误,这些问题可能导致 OCR 无法执行或污染索引错误。

改进:

  • 将 OCR 排除在提问时引用预览和桌面文件预览之外,以避免阻塞交互式工作流。
  • 通过在索引内容之前验证过滤器、位深度、色彩空间、解压缩大小和不受支持的格式,加强 PDF 图像提取的安全性。

构建:

  • 在 Linux CI 任务中安装 tesseract 和简体中文语言包,以进行真实的 OCR 验证。

CI:

  • 添加真实扫描 PDF OCR 端到端测试,并增加针对图像路由、解码、安全限制和 OCR 配置行为的回归测试覆盖。

文档:

  • 在 README、QA/规划材料和发行说明中记录 OCR 的可用性、配置、索引行为、已测量的限制、已知边界以及重新索引的要求。

测试:

  • 添加解析器和索引测试,覆盖扫描 PDF、图像路由、PDF 过滤器变体、ICCBased 色彩空间、位深度验证、解压缩限制以及端到端 OCR 行为。

杂项:

  • 在服务器路由和 OpenAPI 路由清单中注册 OCR 设置端点。
Original summary in English

Sourcery 摘要

通过可靠的索引时 OCR,使图像和扫描文档可供搜索,同时增强提取安全性、配置能力和端到端验证。

新功能:

  • 为独立图像、无文本层的扫描 PDF 以及 DOCX 中嵌入的图像启用索引时 Tesseract OCR,使其内容进入常规检索流程。
  • 添加桌面端设置和仅限操作员使用的服务器端点,用于配置 Tesseract 可执行文件;配置可立即生效,并支持通过环境变量或 PATH 回退解析。

错误修复:

  • 修复图像路由、PDF 图像解码、DOCX 临时文件处理、OCR 进程管道死锁、提取大小保护机制,以及会阻止 OCR 或污染索引诊断信息的误导性空文档错误。

增强功能:

  • 不在询问时的引用生成和桌面预览中执行 OCR,以避免阻塞交互式工作流;同时以保守方式拒绝不受支持或可疑的 PDF 图像数据。

CI:

  • 在 Linux CI 中安装 Tesseract 和简体中文语言包,以进行真实 OCR 验证。

文档:

  • 在用户文档和 QA 文档中记录 OCR 的可用性、配置、索引行为、限制、实测准确率以及重新索引要求。

测试:

  • 添加真实扫描 PDF OCR 端到端测试,以及针对图像路由、PDF 解码变体、色彩空间、位深、解压缩限制和配置行为的回归测试。

杂务:

  • 在服务器路由表和 OpenAPI 规范中注册 OCR 设置端点。
Original summary in English

Sourcery 摘要

启用可靠的索引时 OCR,支持处理图像和扫描文档,同时加强提取安全性、配置能力和端到端验证。

新功能:

  • 为独立图像、无文本层的扫描 PDF 以及 DOCX 中嵌入的图像启用索引时 Tesseract OCR,使其内容进入正常检索流程。
  • 添加桌面端设置和仅限操作员使用的服务器端点,用于配置 Tesseract 可执行文件,并支持立即生效以及通过环境变量/PATH 回退解析。

错误修复:

  • 修复图像路由、PDF 图像提取、DOCX 临时文件处理、OCR 进程死锁、提取限制以及误导性的空文档错误,这些问题可能导致 OCR 失败或污染索引诊断信息。

改进:

  • 将 OCR 排除在提问时引用预览和桌面端文件预览之外,以避免阻塞交互式工作流;同时以保守方式拒绝不受支持或可疑的 PDF 图像数据。

CI:

  • 在 Linux CI 中安装 Tesseract 和简体中文语言包,以进行真实 OCR 验证。

文档:

  • 在用户文档和 QA 文档中记录 OCR 的可用性、配置、索引行为、实测限制、已知边界以及重新索引要求。

测试:

  • 添加真实扫描 PDF OCR 端到端测试,以及针对图像路由、PDF 解码变体、颜色空间、位深、解压缩限制和 OCR 配置行为的回归测试。

杂项:

  • 在服务器路由表和 OpenAPI 规范中注册 OCR 设置端点。
Original summary in English

Summary by Sourcery

Enable reliable index-time OCR for images and scanned documents while hardening extraction safety, configuration, and end-to-end validation.

New Features:

  • Enable index-time Tesseract OCR for standalone images, text-layer-free scanned PDFs, and DOCX embedded images so their content enters normal retrieval.
  • Add desktop settings and an operator-only server endpoint for configuring the Tesseract executable, with immediate application and environment/PATH fallback resolution.

Bug Fixes:

  • Fix image routing, PDF image extraction, DOCX temporary-file handling, OCR process deadlocks, extraction limits, and misleading empty-document errors that could prevent OCR or pollute indexing diagnostics.

Enhancements:

  • Keep OCR out of ask-time citation previews and desktop file previews to avoid blocking interactive workflows, while conservatively rejecting unsupported or suspicious PDF image data.

CI:

  • Install Tesseract and the Simplified Chinese language pack in Linux CI for real OCR validation.

Documentation:

  • Document OCR availability, configuration, indexing behavior, measured limitations, known boundaries, and re-indexing requirements in user and QA documentation.

Tests:

  • Add real scanned-PDF OCR end-to-end coverage and regression tests for image routing, PDF decoding variants, color spaces, bit depth, decompression limits, and OCR configuration behavior.

Chores:

  • Register the OCR settings endpoint in the server route table and OpenAPI specification.

- ocr.rs:tesseract 外部进程封装(30s 超时强杀、PSM 4 单列排版、
  路径探测 OnceLock 缓存、临时文件原子序号防并发冲突)
- 扫描 PDF:无文本层回退提取 XObject 图片 OCR,支持 DCTDecode 直写
  与 [ASCII85Decode FlateDecode] 链式解码(含 ASCII85/Hex 解码器)
- docx 内嵌图片(word/media)OCR 追加,20MB 大小上限
- 独立图片文件(png/jpg/jpeg)进白名单直接 OCR
- 找不到 tesseract 时全链路静默降级,既有行为不变
- settings.json 新增 ocr_tesseract_path,server/desktop 启动时注入
  环境变量(显式 env 优先);ocr_smoke 调试工具自动读 settings
- v2 套件图片/扫描 4 题:文档召回 0/4 → 4/4
评审阻断项/严重项:read_document_text 白名单补 png/jpg/jpeg(图片曾被当 UTF-8 读,OCR 永不执行且每张图都写 last_error);tesseract stdout 管道死锁改为独立线程读管道 + 超时看门狗;DOCX 内嵌图临时目录未创建;ask 期与桌面预览不再触发 OCR(原来会同步阻塞 worker 数分钟);flate 解压加 take(limit+1) 防 OOM;只接受 8bit/通道并跳过 ImageMask/不支持色彩空间;无文本层 PDF 恢复 Some 空串 语义。

复查补充:/Filter 严格解析(解析失败即跳过,不再退化成无过滤器而把压缩字节当 raw 像素解码成乱码);支持 ICCBased/间接引用色彩空间、ASCII85 包裹的 JPEG、仅 ASCII85/ASCIIHex 的 raw 图;tesseract 路径缓存按配置值失效(改配置无需重启)。

配置入口:桌面 设置-模型 可选 tesseract;服务端新增 POST /api/settings/ocr-path(含 OpenAPI 登记与路由计数同步)。

测试:真实扫描件 OCR 端到端(无 tesseract 自动跳过,CI Linux job 安装 tesseract-ocr + tesseract-ocr-chi-sim)、图片路由回归、解码上限、位深、ICCBased、ASCII85+DCT;文档同步 OCR 现状与边界。
当前 workspace/UI/Tauri 版本均为 1.5.2,但 docs/release 只有到 v1.5.0,lint job 的版本一致性检查因此必然失败(上游 collab 本身也缺)。补齐 RELEASE_NOTES_v1.5.2.md,覆盖本次工程硬化与 OCR 变更、已知边界与升级说明。

同时把 docs/README.md 的 Release 索引补上 v1.5.0 与 v1.5.2 两条(此前 v1.5.0 也未登记)。
@sourcery-ai

sourcery-ai Bot commented Sep 14, 2026

Copy link
Copy Markdown

审查者指南

本 PR 将 tesseract OCR 以索引期能力接入图片、扫描 PDF 和 DOCX 内嵌图片的真实检索链路,同时通过严格 PDF 解码与资源上限、无阻塞的进程管道处理、ask/预览隔离、可即时更新的配置入口和真实端到端测试,修复原 OCR 实现的主要评审阻断项并同步产品文档。

索引期 OCR 导入与检索时序图

sequenceDiagram
    participant File as DocumentOrImage
    participant Indexer
    participant Parser
    participant Tesseract
    participant Store
    participant Search

    File->>Indexer: read_document_text(path)
    Indexer->>Parser: extract_document_text(path)
    Parser->>Parser: extract_pdf_images(path)
    Parser->>Tesseract: ocr_image_file(image)
    Tesseract-->>Parser: recognized text
    Parser-->>Indexer: document text
    Indexer->>Store: chunk and index text
    Search->>Store: retrieve(query)
    Store-->>Search: OCR-backed chunks
Loading

无 OCR 的 ask 和预览路径时序图

sequenceDiagram
    participant User
    participant Ask as AskOrPreview
    participant Parser
    participant Store

    User->>Ask: request answer or file preview
    Ask->>Parser: extract_document_text_without_ocr(path)
    Parser-->>Ask: text-layer content or empty text
    Ask->>Store: use existing indexed chunks
    Store-->>Ask: stored OCR-backed evidence
    Ask-->>User: answer or preview
Loading

保守式 PDF 图片 OCR 解码流程图

flowchart TD
    A[PDF page XObject image] --> B{Valid image stream?}
    B -- No --> Z[Skip with debug log]
    B -- Yes --> C{8 bit per component?}
    C -- No --> Z
    C -- Yes --> D{Supported color space?}
    D -- No --> Z
    D -- Yes --> E{Filter chain parses strictly?}
    E -- No --> Z
    E -- Yes --> F[Decode with bounded output]
    F --> G{Within pixel and byte limits?}
    G -- No --> Z
    G -- Yes --> H[Write valid PNG or JPEG]
    H --> I[Run OCR at index time]
Loading

文件级变更

变更 详情 文件
将图片、无文本层扫描 PDF 和 DOCX 内嵌图片接入索引期 OCR,并复用正常文本分块与检索链路。
  • 扩展图片格式白名单,避免二进制图片被当作 UTF-8 读取;OCR 不可用或无文字时返回空文档语义。
  • 从 PDF XObject 提取图片并编码为合法 PNG/JPEG,支持严格过滤器解析、间接引用、ICCBased 色彩空间及 ASCII85/ASCIIHex 包装。
  • 为解压产物、原始像素和图片输入增加尺寸限制,并校验位深、ImageMask、色彩空间及不支持的过滤器。
  • 为 DOCX 内嵌图片创建临时目录、限制读取大小,并在失败时记录告警。
  • 通过独立 stdout/stderr 消费线程和超时看门狗调用 tesseract,避免管道死锁。
memori-parser/src/ocr.rs
memori-parser/src/lib.rs
memori-parser/Cargo.toml
memori-core/src/indexing_rebuild.rs
memori-vault/src/lib.rs
将 OCR 限定在索引阶段,避免 ask、引用摘要和桌面预览执行同步 OCR。
  • 新增不启用 OCR 的文本提取入口,并在引用摘要和文件预览中使用该入口。
  • 无文本层 PDF 保持 Some("") 语义,避免被报告为文件读取失败。
memori-parser/src/lib.rs
memori-core/src/retrieval_output.rs
memori-desktop/src/commands/scope.rs
增加 tesseract 配置的桌面与服务端入口,并实现即时生效及配置优先级。
  • 支持环境变量、settings.json 和 PATH 探测,探测缓存按配置值失效重建,并区分外部环境变量与程序注入值。
  • 桌面模型设置增加选择、清除和路径展示;服务端新增 operator 保护的 OCR 路径接口及 OpenAPI 登记。
  • 同步桌面、服务端 DTO、启动注入和 UI API 状态。
memori-core/src/model_config.rs
memori-desktop/src/commands/settings.rs
memori-desktop/src/dto.rs
memori-desktop/src/lib.rs
memori-desktop/src/model_runtime.rs
memori-server/src/dto.rs
memori-server/src/main.rs
memori-server/src/routes/mod.rs
memori-server/src/routes/openapi.rs
memori-server/src/routes/settings.rs
ui/src/App.tsx
ui/src/app/api/desktop.ts
ui/src/app/types.ts
ui/src/app/useAppInit.ts
ui/src/app/useAppSettings.ts
ui/src/components/SettingsModal.tsx
ui/src/components/settings/tabs/ModelsTab.tsx
ui/src/components/settings/types.ts
补充真实 OCR 和解析护栏测试,并让 CI 安装中文 tesseract 依赖。
  • 新增真实扫描 PDF OCR 测试,验证索引入口及图片入口确实产出中文;缺少 tesseract 或语料时跳过。
  • 新增真实索引路由回归、合法图片解码、解压上限、位深、过滤器、ICCBased 和 ASCII85+DCT 测试。
  • Linux CI 安装 tesseract 与 chi_sim 语言包,确保端到端测试在 CI 中真实执行。
memori-core/src/tests.rs
memori-parser/src/ocr.rs
.github/workflows/rust-ci.yml
memori-parser/examples/ocr_smoke.rs
同步 OCR 能力边界、配置方式、测试结论和发布说明。
  • 更新中英文 README、计划和检索基线,明确索引期 OCR、误识别风险、重建索引要求和已知不支持格式。
  • 新增版本发布说明,汇总 OCR 实现、工程修复、配置入口和边界。
README.md
README.en.md
docs/planning/IMPROVEMENTS.md
docs/planning/plan.md
docs/qa/RETRIEVAL_BASELINE_V2.md
docs/README.md
docs/release/RELEASE_NOTES_v1.5.2.md
Cargo.lock

提示与命令

与 Sourcery 交互

  • 触发新的审查: 在 pull request 中评论 @sourcery-ai review
  • 继续讨论: 直接回复 Sourcery 的审查评论。
  • 从审查评论生成 GitHub issue: 回复审查评论,请 Sourcery 根据该评论创建 issue。也可以回复审查评论并使用 @sourcery-ai issue,从中创建 issue。
  • 生成 pull request 标题: 在 pull request 标题的任意位置写入 @sourcery-ai,即可随时生成标题。也可以在 pull request 中评论 @sourcery-ai title,随时(重新)生成标题。
  • 生成 pull request 摘要: 在 pull request 正文的任意位置写入 @sourcery-ai summary,即可在指定位置随时生成 PR 摘要。也可以在 pull request 中评论 @sourcery-ai summary,随时(重新)生成摘要。
  • 生成审查者指南: 在 pull request 中评论 @sourcery-ai guide,即可随时(重新)生成审查者指南。
  • 解决所有 Sourcery 评论: 在 pull request 中评论 @sourcery-ai resolve,解决所有 Sourcery 评论。如果你已经处理完所有评论且不想再看到它们,此功能会很有用。
  • 忽略所有 Sourcery 审查: 在 pull request 中评论 @sourcery-ai dismiss,忽略所有现有的 Sourcery 审查。如果你想从新的审查开始,这尤其有用——别忘了评论 @sourcery-ai review 来触发新的审查!

自定义使用体验

访问你的控制面板,可以:

  • 启用或禁用审查功能,例如 Sourcery 生成的 pull request 摘要、审查者指南等。
  • 更改审查语言。
  • 添加、删除或编辑自定义审查说明。
  • 调整其他审查设置。

获取帮助

Original review guide in English

Reviewer's Guide

本 PR 将 tesseract OCR 以索引期能力接入图片、扫描 PDF 和 DOCX 内嵌图片的真实检索链路,同时通过严格 PDF 解码与资源上限、无阻塞的进程管道处理、ask/预览隔离、可即时更新的配置入口和真实端到端测试,修复原 OCR 实现的主要评审阻断项并同步产品文档。

Sequence diagram for index-time OCR ingestion and retrieval

sequenceDiagram
    participant File as DocumentOrImage
    participant Indexer
    participant Parser
    participant Tesseract
    participant Store
    participant Search

    File->>Indexer: read_document_text(path)
    Indexer->>Parser: extract_document_text(path)
    Parser->>Parser: extract_pdf_images(path)
    Parser->>Tesseract: ocr_image_file(image)
    Tesseract-->>Parser: recognized text
    Parser-->>Indexer: document text
    Indexer->>Store: chunk and index text
    Search->>Store: retrieve(query)
    Store-->>Search: OCR-backed chunks
Loading

Sequence diagram for OCR-free ask and preview paths

sequenceDiagram
    participant User
    participant Ask as AskOrPreview
    participant Parser
    participant Store

    User->>Ask: request answer or file preview
    Ask->>Parser: extract_document_text_without_ocr(path)
    Parser-->>Ask: text-layer content or empty text
    Ask->>Store: use existing indexed chunks
    Store-->>Ask: stored OCR-backed evidence
    Ask-->>User: answer or preview
Loading

Flow diagram for conservative PDF image OCR decoding

flowchart TD
    A[PDF page XObject image] --> B{Valid image stream?}
    B -- No --> Z[Skip with debug log]
    B -- Yes --> C{8 bit per component?}
    C -- No --> Z
    C -- Yes --> D{Supported color space?}
    D -- No --> Z
    D -- Yes --> E{Filter chain parses strictly?}
    E -- No --> Z
    E -- Yes --> F[Decode with bounded output]
    F --> G{Within pixel and byte limits?}
    G -- No --> Z
    G -- Yes --> H[Write valid PNG or JPEG]
    H --> I[Run OCR at index time]
Loading

File-Level Changes

Change Details Files
将图片、无文本层扫描 PDF 和 DOCX 内嵌图片接入索引期 OCR,并复用正常文本分块与检索链路。
  • 扩展图片格式白名单,避免二进制图片被当作 UTF-8 读取;OCR 不可用或无文字时返回空文档语义。
  • 从 PDF XObject 提取图片并编码为合法 PNG/JPEG,支持严格过滤器解析、间接引用、ICCBased 色彩空间及 ASCII85/ASCIIHex 包装。
  • 为解压产物、原始像素和图片输入增加尺寸限制,并校验位深、ImageMask、色彩空间及不支持的过滤器。
  • 为 DOCX 内嵌图片创建临时目录、限制读取大小,并在失败时记录告警。
  • 通过独立 stdout/stderr 消费线程和超时看门狗调用 tesseract,避免管道死锁。
memori-parser/src/ocr.rs
memori-parser/src/lib.rs
memori-parser/Cargo.toml
memori-core/src/indexing_rebuild.rs
memori-vault/src/lib.rs
将 OCR 限定在索引阶段,避免 ask、引用摘要和桌面预览执行同步 OCR。
  • 新增不启用 OCR 的文本提取入口,并在引用摘要和文件预览中使用该入口。
  • 无文本层 PDF 保持 Some("") 语义,避免被报告为文件读取失败。
memori-parser/src/lib.rs
memori-core/src/retrieval_output.rs
memori-desktop/src/commands/scope.rs
增加 tesseract 配置的桌面与服务端入口,并实现即时生效及配置优先级。
  • 支持环境变量、settings.json 和 PATH 探测,探测缓存按配置值失效重建,并区分外部环境变量与程序注入值。
  • 桌面模型设置增加选择、清除和路径展示;服务端新增 operator 保护的 OCR 路径接口及 OpenAPI 登记。
  • 同步桌面、服务端 DTO、启动注入和 UI API 状态。
memori-core/src/model_config.rs
memori-desktop/src/commands/settings.rs
memori-desktop/src/dto.rs
memori-desktop/src/lib.rs
memori-desktop/src/model_runtime.rs
memori-server/src/dto.rs
memori-server/src/main.rs
memori-server/src/routes/mod.rs
memori-server/src/routes/openapi.rs
memori-server/src/routes/settings.rs
ui/src/App.tsx
ui/src/app/api/desktop.ts
ui/src/app/types.ts
ui/src/app/useAppInit.ts
ui/src/app/useAppSettings.ts
ui/src/components/SettingsModal.tsx
ui/src/components/settings/tabs/ModelsTab.tsx
ui/src/components/settings/types.ts
补充真实 OCR 和解析护栏测试,并让 CI 安装中文 tesseract 依赖。
  • 新增真实扫描 PDF OCR 测试,验证索引入口及图片入口确实产出中文;缺少 tesseract 或语料时跳过。
  • 新增真实索引路由回归、合法图片解码、解压上限、位深、过滤器、ICCBased 和 ASCII85+DCT 测试。
  • Linux CI 安装 tesseract 与 chi_sim 语言包,确保端到端测试在 CI 中真实执行。
memori-core/src/tests.rs
memori-parser/src/ocr.rs
.github/workflows/rust-ci.yml
memori-parser/examples/ocr_smoke.rs
同步 OCR 能力边界、配置方式、测试结论和发布说明。
  • 更新中英文 README、计划和检索基线,明确索引期 OCR、误识别风险、重建索引要求和已知不支持格式。
  • 新增版本发布说明,汇总 OCR 实现、工程修复、配置入口和边界。
README.md
README.en.md
docs/planning/IMPROVEMENTS.md
docs/planning/plan.md
docs/qa/RETRIEVAL_BASELINE_V2.md
docs/README.md
docs/release/RELEASE_NOTES_v1.5.2.md
Cargo.lock

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

嘿——我发现了 2 个问题

面向 AI 代理的提示
请处理本次代码审查中的评论:

## 各条评论

### 评论 1
<location path="memori-parser/src/ocr.rs" line_range="209-212" />
<code_context>
+            continue;
+        };
+        let Some(xobjects) = resources
+            .get(b"XObject")
+            .ok()
+            .and_then(|value| value.as_dict().ok())
+        else {
+            continue;
+        };
</code_context>
<issue_to_address>
**issue (bug_risk):** 某些有效 PDF 的页面资源字典会将 `/XObject` 存储为间接引用,但代码在未解析该引用的情况下调用了 `as_dict()`,因此不会发现页面图像,扫描页面的 OCR 最终返回空文本。

**触发条件:** PDF 页面使用 `/XObject 12 0 R`,而不是内联的 XObject 字典时。

**建议修复:** 在将 `/XObject` 值转换为字典之前,先使用 `resolve_object` 对其进行解析。
</issue_to_address>

### 评论 2
<location path="memori-parser/examples/ocr_smoke.rs" line_range="97-105" />
<code_context>
+    }?;
+    let raw = std::fs::read_to_string(settings_path).ok()?;
+    // 轻量解析:只取需要的字段,避免给 parser 引入 serde 依赖。
+    raw.split('\n')
+        .find_map(|line| {
+            let (key, value) = line.split_once(':')?;
+            (key.trim().trim_matches('"') == "ocr_tesseract_path").then(|| {
+                value
+                    .trim()
+                    .trim_end_matches(',')
+                    .trim_matches('"')
+                    .to_string()
+            })
+        })
</code_context>
<issue_to_address>
**nitpick (bug_risk):** smoke 工具通过按每行第一个冒号分割并去除引号来解析 `settings.json`,但没有执行 JSON 反转义。因此,存储为转义 JSON 的 Windows 路径(例如 `C:\\Program Files\\Tesseract\\tesseract.exe`)会以包含字面反斜杠的形式传递,导致无法找到该路径。

**触发条件:** 配置的 OCR 路径是包含转义反斜杠的 Windows 路径时。

**建议修复:** 使用一个小型 serde 模型反序列化 `settings.json`,而不是按行解析。
</issue_to_address>

Sourcery 评估

需要人工审查。 首先需要处理 1 个发现;如果 OCR 提取或 PDF/图像解码有误,错误识别的文本可能会被持久化到索引中,并在建立索引后影响检索。回滚可以阻止后续 OCR,但已经建立索引的内容需要清除或重建索引;受影响的数据范围有限且可以修复。

阻塞性发现:memori-parser/src/ocr.rs:212


Sourcery 对开源项目免费——如果您喜欢我们的审查结果,请考虑分享 ✨
Original comment in English

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="memori-parser/src/ocr.rs" line_range="209-212" />
<code_context>
+            continue;
+        };
+        let Some(xobjects) = resources
+            .get(b"XObject")
+            .ok()
+            .and_then(|value| value.as_dict().ok())
+        else {
+            continue;
+        };
</code_context>
<issue_to_address>
**issue (bug_risk):** Valid PDFs whose page resource dictionary stores `/XObject` as an indirect reference are skipped because `as_dict()` is called on the reference without resolving it, so no page images are discovered and scan-page OCR returns empty text.

**Triggers:** When a PDF page uses `/XObject 12 0 R` instead of an inline XObject dictionary.

**Suggested fix:** Resolve the `/XObject` value with `resolve_object` before converting it to a dictionary.
</issue_to_address>

### Comment 2
<location path="memori-parser/examples/ocr_smoke.rs" line_range="97-105" />
<code_context>
+    }?;
+    let raw = std::fs::read_to_string(settings_path).ok()?;
+    // 轻量解析:只取需要的字段,避免给 parser 引入 serde 依赖。
+    raw.split('\n')
+        .find_map(|line| {
+            let (key, value) = line.split_once(':')?;
+            (key.trim().trim_matches('"') == "ocr_tesseract_path").then(|| {
+                value
+                    .trim()
+                    .trim_end_matches(',')
+                    .trim_matches('"')
+                    .to_string()
+            })
+        })
</code_context>
<issue_to_address>
**nitpick (bug_risk):** The smoke tool parses `settings.json` by splitting each line at the first colon and stripping quotes without JSON unescaping, so a Windows path stored as escaped JSON such as `C:\\Program Files\\Tesseract\\tesseract.exe` is passed with literal backslashes and is not found.

**Triggers:** When the configured OCR path is a Windows path containing escaped backslashes.

**Suggested fix:** Deserialize `settings.json` with a small serde model instead of line-based parsing.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 1 finding to address first, and if the OCR extraction or PDF/image decoding is wrong, misrecognized text can be persisted in the index and affect retrieval after indexing. Reverting stops future OCR, but already-indexed content requires clearing or rebuilding the index; the affected data is bounded and repairable.

Blocking findings: memori-parser/src/ocr.rs:212


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread memori-parser/src/ocr.rs Outdated
Comment on lines +97 to +105
raw.split('\n')
.find_map(|line| {
let (key, value) = line.split_once(':')?;
(key.trim().trim_matches('"') == "ocr_tesseract_path").then(|| {
value
.trim()
.trim_end_matches(',')
.trim_matches('"')
.to_string()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick (bug_risk): smoke 工具通过按每行第一个冒号分割并去除引号来解析 settings.json,但没有执行 JSON 反转义。因此,存储为转义 JSON 的 Windows 路径(例如 C:\\Program Files\\Tesseract\\tesseract.exe)会以包含字面反斜杠的形式传递,导致无法找到该路径。

触发条件: 配置的 OCR 路径是包含转义反斜杠的 Windows 路径时。

建议修复: 使用一个小型 serde 模型反序列化 settings.json,而不是按行解析。

Original comment in English

nitpick (bug_risk): The smoke tool parses settings.json by splitting each line at the first colon and stripping quotes without JSON unescaping, so a Windows path stored as escaped JSON such as C:\\Program Files\\Tesseract\\tesseract.exe is passed with literal backslashes and is not found.

Triggers: When the configured OCR path is a Windows path containing escaped backslashes.

Suggested fix: Deserialize settings.json with a small serde model instead of line-based parsing.

@FPSZ FPSZ left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

先说结论:上一轮的 11 条全部真修了,而且修法都对路。管道死锁改成 stdout/stderr 独立线程 drain 且超时 kill 路径也 join;allow_ocr 开关把 OCR 彻底赶出问答链路;take(limit+1) 给解压产物设了界;BitsPerComponent != 8 直接跳过并写了理由;探测缓存用配置值做键,改配置不用重启;CI 还补上了 tesseract-ocr + chi-sim,让端到端测试在 CI 真能跑。这批返工质量很高,辛苦了。

"OCR 端到端可用" 这个核心结论仍然不成立,而且是上一轮同一类的问题:单测能过、真实文件挂。所以还得再打回一次。


🔴 阻断项

1. extract_pdf_images 丢弃了间接 / 继承的 /Resourcesmemori-parser/src/ocr.rs:205

对着 lopdf 0.35.0 源码核过了。get_page_resources 的签名是 Result<(Option<&Dictionary>, Vec<ObjectId>)>,实现在 document.rs:429

resource_dict = page.get(b"Resources").and_then(Object::as_dict).ok();   // ← .0
collect_resources(page, &mut resource_ids, self, &mut HashSet::new())?;  // ← .1

.0 只有在 /Resources直接字典时才是 SomeObject::as_dictReference 返回 Err)。间接引用的、以及从 /Parent 继承的,全部走 .1 那个 Vec<ObjectId>

当前写法把 .1 整个丢掉了:

let Ok((Some(resources), _)) = doc.get_page_resources(page_id) else { continue; };

Word / Acrobat / Ghostscript / 多数扫描仪驱动导出的 PDF 都用间接 /Resources,这些页会被直接 continue 跳过,扫描件索引成空——正是这个 PR 要解决的场景。

另外 ocr.rs:203 的注释写的是「第一个返回值是页面资源字典(含继承)」,和 lopdf 的实际行为正好相反,建议一并改掉,免得后面的人照着注释理解。

2. /XObject 没有解引用 — memori-parser/src/ocr.rs:211

resources.get(b"XObject").ok().and_then(|value| value.as_dict().ok())

as_dict() 同样不做解引用(lopdf/src/object.rs:247Reference 分支直接 Err),所以 /XObject 15 0 R 这种写法会让整页被跳过。

lopdf 提供了会解引用的 get_dict_in_dictdocument.rs:201);这个文件里也已经有 resolve_object 了,只是这一步没用上。

3. 为什么测试没挡住

我把测试用的那份语料拆开看了 —— Memory_Test_V2/special_005_扫描件_苍岭_对账.pdf,ReportLab 生成,obj 4 长这样:

4 0 obj << /Contents 8 0 R /MediaBox [...] /Parent 7 0 R
  /Resources << /Font 1 0 R /ProcSet [...] /XObject << /FormXob.93b6... 3 0 R >> >>
  /Rotate 0 /Type /Page >>

/Resources/XObject 都是直接字典,所以上面两个 bug 对这份文件完全隐形。

这个端到端测试本身写得是对的(用真实扫描件、结构性断言而不锁措辞、环境缺失自动跳过、CI 装语言包),问题只在语料选型单一。建议再补一份 /Resources/XObject 都是间接引用的 PDF 作为第二个 fixture——用 Ghostscript 或 LibreOffice 导出就能得到,两行代码的 bug 才会被这个测试真正锁住。


🟠 需要修

4. OpenAPI 悬空 $refmemori-server/src/routes/openapi.rs:318

request: Some("SetOcrTesseractPathRequest") 引用了一个 build_component_schemas() 里没有定义的 schema(对照 SetLocalModelsRootRequest 在 522 行、SetWatchRootRequest 在 526 行都有定义)。这是生成的 spec 里唯一一个解析不了的 $ref#2 刚加的 Swagger UI 打开这个端点会渲染成坏的。

漂移自检目前只断言 paths.len() >= 25ErrorResponse 存在,抓不到这类问题。建议顺手在自检里加一条:遍历 spec 收集所有 $ref,断言每个都能在 components.schemas 里解析到——这样以后加端点漏 schema 会直接红。

5. /DecodeParms /Predictor 完全未处理 — memori-parser/src/ocr.rs:362

全文搜不到 DecodeParms / Predictor。后果分两种:Predictor 15(PNG predictor,很常见)解出来是带行首 filter 字节的数据,会被当成像素;Predictor 2 的图能通过现有全部护栏,然后把一张错位的图 OCR 成噪声写进索引。

这正是上一轮第 6 条(BitsPerComponent)要防的同一类污染——证据链可信是这个项目的卖点,建议和那条一样处理:只接受没有 /DecodeParmsPredictor == 1 的流,其余跳过并 warn。宁可少索引,不能索引噪声。

6. 图片能索引能引用了,但预览白名单没跟上 — memori-desktop/src/commands/scope.rs:189

extracted_text_exts = ["docx", "pdf"],png/jpg 两个列表都不在,点开一条图片 citation 会弹「不支持预览的文件类型: .png」。图片预览按说应该直接显示图,不是走抽取文本那条路,可能需要前端配合;至少不该是现在这个报错。


🟡 建议修

  • -l chi_sim 硬编码,没有 eng 回退 (ocr.rs:110):而 ocr_available() 只探 --version,所以装了英文版 tesseract 的用户会得到「可用」+ 永远识别不出东西。CI 装了 chi-sim 所以 CI 不受影响,这是终端用户侧的坑。建议探测时顺带跑一次 --list-langs,按可用语言包拼 -l chi_sim+eng
  • 逐页图片全解码落盘后才开始 OCR (ocr.rs:195):没有总量上限、也没有按 XObject id 去重(同一张图在多页复用时会重复解码 + 重复 OCR)。500 页扫描件会先把临时目录撑满再串行 OCR 数小时。建议边解码边 OCR、加页数/总字节上限、用 ObjectId 去重。
  • 单图路径少了尺寸护栏 (lib.rs:593):DOCX 和 PDF 两条路径都有 MAX_OCR_IMAGE_BYTES,独立图片文件这条没有。这里是把路径交给 tesseract、不是自己 buffer,所以风险低于另两条,但一张超大图仍会让 tesseract 吃满内存直到 30s 超时。
  • spawn tesseract 没加 CREATE_NO_WINDOW (ocr.rs:107):Windows 桌面端每张图会闪一次控制台窗口。仓库里对 llama.cpp 已经有现成写法,见 memori-desktop/src/commands/model_runtime_cmd.rs:545

✅ 核过没问题的

  • stdout/stderr 独立线程 drain + 超时看门狗,kill 路径也 join,管道死锁确实解决了
  • take(limit + 1) 限制的是解压产物,解压炸弹防住了
  • /Filter 严格解析、ASCII85/ASCIIHex 解码(含短组补位,不会 u32 溢出)
  • Some("") 空文档语义走到 process_file_event 的空 chunk 分支,不写 last_error
  • ask 期确实不做 OCR:retrieval_output.rs:366scope.rs:203 都走 extract_document_text_without_ocr
  • tesseract 探测缓存以配置值为键,改配置无需重启;空值回退 PATH
  • CI 补了 tesseract + chi-sim,端到端测试在 CI 会真的执行
  • REST_ROUTE_METHOD_COUNT 33→34 同步了

另外:release notes 的耦合问题

docs/release/RELEASE_NOTES_v1.5.2.md#2 / #3 / #4 三个 PR 里内容完全相同(md5 一致),而它明确写了 "adds OCR ingestion for images and scanned documents"。

也就是说只要 #2#3 先合,仓库就发布了一份宣称 OCR 已可用的说明,而 OCR 还在返工。建议把这份 release notes 从 #2 / #3 里摘掉,只留在 #4,等 OCR 真正合入时一起发。

(顺带一提:notes 里主动写了 苍岭 → 苑岭/苔岭 的误读限制,这个坦诚度很好,建议保留。)

wjs480 pushed a commit to wjs480/Memori-Vault that referenced this pull request Sep 15, 2026
该 release notes 在 FPSZ#2/FPSZ#3/FPSZ#4 三份 PR 中内容相同,且写明 adds OCR ingestion;若本 PR 先合,仓库会发布与事实不符的说明。按评审统一只保留在 OCR PR。docs/README.md 的两行索引同理一并摘除。
wjs480 pushed a commit to wjs480/Memori-Vault that referenced this pull request Sep 15, 2026
该 release notes 在 FPSZ#2/FPSZ#3/FPSZ#4 三份 PR 中内容相同,且写明 adds OCR ingestion;若本 PR 先合,仓库会发布与事实不符的说明。按评审统一只保留在 OCR PR。docs/README.md 的两行索引同理一并摘除。
阻断项:extract_pdf_images 改用 get_page_resources 的第二个返回值(间接/继承 /Resources 不再被整页跳过,并修正错误注释);/XObject 改用 get_dict_in_dict 解引用;按 ObjectId 去重。必须项:Predictor 非 1 一律跳过;登记 SetOcrTesseractPathRequest schema 并新增 every_ref_in_spec_resolves 守门测试;图片预览放开 png/jpg/jpeg。建议项:--list-langs 组装 chi_sim+eng;PDF 图片张数/总字节双上限 + 边解码边 OCR;独立图片尺寸护栏;Windows CREATE_NO_WINDOW。
@FPSZ

FPSZ commented Sep 16, 2026

Copy link
Copy Markdown
Owner

第三轮复审。两个阻断项确实修好了,我实测验证过,不是只看代码。

实测验证

代码审下来 scan_pdf_images 的改法是对的(get_page_resources 的第二个返回值逐个 get_object 解引用后并入资源字典列表、/XObject 改用 get_dict_in_dict、按 ObjectId 去重),但我想要的是能跑的证据,所以起了两个隔离 worktree 实测。

手工构造了一份全间接引用的 PDF(raw bytes 直写,完全控制 xref):

4 0 obj << /Type /Page /Parent 5 0 R /MediaBox [...] /Resources 3 0 R >>   ← Resources 间接
3 0 obj << /XObject 2 0 R >>                                               ← XObject 间接
2 0 obj << /Im1 1 0 R >>
1 0 obj << /Type /XObject /Subtype /Image ... /Filter /FlateDecode ... >>

对同一份文件跑 extract_pdf_images

代码版本 结果
修复前(e6f2834 FAILED — 提取出 0 张
修复后(1a9e319 ok — 提取出 1 张,PNG magic 正确

所以修复是真实有效的,探针也确实有鉴别力。

其余 8 项也都核过了:Predictor 保守放行(只接受无 /DecodeParmsPredictor == 1)、SetOcrTesseractPathRequest 已登记且加了 every_ref_in_spec_resolves 守门测试(递归收集 $ref 逐个断言可解析,正是需要的形态)、预览白名单放开 png/jpg/jpeg 并按 image 类型走、--list-langs 组装 chi_sim+eng、PDF 图片张数/总字节双上限 + 边解码边 OCR + 超限时清理临时文件、独立图片尺寸护栏、Windows CREATE_NO_WINDOW


唯一剩下的问题:回归测试被 ignore 了

pdf_with_indirect_resources_is_extracted 打了 #[ignore],TODO 写的是:

保存出来的文件 get_pages() 解析为空,尚未定位(renumber_objects() 后仍如此)

这个诊断是错的,我查到了真正的根因。

先说 get_pages():我复现了你的构造方式,实测 get_pages() 存盘再读返回 1,不是空的get_page_resources 也正确返回 direct=false, inherited=[(3,0)]max_id 不设、/Parent 不写,都不影响——这几个我都单独试过。

真正的问题在流本身:

存盘后 /Filter = Some(/FlateDecode)
存盘后 content 字节数 = 0        ← 这里
解一次 Flate 得到 0 字节(期望 16)

原因是手工构造 lopdf::Stream 用了结构体字面量:

lopdf::Object::Stream(lopdf::Stream {
    dict: image,
    content: compressed,
    allows_compression: true,
    start_position: None,
})

这样 dict 里没有 /Length,存盘写出来的流长度是 0,再 load 回来 content 就是空的,解码自然拿不到像素,最后 extract_pdf_images 返回 0 张。lopdf::Stream::new 会替你设好 /Lengthlopdf-0.35.0/src/object.rs:602dict.set("Length", content.len() as i64)),结构体字面量绕过了它。

一行修复(我已实测通过)

-        doc.objects.insert(
-            (1, 0),
-            lopdf::Object::Stream(lopdf::Stream {
-                dict: image,
-                content: compressed,
-                allows_compression: true,
-                start_position: None,
-            }),
-        );
+        doc.objects.insert(
+            (1, 0),
+            lopdf::Object::Stream(lopdf::Stream::new(image, compressed)),
+        );

改完把 #[ignore] 去掉,实测结果:

running 1 test
test ocr::tests::pdf_with_indirect_resources_is_extracted ... ok

顺带一提,Stream::new 默认就是 allows_compression: truestart_position: None,所以这一行完全等价,只是多了 /Length

这样就不需要再去找 Ghostscript / LibreOffice 导出的真实 fixture 了——手工构造的这份已经能精确覆盖「间接 /Resources + 间接 /XObject」这个组合,而且不用往仓库里塞二进制语料。


把这个测试启用之后我就没有别的意见了。这轮返工做得很扎实——十项逐条落实,注释里还把「为什么这么改」的来龙去脉写清楚了(尤其是 scan_pdf_images 里解释 lopdf 两个返回值语义那段,和 Predictor 为什么宁可跳过那段),后面接手的人会省很多事。

合并 FPSZ#2/FPSZ#3/FPSZ#5 后的 collab,三处冲突均为双方各自新增内容,保留双方:
- README.md「已实现但仍在优化」:50k 压测条目 + OCR 条目
- docs/qa/RETRIEVAL_BASELINE_V2.md:作答层 LLM-judge 基线 + OCR 接入与边界
- memori-server/src/dto.rs:AppSettings 的 index_filter 与 ocr_tesseract_path 两个字段

同时启用 pdf_with_indirect_resources_is_extracted(原 #[ignore]):
手工构造 lopdf::Stream 用结构体字面量会绕过 /Length 写入(lopdf object.rs:602
的 Stream::new 才会 dict.set("Length", ...)),存盘后流长度为 0、load 回来
content 为空,解码拿不到像素,测试因此假失败——与 get_pages() 无关(实测
get_pages()=1、get_page_resources 也正确返回 inherited)。改用 Stream::new 后
测试通过,可作为间接 /Resources + 间接 /XObject 的回归锁。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@FPSZ
FPSZ merged commit 6511f74 into FPSZ:collab Sep 16, 2026
7 of 8 checks passed
@FPSZ

FPSZ commented Sep 16, 2026

Copy link
Copy Markdown
Owner

已合并。合并前我往这个分支推了一个提交(7b0f053),说明一下改了什么:

1. 解决与已合并 PR 的冲突#2/#3/#5 先合入 collab 后产生,三处都是双方各自新增内容,保留双方):

2. 启用了 pdf_with_indirect_resources_is_extracted(原 #[ignore]),按上一条评论里验证过的一行修复:lopdf::Stream 结构体字面量 → lopdf::Stream::new(image, compressed)

合并前在本地完整验证过(fork PR 的 workflow 需要批准,Rust CI 一直没在这四个 PR 上跑过,所以这轮是本地把的关):

cargo fmt --all -- --check            OK
cargo test --workspace --all-targets  203 passed / 0 failed / 0 ignored
cargo clippy --workspace --all-targets -- -D warnings   0
pnpm --dir ui exec tsc --noEmit       OK
pnpm --dir ui run build               OK

0 ignored 即是那个回归测试确实在跑的证据。


顺带一提:.github/workflows/rust-ci.yml 的触发条件是 pull_request:(无分支过滤),按理应该在这些 PR 上跑,但实际只有 Sourcery 执行了——原因是 fork 发起的 PR 其 workflow 需要仓库侧批准。建议在仓库 Settings → Actions → Fork pull request workflows 里放开(或对已知协作者放开),否则后续 fork PR 仍然是「没有 CI 覆盖」的状态,只能靠人工本地跑。

这轮三次返工下来质量很好,辛苦了。

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