# 格式契约(改之前先读这里) 这份文件记录**为什么**输出格式长成这样,以及**谁在依赖它**。 这些形状不是审美选择,是被下游归档管线反向约束出来的。 改动任何一条之前,先确认下游有没有依赖。 ## 谁依赖这些格式 | 依赖方 | 依赖什么 | |--------|----------| | 归档替换脚本 | 靠**字符串字面量**把草稿替换成完成态 | | 批次校验器 | 正则校验要点行、标签白名单、status | | 要点重排工具 | 只识别 `\|- [MM:SS]` 开头的行 | | 本 skill | 生成草稿与完成态笔记 | > 如果你要接入自己的 vault,这些约定可能与你现有的笔记规范不同。 > **用之前先确认一遍**:把 `scripts/bili_note.py` 的要点行格式、frontmatter 字段 > 与你库里已有笔记对齐,或者改 `docs/FORMAT.md` 记录你自己的约定。 ## 1. frontmatter:三处字面量是硬要求 典型的归档替换脚本用的是**字符串替换**,不是 YAML 解析: ```python "status: draft" -> "status: completed" "needs_summary: true" -> "needs_summary: false" "tags: []" -> 'tags: ["a", "b"]' ``` 所以下面三处**必须逐字一致**,任何"优化"都会让笔记静默地补不上内容: - `status: draft` —— 不能写成 `status: "draft"` / `status: 草稿` - `needs_summary: true` —— 必须是裸布尔值,不能加引号 - `tags: []` —— 必须是空数组字面量。**不要**因为"留空更好看"改成 `tags:` 空值 其余字段(顺序也照抄): | 字段 | 形状 | 说明 | |------|------|------| | `title` | 裸标量,不加引号 | | | `source` | `https://www.bilibili.com/video/{BV}` | 必须完整 URL 且**不带追踪参数**;裸 BV 号会让 Obsidian 不可点击,带 `?spm_id_from=` 会让下游按 URL 正则匹配不到 | | `author` | 裸标量 | 空则写 `未知` | | `captured` | `YYYY-MM-DD` | | | `duration` | **双引号字符串** | `"5:24"` / `"2:10:35"`。引号是必需的:不加引号时 `38:46` 会被 YAML 1.1 当成六十进制整数 | | `type` | 固定 `video-note` | | | `status` | 草稿 `draft`;完成 `completed` | | | `needs_summary` | 裸 `true` / `false` | 草稿 true,完成 false | | `tags` | 数组 | 草稿 `[]`;完成态由归档管线填 | `---` 结束后**紧接** `> [!info] 视频信息`,中间不留空行。 ## 2. 要点行:`|- ` 行首不可改 ``` |- [MM:SS](https://www.bilibili.com/video/BVxxx/?t=20s) 说明 ``` 这不是合法的 markdown 表格行(没有结尾竖线、没有表头分隔行),但**必须保持**: - 下游校验器的正则 `^\|- \[[0-9]{2}:[0-9]{2}\]` 只认这一种形状。 - 写成标准列表 `- [...]` 会让笔记被判「要点不足」。 测试套件里 `TestDownstreamContract` 用同一条正则验证输出, 任何让要点行偏离契约的改动都会当场失败。 ## 3. 时间戳:两个字段两套规则 这是最容易搞混的地方——**同一个秒数,在两处写法不同**: | 用处 | 格式 | 例 | 函数 | |------|------|-----|------| | frontmatter `duration` | 小时进位 `H:MM:SS` | `2:10:35` | `fmt_ts()` | | 要点行标签 | **总分钟数**,不进位 | `93:31`(不是 `1:33:31`) | `fmt_bullet_ts()` | 跳转链接用一个形态: ``` {视频URL}/?t={整数秒}s # 注意 URL 和 ? 之间有一个 / ``` 因此 `source` 里绝不能残留查询参数,否则会拼出含两个 `?` 的坏链接。 `norm_url()` 负责剥掉 `?spm_id_from=…&vd_source=…`,**但保留 `?p=N`**—— 它是分 P 选择器,不是追踪参数。 ### 超过 99 分钟:3 位分钟是正确输出 要点标签用总分钟数,所以 100 分钟以后必然出现 3 位(`[100:00]`、`[123:49]`)。 实测 2:10:32 的合集里 **24%** 的时刻如此,最早在 100:00。 ⚠️ **下游校验器的正则 `^\|- \[[0-9]{2}:[0-9]{2}\]` 只认 2 位分钟,会漏掉这些行**, 把长视频笔记误判成「要点不足」。这不是格式错误,两边的产出者是同一个格式约定; 修法是下游把 `{2}` 放宽为 `{1,3}`(`bilibili2obsidian` 侧的 `_verify_batch.py` 与 `_sort_bullets.py` 已改)。本 skill 的 `verify` 对这类笔记放行但打印警告。 ## 3b. 多 P 合集:时间戳是平移过的 合集不是一条原生时间轴,而是逐分 P 抓取后**按累计时长平移拼接**的: ``` P1 段原时间戳 + 0 P2 段原时间戳 + P1 时长 P3 段原时间戳 + P1 时长 + P2 时长 ... ``` 因此: - **某个分 P 没字幕时,它占用的时间必须留在时间轴上**(否则它之后的所有时间戳 都会前移,跳到错误的画面)。跳过的分 P 在 `manifest.parts[].skipped` 标出。 - 跳转链接仍然指向 `{合集URL}/?t={累计秒}s`。B站整P播放时累计时间轴成立; 但若用户是**单独打开某个分 P**,该链接不会精确落到那个分 P 的位置。 这是合集的固有限制,不是可以修掉的 bug。 - ⚠️ 选取字幕文件时必须精确匹配 `_pN.{lang}.srt`。**不要**用 `{bvid}*.srt` 这类 glob —— 合集下它会匹配到全部分 P,然后永远取到 `_p1`, 合成结果"看起来正常"但内容是同一个分 P 重复 N 次。`find_duplicate_part_texts()` 就是为了拦这个。 ## 4. 章节骨架 草稿按此顺序产出,`## 相关卡片` **不出现**在草稿里(由归档阶段追加): ``` > [!info] 视频信息 ## 一句话摘要 ## 核心要点 ## 总结 ## 笔记标签 ## 相关卡片 <- 仅完成态,且只在确实有卡片时 ## 原始字幕 ``` - `## 原始字幕` 下必须是 `> [!quote]- 完整字幕(点击展开)`(全角括号,`-` 使其默认折叠),正文每行以 `> ` 开头。 - 视频信息 callout 的分隔符是**全角** `|`(U+FF5C),冒号是全角 `:`(U+FF1A)。 ## 5. 文件系统与 Obsidian 坑位 - **方括号 `[ ]`**:wikilink 定界符,必须从**文件名**里去掉。`title:` 里可以保留,所以 `title` 与文件名可能不一致,做双链时要用**文件名**。 - **`#`**:被 Obsidian 当作标题锚点分隔符,会把链接从 `#` 处截断。已加入清洗列表。 - **ASCII `|`**:文件名会被清洗掉,但 `title:` 里可能有,同样导致 `title != 文件名`。 - **全角 `【】「」`**:不是定界符,B 站标题里常见,**不能误删**。 - **BOM**:**不要**用 PowerShell `Set-Content -Encoding UTF8` 写文本(会带 BOM 污染首行),用 `[System.IO.File]::WriteAllLines` + `UTF8Encoding($false)`。 ## 6. 改动后的回归检查 ```bash python -m unittest discover -s tests -v # 离线:解析 / 格式化 / 契约 / 落盘 python scripts/bili_note.py verify --note <笔记> ```