# bilibili-note > **这是插件封装版**,衍生自上游原件 [`3588834014-del/bilibili-note`](https://github.com/3588834014-del/bilibili-note)。 > > 本仓库 = 上游内容 + DSH bundle 插件封装(`package.json` / `cordis.patch.yml` / `lib/index.js`)。 > 只想要 skill 或 CLI、不需要当插件装的,请用上游原件。 > GitHub 不允许给自己的仓库建同名 fork,所以这里是**衍生副本**,不是 fork 网络里的 fork。 > 把 B站视频链接变成 Obsidian 结构化笔记的 **CLI** 与 dsh skill。 > **只拉字幕,绝不下载视频。** > > English: [README.md](README.md) [![CI](https://github.com/3588834014-del/bilibili-note/actions/workflows/ci.yml/badge.svg)](https://github.com/3588834014-del/bilibili-note/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) [![Python 3.8+](https://img.shields.io/badge/python-3.8%2B-blue.svg)](https://www.python.org/) --- ## 它解决什么 把一条 B站视频链接固化成 vault 里的结构化笔记。反复执行的机械环节交给脚本, 需要判断的内容交给 agent,两边不混: ```mermaid flowchart LR A[B站链接] --> B["fetch
yt-dlp 取元数据 + ai-zh 字幕"] B --> C["manifest.json
字幕全文 + 秒级段落"] C --> D["agent 生成判断性内容
摘要 / 要点 / 总结 / 标签"] D --> E["write
组装 + 写入 vault"] E --> F["verify
结构自检"] ``` | 环节 | 谁做 | 怎么保证可复现 | |------|------|----------------| | 下载、解析、格式化、落盘、自检 | 脚本 | 命令与参数写死 | | 一句话摘要、核心要点、总结、标签 | agent | SKILL.md 里的输入输出规约 | **它不做什么**:不写逆向/爬虫代码(下载只走 yt-dlp),不碰签名算法与私有接口, 不下载视频或音频,字幕缺失时跳过而不是硬造内容。 --- ## 真实产出 对一条真实视频执行 `fetch` —— 注意目录里**没有**什么:没有 `.mp4`、`.flv`、`.m4a`、`.part`, 只有字幕文件和它的 manifest。 ![fetch:只拉字幕,无媒体文件](https://raw.githubusercontent.com/3588834014-del/bilibili-note/main/docs/images/demo-fetch.png) 以及它生成的笔记(Obsidian 渲染效果:frontmatter、可点击的 `?t=` 跳转链接、摘要、要点、标签)。 截图来自真实运行,不是示意图: ![生成的笔记](https://raw.githubusercontent.com/3588834014-del/bilibili-note/main/docs/images/demo-note.png) --- ## 安装 同一个实现支持三种用法:**DSH 插件**、**独立 CLI**、**手动放 skill 目录**。 三者共用 `scripts/bili_note.py`,行为不会出现两份。 ### 作为 DSH 插件(DeepSeek Harness 用户推荐) ```bash dsh plugin --profile web add github:3588834014-del/bilibili-note-plugin ``` 这是标准的 **bundle 插件**:装完重启 profile,`bilibili-note` 就直接出现在 dsh 的 skill 目录里,不用手工往 `~/.agents/skills` 复制。用「B站笔记」或直接贴一条 BV 链接即可触发。 插件自带 `scripts/bili_note.py`、`docs/` 和 `SKILL.md`,并在注册时把 skill 正文里的 `` 占位符替换成**安装后的绝对路径**——模型不需要猜脚本在哪。 卸载:`dsh plugin --profile web remove dsh-bilibili-note`。 > 目前**还没有发 npm 包**,所以是源码安装:会被 [dsh-plugin.org](https://dsh-plugin.org) > 收录,但不会出现在 dsh 内置的「插件中心」里。本包没有 `prepare` 脚本,安装时不会执行 > 任何构建代码。 ### 作为 CLI 无第三方依赖(只用标准库),`yt-dlp` 是**外部命令**而不是 Python 包。 ```bash pipx install git+https://github.com/3588834014-del/bilibili-note.git # 或 git clone https://github.com/3588834014-del/bilibili-note.git cd bilibili-note && python -m pip install . ``` 装完即可: ```bash bili-note --version bili-note note "https://www.bilibili.com/video/BVxxxxxxxxx" --note-dir ~/MyVault/Sources/Videos ``` ### 作为普通 skill(不走插件系统) 仓库本身就是 skill 目录,复制到 dsh 的用户 skill 路径即可: ```bash # Linux / macOS / Git Bash git clone https://github.com/3588834014-del/bilibili-note.git mkdir -p ~/.agents/skills cp -r bilibili-note ~/.agents/skills/ ``` ```powershell # Windows PowerShell git clone https://github.com/3588834014-del/bilibili-note.git Copy-Item -Recurse -Force .\bilibili-note "$env:USERPROFILE\.agents\skills\bilibili-note" ``` 或用仓库里的安装脚本(自动跳过测试与缓存目录): ```bash python scripts/install.py # 装到 ~/.agents/skills/bilibili-note python scripts/install.py --dry-run # 只看会复制什么 ``` 装好后 dsh 会把它列进 skill 目录,用「B站笔记」之类的触发词即可唤起。 ### 前置条件 | 依赖 | 说明 | |------|------| | `yt-dlp` | **唯一允许的下载通道**。`pip install -U yt-dlp` | | B站登录 cookie | AI 字幕需要登录态,见下节 | | Python 3.8+ | 只用标准库 | | DSH `>=0.2.0-rc.2 <0.3.0` | 仅插件 / skill 用法需要 | ### 权限与外部依赖 装之前值得知道这个工具会碰什么: - **网络**:只通过 `yt-dlp` 访问 `bilibili.com`,且只下载**字幕**,不下载视频与音频 (`--skip-download` 是硬性参数,脚本会校验工作目录里没有媒体文件)。 - **凭据**:从 `$BILI_COOKIES` 或 `./cookies.txt` 读取 B站 cookie,会在工作目录旁生成一份 **归一化后的临时副本**喂给 yt-dlp;不上传、不打印、不提交。 - **文件系统**:只写你传入的 `--note-dir` 与一个临时工作目录,不碰其它路径。 - **无遥测、无额外依赖、无逆向接口。** --- ## 配置 cookie(必做) B站的 AI 字幕接口需要登录态,所以必须先导出 cookie: 1. 浏览器安装扩展 **Get cookies.txt LOCALLY** 2. 打开并登录 [bilibili.com](https://www.bilibili.com) 3. 点扩展图标 → Export → 导出 `cookies.txt` 4. 放到**当前工作目录**,或用环境变量指定: ```bash export BILI_COOKIES=/path/to/cookies.txt # Linux / macOS $env:BILI_COOKIES = "C:\path\cookies.txt" # PowerShell ``` > 只有 UP 主自行上传了字幕的视频才能跳过这一步。 ### ⚠️ 为什么不能把导出文件直接喂给 yt-dlp 这是本仓库最值得知道的一个坑。该扩展导出的行长这样: ``` .bilibili.com FALSE / FALSE 1819635894 SESSDATA xxxxx ``` domain 带前导点 `.`,但第 2 列的 host-only 标志是 `FALSE`。Python 的 `http.cookiejar` 内部有一条断言: ```python assert domain_specified == initial_dot ``` 两者不一致 → 抛 `LoadError` → yt-dlp 直接拒绝加载整个文件: ``` ERROR: invalid Netscape format cookies file '...': '.bilibili.com\tFALSE\t/...' ``` 结果是**字幕永远拿不到,而报错看起来像"这个视频没有字幕"**。 解法是归一化:去掉 domain 前导点并把标志置为 `FALSE`(host-only cookie 仍然会发给 `api.bilibili.com` 等子域,登录态不丢)。`bili_note.py` 的 `normalize_cookies()` 做的就是这个,附带过滤出 bilibili 域(导出文件通常混有 其他站点的 cookie)和按 domain/path/name 去重。 --- ## 输出目录 脚本不内置任何默认库路径——不同机器的 vault 位置不同,硬编码一个"看起来对"的 路径只会在别处静默创建出空目录。二选一: ```bash # 方式一:显式传参 python scripts/bili_note.py write --work ./work --note-dir "/Sources/Videos" # 方式二:环境变量 export BILI_VAULT="/path/to/MyVault" export BILI_TARGET_DIR="Sources/Videos" # 可选,默认值 ``` --- ## 用法 ### 用 CLI(推荐) ```bash # 一步到位:抓字幕 + 写笔记 bili-note note "https://www.bilibili.com/video/BVxxxxxxxxx" --note-dir "/Sources/Videos" # 带上判断性内容(摘要/要点/总结/标签) bili-note note "" --note-dir "/Sources/Videos" --content-file content.json # 分两步:先只抓字幕,供 agent 读 manifest bili-note fetch "" --out ./work bili-note write --work ./work --note-dir "/Sources/Videos" --content-file content.json # 结构自检 bili-note verify --note "<笔记路径>" # 环境自检:cookie / yt-dlp / 输出目录(不联网) bili-note selftest ``` `selftest` 回答的正是你实际会遇到的那个问题——**"为什么拿不到字幕?"**——先排除环境原因,再去怀疑代码: ``` ✓ cookie 文件:C:\Users\me\cookies.txt ✓ 含 SESSDATA 登录态:… ✓ yt-dlp:C:\...\yt-dlp.EXE ✗ 输出目录:未配置(传 --note-dir,或设 BILI_VAULT/BILI_TARGET_DIR) ``` ### 直接跑仓库里的脚本(不安装) ```bash # 1. 取元数据 + 字幕(只拉字幕,不拉视频) python scripts/bili_note.py fetch "https://www.bilibili.com/video/BVxxxxxxxxx" --out ./work # 2. agent 读 work/manifest.json 生成判断性内容,写成 content.json # 3. 组装并写入笔记(不给 --content-file 则产出草稿) python scripts/bili_note.py write \ --work ./work \ --note-dir "/Sources/Videos" \ --content-file content.json \ --processed "/Sources/Videos/.processed.json" # 4. 结构自检 python scripts/bili_note.py verify --note "<笔记路径>" ``` `fetch` 会打印每个环节的结果: ``` ✓ cookie 归一化:…(33 条 bilibili 域 cookie) ✓ 元数据:视频标题(UP主)|时长 12:34 可用字幕语言:danmaku, ai-zh, ai-en ✓ 字幕:ai-zh,134 段,1625 字 ✓ manifest:/path/to/work/manifest.json ``` ### content.json 的形状 ```json { "summary": "一句话摘要(60-120 字,结论前置)", "body": "总结正文(200-400 字,讲清论证链条与结论)", "tags": ["认知", "人文"], "cards": ["CC_示例"], "bullets": [ {"time": 0, "title": "小标题", "text": "一句话说明"}, {"time": 23, "title": "小标题", "text": "一句话说明"} ] } ``` `time` 填**秒数**,脚本负责渲染成 `MM:SS` 和跳转链接。`title` 和 `cards` 可省略。 --- ## 产出的笔记长什么样 ```markdown --- title: 视频标题 source: https://www.bilibili.com/video/BVxxxxxxxxx author: UP主 captured: 2026-01-01 duration: "12:34" type: video-note status: completed needs_summary: false tags: ["认知", "人文"] --- > [!info] 视频信息 > **UP主**:UP主 | **时长**:12:34 | [原视频](https://www.bilibili.com/video/BVxxxxxxxxx) ## 一句话摘要 …… ## 核心要点 |- [00:00](https://www.bilibili.com/video/BVxxxxxxxxx/?t=0s) **小标题**:说明 |- [00:23](https://www.bilibili.com/video/BVxxxxxxxxx/?t=23s) **小标题**:说明 ## 总结 …… ## 笔记标签 认知 | 人文 ## 相关卡片 - [[CC_示例]] ## 原始字幕 > [!quote]- 完整字幕(点击展开) > ……(已清理时间码与序号的纯文本) ``` > 要点行的 `|- ` 行首看着像坏掉的表格,但它是**故意的**——下游归档管线的校验器 > 只认这个形状。原因和全部格式约定见 [docs/FORMAT.md](docs/FORMAT.md)。 --- ## 测试 无需网络、无需 cookie: ```bash python -m unittest discover -s tests -v ``` 108 个测试,覆盖 SRT 解析与时间戳清理(含 LF/CRLF/BOM、多行 cue、超 1 小时、 HTML 与 ASS 标签、含 `<` `>` 的正文、零宽字符、重复句、空 cue)、两种时间戳 格式化、要点行与下游管线的契约、草稿字面量契约、cookie 归一化、 URL 规范化(含分 P 参数与追踪参数)、多 P 合集时间戳平移与字幕文件选取、 合集重复分 P 检测、落盘与去重、`verify` 的正反用例、CLI 接线、 Python 最低版本兼容性守卫、仓库卫生。 CI 在 Linux + Windows × Python 3.8/3.10/3.12 上跑同一套测试,见 [.github/workflows/ci.yml](.github/workflows/ci.yml)。 --- ## 已知限制 - **只支持 B站**。yt-dlp 支持的其他站点不在设计范围内。 - **多 P 合集自动合并**。yt-dlp 对 `...BVxxx` 默认只取 p1;本 skill 会识别合集并 逐分 P 抓字幕、按累计时长平移时间戳拼成连续时间轴(实测 6 分 P / 2:10:32 的合集 得到 2807 段)。传 `...BVxxx?p=6` 则只取该分 P。抓取较慢(每分 P 两次请求), 2 小时合集约 2 分钟;某分 P 无字幕时跳过但保留其时间占位,时间轴不错位。 - **依赖 B站的 AI 字幕**。视频没有字幕(且 UP 主未上传)时跳过,不做语音转写。 视频若需转写,属另一个工具链,本 skill 刻意不引入 ffmpeg/whisper 依赖。 - **AI 字幕有识别错误**。`ai-zh` 由语音识别产生,常有错字漏字。处理边界是: 引语照抄识别结果不"顺手改对"、能合理还原的按语义概括而不写成引号引语、 完全不可解的直接丢弃不猜。详见 SKILL.md §2.6。 - **超 1 小时的要点时间戳**用总分钟数(`[93:31]`),刻意不做小时进位——这是与 语料对齐的结果,不是 bug。 - ⚠️ **超过 99 分钟的视频会触发下游校验器漏判。** 要点标签是总分钟数,所以 100 分钟之后的标签是 **3 位数**(`[100:00]`、`[123:49]`);语料里唯一的 3 位 标签就是这么来的。但下游校验器的正则是 `^\|- \[[0-9]{2}:[0-9]{2}\]`, **只认 2 位分钟**,于是长视频笔记会被判「要点不足」。实测:2:10:32 的合集里 **24%** 的时刻分钟数是 3 位,最早出现在 100:00。本仓库的 `bilibili2obsidian` 侧工具已把该正则放宽为 `{1,3}`;`verify` 对这类笔记**放行但打印警告**。 --- ## 贡献 欢迎 issue 和 PR。改格式相关的东西之前请先读 [docs/FORMAT.md](docs/FORMAT.md), 并确保 `python -m unittest discover -s tests` 仍然全绿——测试套件里 `TestDownstreamContract` 就是拿来钉死这些契约的。 --- ## License [MIT](LICENSE)