MemPalace 格式覆盖详解:`mine --mode extract` 如何将 PDF/DOCX/PPTX/XLSX/RTF/EPUB 无损入库
MemPalace 3.3.6 引入了第三个挖掘器 format_miner.py,让 mempalace mine --mode extract 能把二进制办公文档(PDF、DOCX、PPTX、XLSX、RTF、EPUB)在读取时转换为文本并注入向量宫殿,而不修改磁盘上的任何源文件。读完本文,你将掌握该特性的完整用法、按格式路由的转换原理、13 类边缘情况的处理契约、公开 API 与 CLI 接线方式,并能直接在自己的文档目录上运行 extract 模式挖掘。以下内容基于 格式覆盖设计文档 及其对应的 format_miner.py 实现、CLI 入口 与 测试清单 交叉印证。
一、它是什么:第三个挖掘器,按内容类型分治
MemPalace 遵循“每种内容类型一个 miner”的模式。三种模式共享同一条 mempalace mine 命令,由 --mode 参数分发:
mempalace mine <dir> → miner.py (项目文件:代码、文档、笔记)
mempalace mine <dir> --mode convos → convo_miner.py (聊天导出)
mempalace mine <dir> --mode extract → format_miner.py (二进制办公文档) ← 新增
在 cli.py 中,cmd_mine 的实际分发逻辑与设计文档完全一致:mode == "extract" 时延迟导入 mine_formats,并透传 format_dir、palace_path、wing、agent、limit、dry_run 六个参数。--mode 的 argparse 定义位于 cli.py,取值为 ["projects", "convos", "extract"],help 文本明确提示 extract 模式需要 mempalace[extract] 依赖集。
各格式与其对应的转换库如下:
| 扩展名 | 转换库 | Python 版本要求 | 许可证 |
|---|---|---|---|
.pdf |
MarkItDown | ≥ 3.10 | MIT |
.docx |
MarkItDown | ≥ 3.10 | MIT |
.pptx |
MarkItDown | ≥ 3.10 | MIT |
.xlsx |
MarkItDown | ≥ 3.10 | MIT |
.epub |
MarkItDown | ≥ 3.10 | MIT |
.rtf |
striprtf | ≥ 3.6 | MIT |
二、按格式路由:为什么 .rtf 不走 MarkItDown
这是该特性最关键的实证发现:在 2026-05-19 对本地混合格式测试目录的实测中,MarkItDown 0.1.5 并不转换 .rtf——它会把原始 RTF 控制码源码({\rtf1\ansi\ansicpg1252...)原样返回。若不加路由修正,52 个 RTF 文件将全部以不可读的控制码形式入库。
因此 format_miner.py 在 extract_text 内部做了按扩展名的分发:
is_rtf = p.suffix.lower() == ".rtf"
if is_rtf:
text = _extract_via_striprtf(p) # striprtf:纯 Python、约 150 行、专为 RTF 设计
else:
text = _extract_via_markitdown(p) # MarkItDown 处理其余五种格式
设计原则保持不变:转换发生在读取时,绝不动源文件——改变的只是每种格式选用的转换库。测试 test_rtf_routes_to_striprtf_not_markitdown 与 test_non_rtf_does_not_touch_striprtf 分别断言了 RTF 只走 striprtf、非 RTF 只走 MarkItDown,且大写扩展名 .RTF 同样会被正确路由(扩展名匹配统一经 Path.suffix.lower() 小写化,见 format_miner.py 的 SUPPORTED_FORMATS 定义)。
三、可选依赖:按需安装,缺失时给出安装指令
MarkItDown 与 striprtf 都是可选运行时依赖,只挖掘项目文件或聊天导出的用户不需要安装它们。pyproject.toml 中 extract extra 的完整声明比设计文档中的建议版更进一步——MarkItDown 的导入包本身不足以完成转换,必须带各格式的子 extra(拉取 pdfminer-six、mammoth、python-pptx、openpyxl 等):
extract = [
"striprtf>=0.0.27",
# 裸 markitdown 只够 import,转换需要各格式子 extra;
# EPUB 由基础包覆盖(无 [epub] 子 extra),RTF 由 striprtf 覆盖。
"markitdown[docx,pdf,pptx,xlsx]>=0.1.5; python_version >= '3.10'",
]
要点:
- 环境标记
python_version >= '3.10'使 Python 3.9 用户自动只获得 striprtf,RTF 仍可提取,其余格式被静默跳过; - 缺少 MarkItDown(pdf/docx/pptx/xlsx/epub 任一)→ 状态
SKIP_NO_MARKITDOWN,日志输出pip install markitdown; - 缺少 striprtf(任何
.rtf)→ 状态SKIP_NO_STRIPRTF,日志输出pip install striprtf; - 若 MarkItDown 已装但缺某个格式的子 extra(如
markitdown[pdf]),MarkItDown 会抛出MissingDependencyException,extract_text 通过异常类型名(而非 isinstance)识别后返回单独的状态SKIP_MISSING_FORMAT_DEPS,让用户看到可执行的安装信号,而不是笼统的SKIP_EXTRACTION_ERROR。
四、架构设计:读取时转换,存储层零改动
与 3.3.6 的姊妹特性虚拟行号(见 virtual-line-numbering.md)同出一源:转换在读取时完成,存储层原样不动。
磁盘上的 PDF →(字节读入内存)→ MarkItDown → Markdown 文本 → 现有分块器 → ChromaDB 抽屉
- 磁盘上的 PDF 只被读取,从不被修改;
- 不向用户文件系统写入任何中间
.md文件; - 转换完全在内存中完成,文本一旦进入现有分块器,后续处理与其他入库路径完全一致;
- Closet / Drawer / Hall 机制全部照常工作——format miner 产出的抽屉形状与其他 miner 相同。
五、为什么新建一个 miner,而不是改 miner.py
设计文档给出了按重要性排序的三条理由:
- 匹配既有的“按内容类型分 miner”模式:已有
miner.py与convo_miner.py,第三个 miner 是同一形状的自然延伸,阅读代码库的人可立即识别; - 评审面更小:
format_miner.py是纯新增文件,评审者孤立地读一个新文件;miner.py与convo_miner.py的稳定代码路径零改动; - 故障隔离更干净:format miner 若有 bug,从物理上不可能影响既有 miner。
权衡结论在 2026-05-19 的设计评审中敲定:新 miner 模式在架构与评审面两个维度上都胜出。
六、13 类边缘情况:每个路径都有可审计的状态码
每个边缘情况都映射到一个具体的 ExtractionStatus 枚举值(format_miner.py),调用方与评审者可以精确审计到底走了哪条路径;13 个案例全部有 tests/test_format_miner.py 中的专门测试(该文件共 71 个测试函数,覆盖全部边缘案例与编排器行为):
| # | 案例 | 状态码 | 行为 |
|---|---|---|---|
| 1 | MarkItDown 未安装(任一非 RTF 格式) | SKIP_NO_MARKITDOWN |
记录 pip install markitdown 指令;跳过文件 |
| 2 | 文件过大(默认 > 500 MB,调用方可覆盖) | SKIP_TOO_LARGE |
记录大小与阈值;跳过 |
| 3 | iCloud 云端独占文件(.icloud 后缀或 st_flags dataless 位) |
SKIP_CLOUD_ONLY |
在任何可能触发物化的 I/O 之前检测;跳过 |
| 4 | 加密 / 受密码保护的 PDF | SKIP_ENCRYPTED |
捕获消息匹配 encrypt/decrypt/password/protected 的异常;跳过 |
| 5 | 空文件(零字节) | SKIP_EMPTY |
静默跳过 |
| 6 | 权限被拒 | SKIP_PERMISSION |
捕获 PermissionError;记录日志;跳过 |
| 7 | 断裂符号链接(目标缺失) | SKIP_BROKEN_SYMLINK |
通过 is_symlink() + not exists() 检测;跳过 |
| 8 | 提取文本中的脏编码 | (恢复) | decode_robust() 依次回退 UTF-8 → CP1252 → UTF-8-with-replace;绝不抛异常 |
| 9 | Windows 路径语义 | (无专门状态) | 全程使用 pathlib.Path;后缀匹配大小写不敏感;接受 str 或 Path 输入 |
| 10 | 转换库内部崩溃(畸形文件) | SKIP_EXTRACTION_ERROR |
在文件边界捕获通用 Exception;记录文件名 + 异常类型 + 消息尾部;跳过 |
| 11 | 网络 / 同步盘超时 | SKIP_NETWORK_TIMEOUT |
捕获 TimeoutError;跳过 |
| 12 | 未识别的扩展名 | SKIP_UNRECOGNIZED |
廉价的后缀集合检查;跳过 |
| 13 | striprtf 未安装(任一 .rtf) |
SKIP_NO_STRIPRTF |
记录 pip install striprtf 指令;跳过。2026-05-19 加入,起因是实测发现 MarkItDown 0.1.5 不支持 RTF |
另有 SKIP_UNREADABLE 作为 stat 阶段 OSError 的兜底状态(不在原始 13 项之内,为防御完备性补加——见 extract_text 的文件 stat 块)。加密检测的具体实现在 _ENCRYPTED_PATTERNS:一条不区分大小写的正则,使加密判定与底层抛出异常的具体库无关。
一个重要的运行时细节:瞬态跳过不写哨兵
SKIP_NO_MARKITDOWN、SKIP_NO_STRIPRTF、SKIP_MISSING_FORMAT_DEPS、SKIP_NETWORK_TIMEOUT 这四种状态被归入 _TRANSIENT_MISSING_DEP_STATUSES 集合(format_miner.py)。编排器在调用哨兵注册前会检查该集合:瞬态环境性跳过不会写入 file_already_mined 哨兵。原因是哨兵会把文件永久标记为“已挖掘”——如果用户日后补装了缺失的依赖或网络恢复,下一次挖掘必须能重新拾起这些文件。持久性跳过(如加密、空文件)则正常写哨兵,避免每次重挖都重复扫描。
七、明确的边界:本 miner 不解决什么
按设计文档,以下是有据可查的限制而非 bug:
- 针对特定文档类型的定制 PDF 解析器——MarkItDown 尽力而为,接受其能力边界;
- 扫描版图片 PDF 的 OCR——独立关注点,留给后续版本的可选特性;
- DRM 锁定文件——不绕过、也不应尝试;
- 病理性损坏文件——它们就是坏的,
SKIP_EXTRACTION_ERROR加一条清晰的日志行是负责任的结局。
这些限制通过逐文件的跳过码日志上报,用户始终知道哪些文件没进来、为什么。
八、API 参考:两个函数 + 一个枚举 + 两个辅助函数
所有公共符号经 __all__ 导出(format_miner.py)。
extract_text(path, max_file_size=DEFAULT_MAX_FILE_SIZE) -> tuple[Optional[str], ExtractionStatus]
把一个文件转换为文本,返回 (text, status):OK 时 text 是提取出的 Markdown,所有跳过场景为 None。纯函数——path 处的源文件永不被修改。path 同时接受 Path 与 str,内部经 Path(path).expanduser() 解析 ~/foo.pdf 风格的输入。大小上限默认 DEFAULT_MAX_FILE_SIZE = 500 * 1024 * 1024(500 MB,定义处),与其他两个 miner 的默认值一致;调用方可通过参数为合法的大文件(如扫描书)提高上限。
scan_formats(directory) -> list[Path]
递归遍历目录,返回按路径排序的支持文件列表。跳过规则见 scan_formats:
- 隐藏 / 构建目录:复用共享的
palace.SKIP_DIRS常量(与 miner.py、convo_miner.py 同步,含.git、.venv、__pycache__等); - OS 元数据文件:
.DS_Store、Thumbs.db、desktop.ini; - 符号链接:防止循环链接与同一文件经多路径重复处理(与其他两个 miner 对齐);
- 扩展名不在
SUPPORTED_FORMATS中的文件。
排序结果保证重挖时处理顺序确定,便于复现 bug 报告。目录不存在时返回空列表而非抛错。
ExtractionStatus(Enum)
上文 13 个文档化值外加 SKIP_UNREADABLE 与 SKIP_MISSING_FORMAT_DEPS。每个成员配有人类可读的字符串值(如 "skip:no_markitdown")用于日志;测试断言 .name 以保稳定。
decode_robust(raw: bytes) -> str
与终端会话规范化器中的辅助函数同策略(实现):先试 UTF-8(干净路径),失败后试 CP1252(处理旧 Office 文档中 0x91–0x9F 的智能引号字节),最终以 errors='replace' 的 UTF-8 兜底——任何字节都不会丢失,只会被显式化为替换符。函数永不抛异常,空输入返回空串。
is_icloud_dataless(path: Path) -> bool
两个检测信号(实现):
- 字面
.icloud后缀——iCloud 卸载文件的占位约定; - macOS
st_flags的 dataless 位(0x40000000)——尽力而为,非 macOS 平台优雅降级为False(通过getattr(path.lstat(), "st_flags", 0)取值,OSError时返回False)。
返回 True 时 extract_text 会在任何可能触发 iCloud 物化拉取的 I/O 之前短路到 SKIP_CLOUD_ONLY——因为让 MarkItDown 阻塞等待 iCloud 物化可能挂起数分钟。
九、CLI 集成:两处 argparse 编辑 + 一个 elif 分支
对 mempalace/cli.py 的 cmd_mine 的改动极小,实际代码位于 cli.py:
def cmd_mine(args):
...
try:
if args.mode == "convos":
from .convo_miner import mine_convos
mine_convos(...)
elif args.mode == "extract": # ← 新增
from .format_miner import mine_formats # ← 新增
mine_formats(
format_dir=args.dir,
palace_path=palace_path,
wing=args.wing,
agent=args.agent,
limit=args.limit,
dry_run=args.dry_run,
)
else:
from .miner import mine
mine(...)
argparse 侧仅是在 --mode 的 choices 中加入 "extract"(定义)。这就是全部 CLI 改动。
十、编排器 mine_formats:锁、幂等与元数据
文档只列了 API 契约,而 mine_formats 的实现揭示了完整的入库流水线,值得补充说明:
- 配置贯通:加载
MempalaceConfig,把用户的chunk_size/chunk_overlap/min_chunk_size透传给miner.chunk_text,格式模式的挖掘同样尊重用户的分块调参; - 房间路由:读取目标目录的
mempalace.yaml(rooms 列表),无配置时回退到单一documents房间;每份文件经detect_room路由(文件夹匹配 → 文件名匹配 → 内容关键词评分 → 兜底general),与 miner.py 语义一致; - 幂等与锁:
mine_lock锁源文件后重查file_already_mined(check_mtime=True, extract_mode="format")——extract_mode="format"把幂等检查限定在格式模式抽屉集合内,同一source_file上 convo / project miner 的抽屉不会误判;源文件 mtime 变化会触发重新挖掘; - 清除 + 批量写入:先
delete该源文件的旧抽屉,再按DRAWER_UPSERT_BATCH_SIZE = 1000分批 upsert;写入前经assert_no_collisions校验; - 抽屉元数据:
ingest_mode="extract"、extract_mode="format"使格式挖掘的抽屉在宫殿中可与其他模式区分;另含wing、room、source_file、chunk_index、added_by、filed_at、hall(内容关键词路由)、entities、source_mtime、content_date、line_start/line_end等; - 健壮性:每个文件的处理包在独立 try/except 中,单个坏文件不会打断整个挖掘;
KeyboardInterrupt在编排层捕获,Ctrl-C 时仍打印摘要——抽屉 ID 是确定性的(make_drawer_id_from_chunk),重挖 upsert 到相同行,部分进度天然安全; - 收尾校验:挖掘结束后执行 FTS5 完整性检查(
_validate_palace_fts5_after_mine),并清理 hook 派生挖掘遗留的 PID 文件。
dry_run=True 时完成遍历 + 提取 + 分块,但不打开集合、不 upsert,仅打印“将归档”的内容。
十一、实证冒烟测试:52 RTF + 11 PDF 的真实数据
设计文档记录了 2026-05-19 在本地混合格式目录(52 个 .rtf + 11 个 .pdf,真实书信与研究/架构文档,无合成夹具)上的端到端结果(Python 3.13,MarkItDown 0.1.5 + striprtf 0.0.27):
Supported formats: ['.docx', '.epub', '.pdf', '.pptx', '.rtf', '.xlsx']
scan_formats: found 63 supported files
| 格式 | 文件数 | 结果 | 提取字符数 |
|---|---|---|---|
| RTF(经 striprtf) | 52 | 52/52 OK | 1,350,679 |
| PDF(经 MarkItDown) | 11 | 10 OK,1 SKIP_ENCRYPTED | 1,340,869 |
唯一跳过的 PDF 是真实的受密码保护文件——边缘案例 4(SKIP_ENCRYPTED)经由异常消息匹配器在真实数据上正确触发,设计行为得到实证验证。防回归断言:零个原始 \rtf1 / \ansi 控制码泄漏进提取文本。若没有按格式路由修正,52/52 的 RTF 都会以不可读的控制码形式入库;修正后每个 RTF 都产出干净的纯文本。这是被 mock 单元测试无法捕获、却促成了 SKIP_NO_STRIPRTF 状态与按格式分发的集成缺陷。
十二、向后兼容与 3.3.6 范围外的事项
兼容性承诺(均可从仓库确认):
- 既有 miner 零改动:
miner.py与convo_miner.py未被触碰; - 无新增必选依赖:MarkItDown 与 striprtf 是可选 extra,安装方式为
pip install mempalace[extract](pyproject.toml 中已按上文的子 extra + 环境标记声明); - 无磁盘格式变化:无论哪个 miner 产出,抽屉 / closet / hall 形状完全相同;
- 无迁移步骤:已有宫殿的用户直接运行
mempalace mine --mode extract ~/docs/,新抽屉落入同一宫殿、同一 wing、同一房间约定; - 大文件上限:默认 500 MB,
extract_text接受max_file_size覆盖,供扫描书等合法大文件场景使用。
3.3.6 范围外的真实未来特性:超大 PDF(> 500 MB)的流式提取(MarkItDown 默认整体读入内存,pypdf 逐页流式是自然扩展)、音视频转写(Whisper)、扫描 PDF 的 OCR(Tesseract / 云 OCR)、迁移导入器(Notion / Obsidian / Mem0 导出格式)。这些各有独立 scope,不属于“通过 MarkItDown 增量扩展格式覆盖”的 3.3.6 发布范围。
小结
mempalace mine --mode extract 的完整价值链条可以概括为:scan_formats 确定性遍历 → extract_text 按格式分发到 MarkItDown / striprtf 并以 13+ 状态码全路径可审计 → 用户配置驱动的分块与房间路由 → 带锁、带哨兵、带 mtime 幂等的抽屉归档。所有环节都以“读取时转换、源文件零改动、可选依赖缺失时给指令而非崩溃”为底线。若你想在仓库中继续深入,建议按此顺序阅读:docs/format-coverage.md(设计与边界)、mempalace/format_miner.py(实现)、tests/test_format_miner.py(71 个测试逐一对应状态码)与 mempalace/cli.py(CLI 接线)。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0625
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00