ADR-0014: MinerU Read Command
历史决策记录:保留当时的背景、状态和设计。当前接口与实现归属以 开发入口 和现行规范为准;文中的历史模块路径不保证仍存在。
状态: Accepted 日期: 2026-06-16 相关文档: 0012-doclib-block-locator.md, 0013-doc-content-progressive-reading.md, ../cli/mineru-parse.md, ../workflows.md
背景
mineru parse 从文件 path 出发,负责入库、解析、等待任务完成,并读取默认内容。Agent 后续继续阅读时,不应一直依赖本地绝对路径或重新传递 path,而应进入稳定 locator 世界。
ADR-0011 定义了 docs.short_id,ADR-0012 定义了 page/block locator,ADR-0013 定义了 parse content 的渐进式阅读协议。基于这些能力,需要新增一个 locator-first 的顶级命令:
mineru read <locator>
read 的目标是读取 doclib 中已有解析结果。它不负责 discover、scan、ingest,也不默认创建 parse task。
决策
新增 mineru read 顶级命令:
mineru read <locator> [--after CURSOR] [--format markdown|image] [--limit 30000] [--context N] [--output PATH] [--json] [--no-marker]
parse 与 read 的职责边界:
parse(path) = ensure document is parsed, then read default content
read(locator) = read existing parsed content by stable locator
parse 当前只支持 --format markdown。
read P0 支持:
ContentFormat = Literal["markdown", "image"]
--format 表示 content format;--json 表示 CLI response envelope 使用 JSON。二者不是同一层语义。
Locator 输入
read 支持以下 locator 粒度:
doc:{short_id}
doc:{short_id}/tier:{tier}
doc:{short_id}/tier:{tier}/page:{page_no}
doc:{short_id}/tier:{tier}/page:{page_no}/block:{block_no}
doc:{short_id}/tier:{tier}/page:{page_no}/block:{block_no}/char:{offset}
规则:
page_no和block_no使用 1-based 编号。char:{offset}使用 block 渲染文本内的 0-based 字符 offset。doc:{short_id}会解析为当前可用的默认高质量 tier;不会静默把flash当作高质量读取结果。doc:{short_id}/tier:{tier}明确读取指定 tier 的已有结果。
请求定位器的 page: 同步采用输入页范围语法:数字、rN、区间、逗号组合与 all,具体规则见 ADR-0012。返回引用仍使用实际页码;block/char 和图片读取必须选择单页,多页选择不支持非零 context。
Markdown 读取语义
visual block 的 Markdown 引用与 image 可用性规则已由 ADR-0027 补充;
image_path不再作为 doclib 用户接口暴露。
--format markdown 是默认格式。
| Locator | 默认读取范围 |
|---|---|
doc:{short_id} |
默认 tier 的默认阅读范围。 |
doc:{short_id}/tier:{tier} |
该 tier 的默认阅读范围。 |
.../page:{page_no} |
该页。 |
.../block:{block_no} |
该 block。 |
.../char:{offset} |
从该 block 的 offset 起读取。 |
--limit 是软字符限制,沿用 ADR-0013 的规则,尽量在页、block、段落、句子、换行或空白边界停止。
--context
--context N 的含义随 locator 粒度变化:
| Locator | --context 行为 |
|---|---|
| page locator | 读取前后各 N 页。 |
| block locator | 读取前后各 N 个 block。 |
| char locator | 按 block locator 处理上下文,char 只影响起点。 |
| doc locator | 返回 context_not_applicable。 |
| doc/tier locator | 返回 context_not_applicable。 |
Image 读取语义
--format image 是 P0 能力,但范围受文档类型和 locator 粒度约束。
不支持多页 image 输出。
| 文档类型 | doc locator | doc/tier locator | page locator | block locator |
|---|---|---|---|---|
| PDF / image | 不支持 | 不支持 | 支持 | 有有效 bbox 时支持;否则可回退到 sidecar |
| Office / HTML | 不支持 | 不支持 | 不支持 | visual block 有可访问 image sidecar 时支持 |
PDF / image 的 image 输出规则:
- page image 从源文件重新渲染得到。
- block image 从源页面渲染后按 bbox 裁剪得到。
- block image 不按 block type 判断;只要 bbox 存在且非 empty,就可以输出 image。
- PDF 直接构造
PDFDocument;image 使用PDFDocument.from_image()重建解析时使用的单页 PDF。两者复用render_page()/crop_image()。
Office / HTML 的 image 输出规则:
- Office / HTML 不支持 doc、doc/tier、page locator 输出 image。
- image、table、chart、formula 等 visual block 仅在 doclib parsed 目录中存在对应 image sidecar 时支持 image 输出。
image_path只用于 doclib 内部安全解析 sidecar,不向 Markdown 读取者暴露。
默认情况下,image 输出打印 asset path。传入 --output 时,CLI 先根据输出路径后缀选择 image_format,由 server 生成匹配编码的临时 asset,再由 CLI 在 client 侧 copy 到指定路径。传入 --json 时输出完整 DocContentResponse。
image 输出只接受 .png、.jpg、.jpeg、.webp 输出路径。无后缀、其它后缀以及 --output - 都在 CLI 参数校验阶段返回 image_output_extension_unsupported。CLI 不负责图片转码。
响应模型扩展
read 与 parse 的读取结果都继续使用 DocContentResponse。不能为了 locator-first 删除 parse read 已有字段。
在 ADR-0013 的基础上做最小扩展:
ContentFormat = Literal["markdown", "image"]
class ContentRequestScope(DoclibModel):
page_range: str | None = None
after: str | None = None
limit: int = 30000
locator: str | None = None
context: int = 0
class ContentNextRequest(DoclibModel):
page_range: str | None = None
after: str | None = None
locator: str | None = None
class ContentAsset(DoclibModel):
path: str
mime_type: str
size_bytes: int | None = None
width: int | None = None
height: int | None = None
class DocContentResponse(DoclibModel):
sha256: str
short_id: str
tier: Tier
format: ContentFormat
content: str
request_scope: ContentRequestScope
content_ranges: list[ContentRange]
truncated: bool = False
next_request: ContentNextRequest | None = None
asset: ContentAsset | None = None
字段规则:
- parse read 保留现有字段语义。
- parse read 的
request_scope.locator = None,request_scope.context = 0。 - locator read 的
request_scope.locator填入规范化 locator。 - locator read 的
request_scope.context填入实际生效的 context。 - parse read 的
next_request只写page_range/after,不写locator。 - 单点 read 的
next_request只写locator;范围内截断时保留范围locator并携带绝对块/字符after,可通过mineru read <locator> --after <cursor>继续读取。 - 范围 read 读完后
next_request=null,续读不会进入未选择的页面;已有单点 read 的续读行为保持不变。 - image 输出时
content="",asset 信息写入asset,不把图片路径塞进content。
parse read 返回示例:
{
"sha256": "...",
"short_id": "ab12cd3",
"tier": "basic",
"format": "markdown",
"content": "...",
"request_scope": {
"page_range": "1-10",
"after": null,
"limit": 30000,
"locator": null,
"context": 0
},
"content_ranges": [
{
"page_range": "1-10",
"start": "doc:ab12cd3/tier:basic/page:1",
"end": "doc:ab12cd3/tier:basic/page:10/block:18"
}
],
"truncated": false,
"next_request": {
"page_range": "11-20",
"after": null,
"locator": null
},
"asset": null
}
locator read 返回示例:
{
"sha256": "...",
"short_id": "ab12cd3",
"tier": "basic",
"format": "markdown",
"content": "...",
"request_scope": {
"page_range": "4",
"after": null,
"limit": 30000,
"locator": "doc:ab12cd3/tier:basic/page:4/block:12",
"context": 2
},
"content_ranges": [
{
"page_range": "4",
"start": "doc:ab12cd3/tier:basic/page:4/block:10",
"end": "doc:ab12cd3/tier:basic/page:4/block:14"
}
],
"truncated": false,
"next_request": null,
"asset": null
}
image read 返回示例:
{
"sha256": "...",
"short_id": "ab12cd3",
"tier": "basic",
"format": "image",
"content": "",
"request_scope": {
"page_range": "4",
"after": null,
"limit": 30000,
"locator": "doc:ab12cd3/tier:basic/page:4/block:12",
"context": 0
},
"content_ranges": [
{
"page_range": "4",
"start": "doc:ab12cd3/tier:basic/page:4/block:12",
"end": "doc:ab12cd3/tier:basic/page:4/block:12"
}
],
"truncated": false,
"next_request": null,
"asset": {
"path": "/Users/me/.mineru/exports/ab12cd3-page-4-block-12.png",
"mime_type": "image/png",
"size_bytes": 12345,
"width": 800,
"height": 320
}
}
Continuation 与 marker
parse read 保持 ADR-0013 现有行为:
next_request.page_range/next_request.after用于下一次mineru parse。- CLI continuation marker 渲染为
mineru parse <path> --pages ... --after ...。
read 使用 locator continuation:
next_request.locator用于下一次mineru read。- CLI continuation marker 渲染为:
<!-- Next: mineru read doc:ab12cd3/tier:basic/page:5 -->
P0 暂不实现 block locator marker,即不在每个 block 前默认输出:
<!-- mineru:locator ... -->
--no-marker 关闭 continuation marker。
实现方案
parse 和 read 不合并 CLI 参数,也不共享入口 request model。二者共享内部读取计划和执行器。
parse_cmd
-> ensure_parse
-> ParseReadRequest
-> build_read_plan_from_parse()
-> execute_read_plan()
-> output_doc_content_response()
read_cmd
-> LocatorReadRequest
-> build_read_plan_from_locator()
-> execute_read_plan()
-> output_doc_content_response()
入口 request:
class ParseReadRequest(DoclibModel):
sha256: str
tier: Tier
page_range: str | None = None
after: str | None = None
limit: int = 30000
format: Literal["markdown"] = "markdown"
no_marker: bool = False
class LocatorReadRequest(DoclibModel):
locator: str
context: int = 0
limit: int = 30000
format: Literal["markdown", "image"] = "markdown"
no_marker: bool = False
内部计划:
class ReadPlan(DoclibModel):
sha256: str
short_id: str
tier: Tier
page_range: str | None = None
after: str | None = None
locator: str | None = None
context: int = 0
limit: int = 30000
format: Literal["markdown", "image"] = "markdown"
no_marker: bool = False
转换规则:
build_read_plan_from_parse()只处理sha256/tier/page_range/after/limit。build_read_plan_from_parse()生成的 plan 中locator=None、context=0。build_read_plan_from_locator()解析 locator,查出sha256/short_id/tier,并根据 locator 粒度生成page_range、after、locator和context。- doc locator 和 doc/tier locator 使用默认阅读范围。
- page locator 使用单页或 context 扩展后的页范围。
- block locator 使用所在页,并由执行器选择目标 block 和上下文 block。
- char locator 使用所在页,并从 char offset 起读取。
执行器:
def execute_read_plan(plan: ReadPlan) -> DocContentResponse: ...
执行器负责:
- load done JSON batches。
- filter pages。
- select page/block/char/context。
- render markdown。
- render page/block image。
- 有有效 bbox 的 block 使用 PDF/image 源页面渲染和裁剪;其他 visual block 使用 doclib parsed 目录中可访问的 sidecar。
- apply soft limit。
- build
content_ranges。 - build
next_request。 - create temporary image assets when
format="image"。
CLI 输出 helper:
def output_doc_content_response(
response: DocContentResponse,
*,
json_mode: bool,
output: str | None,
source_path: str | None,
read_mode: bool,
) -> None: ...
规则:
json_mode=True时输出完整DocContentResponse。- markdown 非 JSON 时输出
content,并按入口输出 continuation marker。 - image 非 JSON 时输出
asset.path。 output由 CLI 在 client 侧处理: markdown 写入content,image copyasset.path到目标路径。- parse marker 使用
page_range/after。 - read marker 使用
locator。
HTTP / SDK
P0 新增 locator-first 接口:
def read_content(
self,
locator: str,
*,
context: int = 0,
limit: int = 30000,
format: Literal["markdown", "image"] = "markdown",
image_format: Literal["jpeg", "png", "webp"] = "jpeg",
no_marker: bool = False,
) -> DocContentResponse: ...
HTTP 路由:
GET /api/v1/content?locator=doc:ab12cd3/tier:basic/page:4&format=markdown&limit=30000
GET /api/v1/content?locator=doc:ab12cd3/tier:basic/page:4/block:12&format=image&image_format=png
P0 不为 locator-first content API 提供 POST export。GET /api/v1/content 可以在 server 内部生成临时 image asset,并接受 image_format=jpeg|png|webp 控制 asset 编码,但不接受 client 指定的 output path。CLI read --output 是 client 侧文件写入/copy 行为,不是 doclib HTTP API 行为。
实现时在 Interface 中定义同步和异步方法,再由 client/server 使用相同签名实现。
错误码
| code | 场景 |
|---|---|
invalid_locator |
locator 格式非法。 |
doc_not_found |
short_id 找不到。 |
tier_not_cached |
指定 tier 没有缓存解析结果。 |
page_not_cached |
指定页没有缓存解析结果。 |
block_not_found |
block 不存在。 |
char_offset_out_of_range |
char offset 超出 block 内容长度。 |
context_not_applicable |
doc 或 doc/tier locator 使用了 context。 |
format_not_supported |
当前 locator 或文档类型不支持该 format。 |
multi_page_image_not_supported |
请求范围覆盖多页 image。 |
bbox_not_available |
block 没有有效 bbox。 |
asset_not_available |
Office image block 没有可读取 asset。 |
P0 范围
必须实现:
mineru read <locator>。- doc / doc+tier / page / block / char locator。
--format markdown。--format image,按本文 image 边界实现。--json。--limit。--context,按 locator 粒度规则实现。- read continuation marker。
- parse/read 共享
ReadPlan与execute_read_plan()。 DocContentResponse增加asset,ContentRequestScope增加locator/context,ContentNextRequest增加locator。
暂不实现:
- 多页 image 输出。
--format structured_content。--format text。--format csv/--format html。- block locator marker。
- search result locator。
- doc/tier locator 的
--format image多页导出包。
后果
正面:
- Agent 可以从 search、content_ranges 或 continuation 中拿到 locator 后,用统一命令继续读取。
parse和read共享实际读取执行器,避免两套 render/limit/marker/image 逻辑。- parse read 已有响应结构不被破坏。
- image 读取成为 locator 的通用能力,而不是 image block 的特殊命令。
代价:
DocContentResponse会同时承载 parse read 和 locator read 的字段。ContentNextRequest同时有page_range/after和locator,但通过入口规则避免混用。- image 输出需要补 PDF page render/crop 和 Office asset 读取两条实现路径。