Skill v2.3.0
currentLLM-judged scan90/100name: wiki-build description: 构建/重建本地知识库的 Wiki。扫描 vault 中所有源文件,从中构建一个结构化的 wiki(源文件摘要、实体、概念、对比、概述页),创建分层 INDEX(根索引 + 各目录索引)/log/hot,密集交叉链接。支持超长源文件(百万字级小说等):prep.mjs 确定性预处理 + 分层 digest 构建 + 断点续传。开工前先侦察源文件:扫描版走 OCR、电子版直接抽文字,电子版文字层被坏字体污染(方正书版把数学斜体字母映射成不相干汉字)则先用 fontmap.py 清洗——三类管线完全不同,选错代价极大。支持含图源文件(扫描版教材、PPT、带插图的 PDF/论文):docling 抽图 → media.mjs 归置与规范化 → 页面内 ![[ ]] 嵌入 → 图版对账。也能把旧单索引库的索引重构为分层布局。Triggers on: 构建 wiki, 重建 wiki, build wiki, 扫描源文件构建, 初始构建, 重新构建知识库, start wiki build, 重构索引, 索引分层, 索引迁移, restructure index, 图片入库, 图版, 插图, 带图的源文件, 扫描版教材, 教材入库, figure, image ingest. version: 2.3.0
wiki-build: 构建 Wiki
wiki 不是一次性的输出,而是一个持续增长的复利资产 — 每次构建、导入、查询都会让它更丰富。
维护注记:wiki-* 五件套(build/query/ingest/save/lint)同版本号共进——改任一 skill 时,五个version:一起 bump 到同一下一个版本;同一 PR(或一次对外发布)内的连续修订只 bump 一次。
核心原则
- 源文件不可变:源文件(notes/、docs/ 等)是用户的原始资料,只能读取,绝对不能修改或删除。
- 密集交叉链接:[[wiki 链接]] 是这个知识库的核心价值。每个页面都应大量链接到其他相关页面,形成知识网络。宁可多链接,不要少链接——但只链接最终页面集合里存在的页面:没有页面的名字在正文中一律用纯文本(或先建 stub 页再链)。收尾对账(deadcheck/linkpass,见"链接对账"节)机械保证零死链、零漏链。
- 图是内容,不是附件:源文件里的图形承载的信息常常无法用文字替代(几何关系、函数形态、解剖结构、电路拓扑)。建页时必须把图归置到 `wiki/images/`、用 `![[ ]]` 引用、并看图写准 caption;丢了图,页面就只剩空话。图版与页面受同等强度的对账门禁约束。
- 合成而非搬运:wiki 不是源文件的简单摘要集合,而是要提炼出跨源文件的综合观点、论点和洞察。
- 结构化元数据:每个页面必须带完整的 frontmatter,这是知识网络可查询、可审计的基础。
Vault 结构
vault 根目录就是当前工作目录。源文件在子目录中(如 raw/、notes/、docs/)。 wiki 相关内容的目录结构:
raw/— 未处理的原始资料目录raw/wechat/— 微信通道收到的网页、文件等原始资料统一先放在这里wiki/— 所有 wiki 页面的根目录wiki/INDEX.md— 根索引,只列目录级概览(各目录页数 + 覆盖范围)与概述入口页,不逐页罗列wiki/<dir>/INDEX.md— 每个内容目录(sources/entities/concepts/comparisons/questions)自己的索引,列全该目录页面及一句话摘要wiki/log.md— 按时间顺序记录的操作日志(最新条目在最上面)wiki/hot.md— 近期上下文缓存(~500 字,每次操作后刷新)wiki/images/— 图版根目录;每个含图源文件一个子目录(wiki/images/<源文件主名>/),图按文档顺序命名001.png、002.png…,整页兜底图为page-042.png。不参与页面索引与知识图谱(图谱只收.md),但会出现在文件树里wiki/meta/— 元数据目录(lint 报告等)wiki/sources/— 源文件摘要页,由 raw/、notes/、docs/ 等原始资料生成;不要把原始资料直接放入这里wiki/entities/— 人物、组织、工具等实体页wiki/concepts/— 概念、模式、框架等wiki/comparisons/— 对比分析页wiki/questions/— 归档的问答页
构建超长源文件时,确定性过程文件统一放 .molio/wiki-build/(文件树扫描会跳过 .molio,不污染 vault;跨会话可续传)。
页面路径规则:
- 默认使用单文件页面。文件名 = 实体/概念的规范名本身:中文内容用中文名直做文件名(如
wiki/entities/李白.md),英文内容用 kebab-case(如wiki/entities/molio.md)。[[wiki 链接]]的链接名必须与目标文件名(去掉.md)完全一致——[[李白]]对应李白.md,写成libai.md会断链 - 只有当某个实体、项目或主题需要拆成多个稳定页面时,才建立同名目录,并用
index.md作为该目录入口 - 同名目录下的子页面必须围绕该入口主题展开,例如
wiki/entities/molio/index.md、wiki/entities/molio/architecture.md - 不要把项目命名空间强行放进错误的内容类型目录;目录首先按页面类型归类,再按主题自然生长
Frontmatter 规范
每个 wiki 页面必须包含以下 YAML frontmatter:
---type: source | entity | concept | comparison | overview | question | sessiontitle: "人类可读的标题"created: YYYY-MM-DDupdated: YYYY-MM-DDtags:- 领域标签related:- "[[相关页面]]"sources:- "[[源文件名]]"---
字段说明:
type:页面类型,必须是以上值之一title:人类可读标题created/updated:创建和最后更新日期tags:领域标签列表(至少一个)related:相关页面的 [[wiki 链接]] 列表(尽量多填)sources:信息来源的 [[wiki 链接]] 列表(source 类型页面填原始文件名,其他类型填参考了哪些 source 页面)
页面类型说明
根据源文件的内容、规模和领域,自行决定最合适的页面类型。以下是参考:
- source(源文件摘要)— 每个源文件一个摘要页,提取关键信息,放在
wiki/sources/ - entity(实体)— 人物、组织、工具等命名实体,放在
wiki/entities/ - concept(概念)— 关键概念、想法、模式、框架,放在
wiki/concepts/ - comparison(对比)— 相关概念或方法之间的对比分析,放在
wiki/comparisons/ - overview(概述)— 当源文件较多(≥5 篇)且跨多个主题时创建,提炼跨源文件的核心论点
目录结构应从内容中自然生长,不要强行套用固定模板。如果源文件只有几篇且主题集中,扁平结构就够用。
建页粒度
哪些实体建页、建什么样的页,按信息落点判断,不按实体类别。判据只问三个结构量,与语料领域无关——同一套规则适用于小说、史书、技术文档、论文任何语料;不要按"某类东西该建/不该建"的领域直觉分类(人物/地点/器物/概念在各自语料中的重要度完全不同,预置类别会把管线锁死在一种语料上):
- 覆盖深度:它出现在几个独立区段/章节(跨度)。可机械统计(census/segments)。
- 独立信息量:去掉它,已有页面是否仍能完整表达同等信息。若它的全部内容可并入一句描述 → 没有独立落点。
- 引用需求:其他页面是否把它的名字当作讨论对象、而不是行文背景来提及(判法见下)。
分三档:
完整页(任一满足):
- 出现在章节标题/目录中(强信号,必有内容可写)
- 能用
grep -nF 名字取到足够上下文写出有实质内容的独立描述(首次出现 + 身份归属 + 至少一个关键事件/关系),且跨度≥2 区段。跨度=1 的内容通常不足以当独立页——除非它承载了不可并入其他页的关键信息。
stub 页(内容撑不起完整页,但有引用需求):
- 格式:完整 frontmatter +
stub: true标记 + 一句话身份定性(必须 grep 取证,不靠记忆)+ 关键关系链接,3-8 行即可 - 判据只有一个:其他页面会主动链接它(它是文本中反复被"讨论"的话题,即使篇幅小)。判断方法见下一节"讨论对象 vs 背景挂点"。
- 图谱/问答链路需要这些中间节点(别处提及时钟链到它);缺了就是死链或漏链。
不建页(噪音):
- 通用词、尊称、无名字的泛称("某某的娘""某某家的"这类)→ 收纳进相关页的一句话或概念页表格行
- 仅作行文背景、不被讨论的名字(判据见下)→ 正文出现用纯文本,禁止建页、禁止在正文中以
[[ ]]出现 - 判定启发式:跨度=1 且内容可并入其他页面的一句话 → 不建页(信息已被承载,单独立页是重复,只会产生一张空洞的 stub 图)
讨论对象 vs 背景挂点(领域无关的二元判据)
对正文里每个候选名字的每一处提及,问:它是被"讨论"还是被"用作背景"?
- 讨论对象:句子以它为主语/宾语在陈述它的属性、行为、关系("X 在 N 处做了某事,因其 Y 而被 Z 所知";读者查它是因为它本身构成话题)。此类提及即使零散,也指向实体。
- 背景挂点:它只出现在方位/工具/附属语境里,删掉或换成泛指后句子语义不变("某事发生在 P 处""用 T 完成了……""安置于 Q"——P/T/Q 是描述其他实体的背景,读者查的是句子的主体而不是它)。
同一句话里两者可并存("发生在 R 的那个事件"——R 是背景;"事件本身"是讨论对象)。逐处判定时要看这句话的语义中心,不是按候选名所属的领域类别划分。拿不准时用删除测试:把该名字从所有出现处换成纯文本/泛指,若任何页面的可读性都不受损 → 背景挂点,不建页;若某处去掉后话题悬空(读者会问"它到底是谁")→ 有引用需求,建 stub 都值得。
不同源文件的粒度不同,按各自内容特征独立判断。census 频率是排序信号,不是建页阈值。建了 stub 不代表必须留着:收尾时对入度=0 的 stub 做孤儿审计(见步骤 3),确认是背景挂点误建就删除/降级为纯文本。
索引分层结构(根 INDEX + 各目录 INDEX)
索引固定分两层:根 INDEX 只给目录级概览(与页面总数解耦,始终几十行),每个内容目录有自己的 INDEX.md 列全该目录页面。检索层层递进:根 INDEX → 相关目录 INDEX → 页面。根索引若逐页罗列,页面一多就会长到 runtime 一次读不完(Read 上限 2000 行),整个问答链路随之失效——这是分层的原因,不要因为"库还小"而退回单索引。
根 `wiki/INDEX.md` 格式:
# Wiki 索引> 一句话全库概述 + 页面总数构成(源摘要 N · 实体 N · 概念 N · 对比 N …)## 概述-[[概述页]] — 一句话摘要(入口页,直接内联列出)## 目录-[[sources/INDEX|源文件摘要]](N 页)— 覆盖主题一句话概览 + 2-3 个代表条目名-[[entities/INDEX|实体]](N 页)— 人物/组织/工具;代表条目名-[[concepts/INDEX|概念]](N 页)— 覆盖领域;代表条目名-[[comparisons/INDEX|对比分析]](N 页)— 有哪些对比-[[questions/INDEX|归档问答]](N 页)— …(有页面时才列)
各目录 `wiki/<dir>/INDEX.md`(sources / entities / concepts / comparisons / questions)格式:
# <分类名>索引-[[页面]] — 一句话摘要
规则:
- 概述页(通常 1-2 篇、全库入口)直接在根 INDEX 内联,不为 overview/ 单建目录索引
meta/是元数据目录(lint 报告等),不建索引、不列入根 INDEX- 目录 INDEX 列全该目录页面(wikilink + 一句话摘要);条目多时用
##子标题分组(sources 按主题、entities 按人物/组织/工具、concepts 按领域) - 目录索引统一大写
INDEX.md,与多页主题目录的小写index.md入口页区分 - 单目录索引几百行 Read 一次可读完,不做二次分片
索引分层迁移(已有 wiki)
存量库若是旧单索引布局(根 INDEX.md 直接以 - [[页面]] — 摘要 逐页罗列、内容目录无 INDEX.md),不重建页面、只重构索引:
find wiki/ -name '*.md'一次,建「页名 → 所在目录」映射- 读旧根 INDEX.md,每条页面条目按映射分流写入对应目录的 INDEX.md(保留原摘要;条目多则
##分组) - 根 INDEX.md 按上节分层格式改写(概述条目内联,其余目录各一行:目录链接 + 页数 + 覆盖范围)
- log.md 记一条
index | 索引分层迁移
触发:用户说「重构索引 / 索引分层 / 索引迁移」。过程幂等——目录索引已存在即跳过。wiki-ingest / wiki-save 在旧布局库上首次触发时也会自动执行本流程(见各自 SKILL.md)。
wiki/log.md 格式
最新条目在最上面:
# 构建日志## YYYY-MM-DD HH:MM | build | 初始构建-扫描源文件数:N-创建页面数:N(按类型列出)-关键发现:一句话概述
超长源文件处理(prep + curation + 建页 + 对账)
源文件若无法在一次上下文内通读(通读后还要留空间规划+生成+交叉链接),不能按"读全文→语义识别"抽取实体,必须走本节的管线:确定性预处理把机械活(转码、切章、频率普查)做完,agent 只做语义工作(curation 审核、取证、撰写、链接)。
判断超长:wc -c 源文件 > 1.5MB(约 50 万中文字),或 Read 工具一次读不完(默认上限 2000 行)。未超长的源文件直接通读建页,不必走本管线。
步骤 0:断点检查 + 构建权(每次进入必做)
ls .molio/wiki-build/ 2>/dev/nullnode "<skill_dir>/scripts/prep.mjs" status <源文件> --vault .node "<skill_dir>/scripts/build-lock.mjs" acquire "build-<x>-<日期>" --vault .
- 若已有
curation-<x>.tsv/batches/:这是续传——不要重跑 prep/curation,直接从断点继续建页。 - 同一 vault 一次只允许一个构建会话。label 用 "build-<源文件主名>-<日期>" 固定,整个构建过程(含断开续传)都传同一 label。异 label 锁 → exit 1,不要抢。
- 构建结束时:
build-lock.mjs release "build-<x>-<日期>" --vault .
步骤 1:预处理(确定性,零 LLM)
node "<skill_dir>/scripts/prep.mjs" <源文件> --vault . [--profile <name>] [--charset <编码>]
- profile 选择:prep 自动检测(检测到章节结构会选
novel并在 stderr warnings 注明);你若通读源文件开头后判断不同(如它其实是访谈实录/史料),用--profile default覆盖。自定义 profile 可放scripts/profiles/*.json。注意:若 prep 回退到 default 且 candidates 为 0,说明 profile 不匹配——必须定制 profile 而非继续。 - 编码:GBK/Big5 等自动探测转码;若 warnings 报有损解码或 PUA 字符,用
--charset指定后重跑。 - 产物(
.molio/wiki-build/):transcode-<x>.txt(行号稳定地址)、segments-<x>.json(分段+范围)、census-<x>.json(频率普查+别名线索)
步骤 2:L1 章节 digest(覆盖全文)
每个处理范围(segments.json 的一个 range,≤15 万字)派一个 subagent,只 Read 自己范围的行号区间,输出 digests/R###.md(实体+定性+事件+行号+别名线索)。digest 文件落地 = 该范围完成的唯一信号。主 agent 不通读全文。
步骤 3:curation(信息落点判据,脚本预填 + agent 审核)
这是整条管线的核心决策环节——决定建哪些页、什么类型、怎么分批。
# 3a. 脚本预填:census → 草稿 TSV(机械映射,零 LLM)node "<skill_dir>/scripts/curate.mjs" draft <x> --vault .# 3b. agent 审核草稿:按「建页粒度」判据筛选、分组、标注页类# 产出: curation-<x>.tsv(审核后的最终版)# 3c. 脚本分批:校验格式 + 按 # cat= 分组 + 单批超 15 条自动拆分node "<skill_dir>/scripts/curate.mjs" split <x> --vault .
批次 TSV 格式(制表符分隔,每行 5 列):
# cat=<类别标签>名字 定性 别名: X/Y/Z 证据行号: N,N,N 页类: entity|concept
agent 审核时做什么(脚本做不了的语义判断):
- 按「建页粒度」的信息落点判据筛选:删噪音、删背景挂点、保留有引用需求的
- 按主题分组(
# cat=帝系/# cat=概念与制度页等)——批次即类型标签 - 每行标注
页类: entity或页类: concept - 补全别名(从 census.aliasHints / digest 中认领)
- 补全证据行号(代表性行号,不是全量)
脚本预填做什么(机械部分,agent 不需花 token):
- 按 count 排序、过滤低频噪音
- 预填唯一匹配的别名
- 预填证据行号(census 有则填,无则留空)
批次规模:留白为主(按主题自然分组),脚本兜底(单批超 15 条自动拆分)。
步骤 4:建页(每批一个 subagent)
每个批次派一个 subagent,读批次 TSV 该批行 + transcode 取证 → 写 drafts/<名字>.md。
- 一个 subagent 只做一批,即读即写,不要预先规划多个批次(防 32k 输出上限)
- prompt 按
页类给模板:concept 页写综合论述,entity 页写身份+事件+关系 - 建页 agent 只能链接批次 TSV 里出现的名字(白名单),清单外用纯文本
- 高频名取证封顶:首现 + 均匀采样,总量 ≤30 条
- 并发 ≤5-6 个 subagent(防 API 429 限流);完成度以磁盘落盘为准,不信通知
- 全部批次完成后跑落盘对账门禁(硬性,exit 1 即中止):
``bash node "<skill_dir>/scripts/verify-drafts.mjs" <x> --vault . ``
- 校验批次 TSV 声明的每个名字在
drafts/有对应文件且非空 - 治"subagent 输出超限被截断后静默缺失"和"报告写了卫青、文件实际不存在"——超限与漏写最终都表现为"批次声明了但没落盘",一个门禁统一定位
- 缺页 → 补建后重跑,直至 exit 0
步骤 5:安置(确定性,零 LLM)
node "<skill_dir>/scripts/place.mjs" <x> --vault .
- 按批次 TSV 的
页类列分发:entity →wiki/entities/,concept →wiki/concepts/ - 生成/更新对应目录的 INDEX.md(摘要自动提取)
- 幂等可重跑;中途打断后续传只需重跑 place
步骤 6:对比页策划(主 agent 创作,数量不固定)
实体/概念页安置完成后,主 agent 从已建页面中识别值得横向对比的主题,写 comparison 页到 wiki/comparisons/(由主 agent 直接写,不派 subagent——数量少、需全局视野,subagent 没有这个上下文):
- 数量由知识网络结构决定,不设固定数:有多少值得对比的主题就写多少——有天然对比关系(同类的并置、竞争、沿革、对立)才写,宁缺毋滥;没有就不写。不同语料的对比主题多寡差异很大,以实际建出的页面网络为准,不预设数量。
- 判断标准:两个及以上实体/概念之间存在可比维度(成败、风格、制度沿革、思想对立),且对比本身能产出超越单页的新洞察——单纯罗列不是对比,不写。
- 每页带完整 frontmatter(
type: comparison)+ 交叉链接到被对比的页面,并更新wiki/comparisons/INDEX.md。 - 这一环节是 agent 的收尾创作,不是从 census 候选筛出来的,所以不在批次 TSV 里。
步骤 7:链接对账(硬性门禁,exit 0 才算通过)
node "<skill_dir>/scripts/linkpass.mjs" --vault . --batches .molio/wiki-build/batchesnode "<skill_dir>/scripts/deadcheck.mjs" --vault .
linkpass --batches:从批次 TSV 的别名列读取别名映射,把每个页面名/别名在其他页面正文中的首次出现包成[[ ]]。庙号/别名死链在这一步自动消解(如"项王"→[[项羽|项王]])。每次运行还会顺手机械清理旧版遗留的[[T|Y]]Y]]双重包裹残渣(→ 纯文本Y;代码块/引文/frontmatter 内的原样保留),防止残渣随页面拷贝自我复制- CJK 词中防护:≤3 字的中文名/别名前后都紧贴汉字时视为词中子串,跳过不包(宁可漏链,不可错链——"心理"在"核心理念"里不包,"网络"在"神经网络"里不包)。页面名也不例外:
[[网络]]能正常解析,包错地方 deadcheck 看不出来,是静默损坏。注意:跳过的是该名字在本页的唯一链接机会(只包真首次出现),不是只跳过这一处。要恢复旧的全量包裹行为加--no-cjk-guard - 前缀缺口 → `wiki/.linkpass-compounds.json`:上面那条要求前后都是汉字,所以当名字后面跟汉字、前面是空格或标点时会失效——
第六章 平面向量被包成[[平面]]向量、,对数学被包成[[对数]]学。这属于「名字是更长词的前缀」。通用解法需要一张汉语虚词表,而虚词表会误伤大量正常链接([[平面]]内的点是对的)。折中办法是由 vault 自报:在wiki/.linkpass-compounds.json里列出实际踩过的对——
``json { "对数": ["学"], "平面": ["向"] } ` (名字 → 其后不该切断的字)。file 存在则 linkpass 自动读取、叠加到现有防护上,零误伤;不存在则行为与从前完全一致。 **怎么知道该加什么**:收尾时扫一遍 [[X]]Y` 形态(X 是 ≤3 字中文名、Y 是汉字)逐条人工判断;或留意建页 agent 的回报——实测一轮里多个 agent 各自撞上并手工改过同一类错链
- 别名与页面名撞名:页面名优先(
[[网络]]本就解析到网络.md),撞名别名不会包成链接,会计入运行报告的collidedAliases——curate 时应把这类别名改掉 deadcheck:exit 0 才算通过。有死链 → 补页或改写链接,重跑直至 exit 0;检出包裹残渣只报告不阻断(下次 linkpass 会自动清理)。代码块里的[[...]]是字面文本,不算死链也不参与清理;代码块/引文/frontmatter 内的残渣单独标注(residueProtected),linkpass 不会动它们,需要时手工清理。引文与 frontmatter 里的死链照常拦下(这些区域渲染时链接仍然生效),而这些区域所有自动工具一个字都不能动(要供prep.mjs verify逐字节核验),所以命中时的唯一出路是补一个页面——哪怕只是占位 stub
步骤 8:引文核验(确定性,零 LLM)
node "<skill_dir>/scripts/sweep.mjs" <x> --vault .
- 全量 verify + missing 分类:
wikilink(链接误报)/blockquote(整行提取噪音)/pua(转码差异)/real(真错) - 只修 `real` 类:写进
rules.json的repair.repl,跑repair.mjs双份修复(wiki + drafts) - 其他三类是机械噪音,不需要修
- 含图 / 数学语料的额外两类预期噪音(实测一个 35 页教材章节报 98 条,逐条查完全部属于这两类):
- 引号被当作强调用——中文里「」/"" 既表示引用也表示强调("同时满足两个条件"这种)。verify 只看字符,分不出意图。约定:只有原文引用才用引号,强调一律用粗体 `…`;建页 agent 必须遵守,否则门禁会被自己的强调语淹没。
- 从旁批小贴士图片转写的文字——这些字只存在于图里,transcode 里没有(版式模型把带底色的文字框裁成了图)。它们天然无法核验,属于预期噪音,不必修。
- 含公式的引文(
$…$)已由prep.mjs自动跳过:页面上的公式是按数学含义重写的干净 LaTeX,与 OCR 原文在字符层面必然不同。
收尾
- 创建/更新根 INDEX.md + log.md + hot.md
build-lock.mjs release- 汇报(页面数必须用
find wiki -name '*.md' | wc -l实时统计,禁止凭记忆报数)
多源文件场景:每个源文件各一套过程文件,逐个走完整管线;链接对账是全库级的,所有源文件安置完后跑一次即可。
源文件侦察(任何构建的第一步)
不要看到"同一出版社的 PDF"就套用同一套参数。 实测人教 A 版高中数学全套五册,情况全不一样,而选错管线的代价是十小时 vs 五十分钟,或者一整本满篇错字的 wiki:
| 性质 | 现象 | 走哪条路 | |
|---|---|---|---|
| 扫描版 | get_text() 几乎为空(整册可能只有几十个字符) | 必须 OCR(步骤 A) | |
| 电子版·文字层正常 | 每页数百字,抽出来能读通 | 直接抽文字 | |
| 电子版·文字层坏 | 字数看起来完全正常,但正文读不通 | 必须先清洗(见下) |
一段脚本就能定性:
python - <<'EOF'import fitz, globfor f in sorted(glob.glob('*.pdf')):d = fitz.open(f); n = d.page_counttot = sum(len(d[i].get_text().strip()) for i in range(n))empty = sum(1 for i in range(n) if len(d[i].get_text().strip()) < 20)fonts = set()for i in range(0, n, max(1, n // 10)):for fo in d[i].get_fonts(): fonts.add(fo[3])print("%-32s 页数=%3d 字符=%7d 均值/页=%4d 近空白=%2d" % (f[:32], n, tot, tot // max(n, 1), empty))print(" 字体:", sorted(fonts)[:8])EOF
均值/页 < 100→ 扫描版;均值/页正常但抽查一段读不通 → 文字层坏。- 教材页 = PDF 页 − 偏移 是后续所有定位的基础(实测各册偏移 5 或 7)。在建页 brief 里给页码前必须先核对,见「分派多批 subagent」一节。
电子版「坏字体文字层」——本库踩过的最深的坑
方正书版(及其他一些中文排版工具)给数学/正文用了一组子集字体,其 ToUnicode 映射是坏的:数学斜体字母被整体映射到不相干的汉字。
实测人教 A 版必修第二册:$A$→犃、$a$→犪、$x$→狓、$\boldsymbol e_1$→犲1;一批符号($\to$、$\subset$、$\Rightarrow$、$\varnothing$、$\sqrt{\ }$、$\parallel$)落进私用区。正文原样读出来是「小船由犃地向东南方向航行」。
为什么危险:get_text() 的字数完全正常,不逐字读根本发现不了。照"电子版直接抽文字"做,会产出一整本满篇错字的 wiki。
识别:按字体名筛。坏字体是一小组命名可疑的子集字体,实测形如 E-BX9-*、E-HX9-*、E-HZ9-*、O9-*、FzBookMaker*。判据是:某字体同时承载 ASCII 与 CJK 汉字,且汉字样本是"犃犅犆犪犫"这类生僻字——正常中文字体不会这样。
清洗:用 scripts/lib/fontmap.py。
python "<skill_dir>/scripts/lib/fontmap.py" scan <pdf> # 找坏字体与坏字(带上下文)python "<skill_dir>/scripts/lib/fontmap.py" dump <pdf> <outdir> # 逐个坏字裁成放大 PNGpython "<skill_dir>/scripts/lib/fontmap.py" clean <pdf> <out.md> --map map.json [--offset N]
scan 认出坏字体的判据是独有性:它渲染的字符几乎没有别的字体渲染(分 正文字体与标题字体都画,犪 只有那个数学斜体子集画)。这个信号与语言、与字体名都无关,所以换出版社也成立。
别用"这个字体字形少"当判据。 小样本下正常的标题字体字形同样很少,会把 分/的/平面 这类正常汉字误报成坏字——这个坑实测踩过。scan 默认扫全册正是为此:独有性要靠覆盖率才准。
然后四步:
- 看
scan报的「需要人工定映射」那一档(私用区 + 被当字母用的汉字码位)。它已经把全角标点(clean会自动转半角)和本来就合法的符号(≤ ∑ ∈ ×)分出去单列了,不必逐个看; - 对每个坏字看
scan给出的上下文——绝大多数字母靠上下文就能认出来(向量也可以用字母犪,犫,犮,…表示); - 拿不准的用
dump裁成放大图逐一看过; - 写成
map.json(形如{"犃":"A","犪":"a","狓":"x"}),跑clean。
几条经验:
- 同一出版社同一套排版,字体名一致则映射表可跨册复用——本库选必三直接复用了必修二定出的表。先试复用,不行再重新定。
- 清洗后仍会剩约 1% 的未识别字符,它们几乎全部落在图内部(坐标轴标签、图注),不用管:图本就当图片用,caption 由建页 agent 开图据实撰写。
- 顺带把控制字符一律丢弃;图注里的括号对字形(如
U+1F46/U+1F47)还原成()。 - 不要"猜"某个坏字是什么。
犪到底是a还是别的字母,只有把字形裁出来看过才知道。按上下文猜字母,猜错一个、整本书的相关公式全错。
含图源文件(图版)处理
触发:源文件里有图,且图承载信息——扫描版教材、试卷、PPT、带插图的论文/报告。数学、物理、医学、工程类语料尤甚:图形往往就是那一页的正文,丢了图,页面就只剩空话。纯文字源跳过本节,直接走上面的管线。
图不是附属品,是一等公民:它要有稳定的存放位置、可解析的引用语法、和页面同等强度的对账门禁。
硬规范:引用语法只有一种
![[wiki/images/<源文件主名>/007.png]]![[wiki/images/<源文件主名>/007.png|400]]*图 1.3-4 补集示意图*
- 必须是 `![[ ]]` 嵌入,不能是 `[[ ]]`。 不带
!的链接在图谱构建里会被拒(graph.ts硬拒附件扩展名),变成一个 dead 节点。 - 必须写完整 vault 相对路径,正斜杠,不带前导
/。Obsidian 官方文档就是这个形式(路径从 vault 根开始、Windows 上也用正斜杠),Molio 也按同一形式解析。 - 生成页面统一使用 `![[完整 vault 相对路径]]`。 Molio 也支持普通 Markdown 图片,但
按笔记所在目录解析;移动页面后容易失效。media.mjs check会校验两种写法的本地引用。 - caption 写在图片下一行,用
*...*斜体。![[ ]]的竖线只吃尺寸(|400/|400x300),不吃 alt 文本。 - 图片绝不能放进 `.molio/`——点前缀目录会被文件树剪掉,图在界面上完全不可见。
- 单个源目录图数别超 5000:Molio 每目录上限
MAX_DIR_ENTRIES=5000,超了整个目录被剪掉,图全部消失。media.mjs collect会在超限时报错并提示分片。
步骤 A:抽出图(docling)
按 docling skill 转换源文件,必须带 `--image-export-mode referenced`(默认的 embedded 会把图以 base64 内联进 markdown,产出几百 MB 的 md,且图没有独立文件):
docling "<源文件>" --to md --image-export-mode referenced --output .molio/docling
- 长文档必须按页分块跑,因为 docling 只在全部跑完时才落盘——中途被打断就整批作废(实测 270 页那次跑到 20 分钟被中断,20 分钟算力全丢、一个文件都没写出来)。分块后每块独立落盘,中断只损失当前块。
- 用脱离进程跑,不要挂在前台/后台任务里。后台任务在 15–20 分钟前后会被中止(实测 9.7 分钟完成、10.6 分钟完成、14 分钟完成;预计 ~20 分钟的都被中止),而 docling 慢起来单块轻易超过这个时间。用
Start-Process(Windows)/nohup … &(POSIX)起独立进程、日志落文件,就不受任务生命周期限制:
``powershell Start-Process -FilePath docling -ArgumentList @("convert",$pdf,"--page-range","126-149", "--to","md","--image-export-mode","referenced","--ocr-lang","ch_sim,en", "--enrich-formula","--output",$out) -WindowStyle Hidden -RedirectStandardOutput "$log" -RedirectStandardError "$err" -PassThru `` 之后靠**轮询产物目录**判断完成(find <out> -type f | wc -l`),不要靠任务通知。
- 吞吐差异极大,按时间而非页数估算:本册实测 16.6 → 91 秒/页(第一章 35 页 9.7 分钟;第三章 33.6 秒/页;第四章公式最密,91 秒/页)。公式越多
--enrich-formula越慢。脱离进程跑时块可以开大到 25-30 页,减少 docling 重复加载模型的开销;若挂在任务里则必须切到 10 分钟以内。
分块后每块的图在各自的 *_artifacts/ 目录,用 collect 逐块收。是否加 `--append` 的唯一判据是 `media-<主名>.md` 是否已存在——已存在就必须全程 --append:
``bash # 全新集合:第一块不加参数,后续每块加 --append node "<skill_dir>/scripts/media.mjs" collect .molio/docling/chunk-1 <主名> --vault . node "<skill_dir>/scripts/media.mjs" collect .molio/docling/chunk-31 <主名> --vault . --append # …编号自动接着排;media-<主名>.md 按块顺序拼接 ``
⚠️ 续传 / 追加到既有集合时,第一块也要 `--append`。"第一块不加参数"里的"第一块"指的是这个集合的第一块,不是这次任务的第一块——实测踩过:给已有前四章图版的集合追加第五章时,把新块当成"第一块"不加参数,137 张既有图被清空并从 001 重编号(图内容全变了,页面里的图引用静默指向了别的图)。collect 现在会在这种情况下拒绝执行并要求显式 --force。
- 扫描版(无文字层)会自动走 OCR;中文务必带
--ocr-lang ch_sim,en。 - 首次跑 PDF 会下载 ~500MB 模型。卡住不动≠算不动:若进程 CPU 时间长时间不涨却挂着一条 HTTPS 连接,是在下载模型,等它下完即可;国内先设
HF_ENDPOINT=https://hf-mirror.com。 - 公式:先决定要不要花这个时间。 不加
--enrich-formula时,版面模型判为公式的区域会变成<!-- formula-not-decoded -->;加了它,这些内容被还原成 LaTeX。
代价实测差 17 倍(人教 A 版扫描版):带 --enrich-formula 是 149 秒/页(268 页 ≈ 10 小时),不带是 8.6–11 秒/页(同样 268 页 ≈ 50 分钟)。所以"不加就彻底丢失"这个说法虽然对,但结论下早了——公式可以找回来。
两条路,按语料选:
- 公式密集且正文不复述 → 带
--enrich-formula,慢但一次到位。 - 公式能从别处找回 → 不带,改用整页渲染图兜底(见「步骤 B」的整页渲染)。成立条件是:① 教材类语料的正文常把公式用文字复述一遍("存在唯一的有序实数对 $(x,y)$,使得【公式】");② 建页 agent 能打开那一页的原图把公式读出来。实测两册 268 页全程走这条路,多个 agent 独立确认"所有未解码占位均已按整页图读出并写全"。
只要选了不带,就必须先渲染整页图,并把映射规则写进建页 brief(教材第 $N$ 页 → page-(N+偏移).png)——否则 agent 看到占位符只能凭记忆编公式。
带 --enrich-formula 时,还原质量参差,必须按"带噪音的素材"对待:
- 公式框里混进中文时,模型会输出
\mathbb{X}\mathbb{I}\mathbb{I}\mathbb{I}…这类幻觉串(连续重复的\mathbb{I}是它的标志)。它们是噪音,直接忽略。 - 运算符会被认错:补集符号
∁实测被识别成[_。关键符号要对照同页的图形裁切核验,不要照抄 LaTeX。 - 好在简单公式还原准确(
p \Rightarrow q、A = \{1,2,\cdots,n,\cdots\}都对),且不会破坏原本干净的 OCR 正文段落。 - 用法:enriched LaTeX 只是给建页 agent 读的原始素材,页面里的公式一律由 agent 自己写成干净记号——不要原样搬进 wiki 页面。
步骤 B:归置与规范化(确定性)
node "<skill_dir>/scripts/media.mjs" collect .molio/docling <源文件主名> --vault .
做四件事:按文档顺序重命名为 wiki/images/<主名>/001.png;丢掉短边小于 64px 的裁切(版面模型把杂线、页码当成图,实测真图 ≥200px、噪点 ~40px);配对 caption;产出图片引用已规范化的 `media-<源文件主名>.md`。
media.mjs collect 的产物才是给 prep 的源——图标记随行号留在 transcode 里,下游 digest/建页 agent 在正文流中就能看到图,不需要额外管线:
node "<skill_dir>/scripts/prep.mjs" ".molio/wiki-build/media-<主名>.md" --vault .
整页渲染(两种用途,都别省):
node "<skill_dir>/scripts/media.mjs" pages "<源PDF>" "<主名>-整页" --vault . [--pages 42,57-60] [--dpi 110]
- 裁切失效时的兜底:docling 的版面模型偶尔会把图并进正文而漏检测,那页的图就彻底丢了。整页图引用同理(
![[wiki/images/<主名>-整页/page-042.png]])——扫描版里"整页"本身就是可用的图版,宁可用整页图,也不要丢。 - 公式找回:走「不带
--enrich-formula」那条路(见「步骤 A」的公式一节)时,必须先整册渲染。--dpi 110足够读公式,比 150 快不少。这些整页图是给建页 agent 读的素材,不贴进 wiki 页面——在建页 brief 里写明映射规则(教材第 $N$ 页 →page-(N+偏移).png)并点明"不要贴整页图"。
与抽图的数目关系:整页图通常全册每页一张(扫描版 154 页就产生 154 张),它们天然是"孤儿图"(不被任何页面引用)。media.mjs check 报出的孤儿数里包含这一大块,收尾汇报时必须把它单独说明,否则看起来像建页时成批漏用了图。
步骤 C:建页时用图(这一步的价值所在)
- digest / 建页 agent 在 transcode 里看到
![[wiki/images/...]]标记,就知道该处有一张图、caption 是什么。 - 每一张裁切都要 Read 过再归类——不是所有裁切都是图。 实测三类(
min-px阈值挡不住后两类,它们尺寸正常):
| 类别 | 长什么样 | 怎么处理 | |
|---|---|---|---|
| 真图形 | 函数图象、几何图、Venn 图、示意图、图表 | 贴到它说明的那一页,caption 看图写 | |
| 旁批/小贴士文字框 | 教材侧边的带底色小框,里面是可读的文字("常见的全称量词还有…"、"这个方法就是举反例") | 读出文字写进正文,不要当图贴——贴一张文字截图毫无价值 | |
| 装饰图 | 章首跨页照片、纯配图 | 不贴;收尾报告里列为"装饰图 N 张" |
判据是看它承载信息的方式:内容能用文字无损表达 → 是文字框;只能用图形表达(形状/相对位置/走势)→ 是真图形。分错的代价:文字框当图贴 = 一堆没用的截图;真图形当文字框 = 又变成"望文生义写图"。
- caption 必须看图写,不许望文生义:图的相关页面里,用 Read 工具直接读图片文件(agent 能看图),据实写下图里画的是什么。数学图形靠文字上下文猜必然出错——这是图片能力真正的价值,别退化成"把路径贴进页面"。
- 每张图应当被内容需要才贴:贴在它真正说明的那个概念/结论旁边,不要在一个页面里堆图集。
- 同一张图可以被多个页面引用(共享的示意图/总览图),不必复制文件。
步骤 D:图版对账(门禁)
node "<skill_dir>/scripts/media.mjs" check --vault .
- 每个
![[图片]]和普通 Markdown 本地图片引用必须解析到真实文件;悬空或歧义引用 exit 3,必须修到 exit 0。 - 顺带报告孤儿图(在盘上但没有任何页面引用)和裸文件名歧义。孤儿图不是门禁失败(可能留给后续 ingest),但成批孤儿通常说明建页时漏用了图,值得回看。
deadcheck.mjs已把图片嵌入纳入同一门禁(它复用同一套解析逻辑),所以收尾的deadcheck也会拦下裂图;media.mjs check的额外价值是孤儿图与歧义名报告。
收尾对账(图版相关,必报)
- 抽出图数 / 被页面引用数 / 孤儿图数
deadcheckexit 0 且media.mjs checkexit 0
多源文件场景:每个源文件各跑一次 media.mjs collect(用不同的 `<主名>`,图片各自成目录);media.mjs check 是全库级的,所有源安置完后跑一次即可。
分派多批 subagent 时按「完整小节」切,别切碎插页
一章内容多时按小节分派多个建页 agent,但切分点必须落在小节之间。教材里「阅读与思考」「探究与发现」「信息技术应用」「文献阅读与数学写作」这类插页常夹在两节中间,一旦被切到某一批的范围之外,那批 agent 就看不到它——实测发生过一次:某页的原始素材落在别批的行区间里,agent 只看到同名的写作任务框架,于是凭通用数学史补写了整页内容。虽然内容大体不错,但它不是本库源文件的产物,且交付时不易察觉。
做法:定行区间时先扫一遍 ^## 标题,确保每个插页完整落在某一批内;拿不准就把它并进相邻批次,并在 prompt 里点名"本批含 XX 插页"。
范围标注必须可验证。 发 brief 时往往会写"你这批对应教材 XX–YY 页"——这个页码标注要么核对过,要么就别给。实测踩过:行区间(agent 真正据以读取素材的依据)是对的,但顺手写的页码区间整批都标错了;好在多个 agent 发现"按标签去找的整页图根本不存在"后,改用转码里 OCR 出来的页脚印刷页码自我校准才没出事——但这是运气,不是设计。
两条做法:
- 优先给可自查的定位依据,而不是结论性页码:写清「教材页 = PDF 页 − 偏移」这条规则,外加一句「以转码里 OCR 出来的页脚页码为准」,让 agent 能自己验证。给规则比给结论更抗错。
- 确实要标页码时,先抽查一页确认(渲染该页看页脚数字)再写进 brief。
- 同理:行区间才是权威,页码只是辅助。prompt 里要明确说"按行区间取材",否则 agent 可能反过来被错误页码带偏。
操作步骤
- 扫描 + 侦察:列出 vault 中所有源文件(跳过隐藏文件、
wiki/和.molio/本身),了解全貌,用wc -c标出超长文件;紧接着对每个源文件做一次侦察(见「源文件侦察」)——扫描版 / 电子版正常 / 电子版坏字体三者的管线完全不同,选错的代价是十小时,或一本满篇错字的 wiki。含图源文件(扫描版 PDF、PPT、带插图的文档)→ 走「含图源文件(图版)处理」 - 断点检查 + 构建权 + 预处理:超长文件先走"超长源文件处理"步骤 0→0.5(build-lock acquire)→1;含图源文件先走「图版处理」步骤 A→B 拿到
media-<主名>.md再对其跑 prep;普通文件通读 - 规划:列出打算创建的页面清单和类型,确定目录结构
- 检查已有 wiki:如果 wiki/ 已存在,先读根 INDEX.md + 相关目录的 INDEX.md 了解已有内容,只新增/更新,不重复创建;若发现是旧单索引布局(根 INDEX 逐页罗列、无目录索引),先按「索引分层迁移」重构索引再继续
- 生成页面:普通文件逐个建页;超长文件按 L1 digest → L2 主表+建页的分层流程执行(merge-master 合并、batcher 分片、G1/G2 门禁)
- 完成自检:
prep.mjs status确认目标层级达标 + 孤儿 stub 审计,未达标继续补建 - 引用抽查:
sweep.mjs全量 verify →repair.mjs(rules.json 修复表)修正失准引文,重跑直至归零 - 链接对账:
linkpass.mjs补漏链 →deadcheck.mjs门禁,必须 exit 0;有死链先补 stub 或改写再重跑;含图库再加media.mjs check,必须 exit 0(悬空图片引用 exit 3) - 创建/更新根 INDEX.md + 各目录 INDEX.md(分层结构,目录行页数在此统一重算,stub 页也要列入)、log.md、hot.md
- 收尾:
build-lock.mjs release(释放构建权) - 汇报:页面数量、核心论点摘要、剩余知识缺口(从不被链接的 L3 长尾;高频实体应已在自检中补全);含图库还要报抽出图数 / 被引用数 / 孤儿图数;汇报必须包含"死链 0(deadcheck 通过)",含图库另加"图片引用 0 悬空(media check 通过)"
页面内容要全面但简洁,优先保证准确性。