---
name: lov-media-publisher
description: >-
  通过已登录的创作者后台发布本地视频到微信视频号或 Bilibili，并以媒体预检、描述来源冻结、
  字段回读、封面安全区、终稿确认与列表回读为门禁。Use when users ask to publish, draft,
  schedule, or check a video post, including 发视频号、发 B 站、投稿、上传成片。
license: MIT
compatibility: >-
  只读预检需要 Python 3.8+ 与 ffprobe；全部网页交互走 ego-browser 的 task space
  共享用户已登录态，不调用私有 API，也不需要额外平台凭据。
metadata:
  author: lovstudio
  version: "0.5.3"
  tags:
    - wechat-channels
    - bilibili
    - video-publishing
    - browser-automation
---

# 发布视频到微信视频号 / Bilibili

只操作平台自己的创作者后台可见网页。把页面当作动态界面：每一步重新读取当前页面，
按语义定位控件，不依赖历史 CSS 选择器、旧句柄或坐标。

## 平台路由

**开工第一件事是确定平台**，后面每一步的限制、DOM 写法和可逆性都由它决定。用户
只说「发出去」而素材、账号或历史上下文不足以判定时，问一次。

| 平台 | `--platform` | 入口 | 结构 | 页面细节 | 平台约束 |
| --- | --- | --- | --- | --- | --- |
| 微信视频号 | `wechat-channels` | `https://channels.weixin.qq.com/platform/post/create` | wujie 微前端 + shadow DOM | [创建页结构](references/wechat-channels/page-anatomy.md) | [视频号约束](references/wechat-channels/platform-constraints.md) |
| Bilibili | `bilibili` | `https://member.bilibili.com/platform/upload/video/frame` | 普通 Vue 应用 | [投稿页结构](references/bilibili/page-anatomy.md) | [B 站约束](references/bilibili/platform-constraints.md) |

四条会反复咬人的平台差异，先记住再动手：

- **可逆性不同**：视频号发布后不可撤改；B 站稿件投出后可 `?type=edit&bvid=` 改标题、
  简介、标签、封面、合集，但**改完必须重新点「立即投稿」**。终稿确认门禁两边都走，
  只是报告口径要跟着变，不要把 B 站的错发渲染成灾难。
- **硬限制差一个数量级**：视频号 4 GiB / 2 小时，B 站 16 GB / 10 小时。拿一边的默认值
  去卡另一边就会得到假结论，所以脚本一律显式传 `--platform`。
- **写值方式完全不同**：视频号的描述是 contenteditable、话题必须由平台按钮生成；
  B 站的输入框要用原生 setter，简介是 Quill 实例。不要互相套用。
- **控件类名一律从 page-anatomy 抄，不许猜**：两边都是自研组件库（视频号 wujie +
  shadow DOM，B 站 `data-v-*` scoped CSS）。套 antd 或通用猜测（`.ant-select-*`、
  `[role="combobox"]`、`.choose-btn`、`.category-item`）一个都命中不了，而且**查不到
  不报错**，表现为「点了没反应」，极易被误读成时机未到而空等几轮。**填任何字段前
  先打开该平台的 page-anatomy 对一遍选择器表**——2026-08-18 EP.03 就是跳过这一步，
  B 站的分区、合集、封面三项全填不进去，最后由用户手填。

两个平台共用的部分只有：[浏览器工作流](references/browser-workflow.md)（helper 签名、
控制权、任务空间生命周期）、[发布门禁清单](references/publish-gates.md)、
[重压方案](references/encoding-recipes.md)、[视频描述结构](references/description-template.md)。

## Triggers

### Activate when

- 用户要求把本地视频发布到微信视频号或 B 站，或保存成草稿。
- 用户要求安排定时发布、核验发布状态，或 publish a video to Bilibili / video channels。
- 用户要求基于本会话上一版内容重发同一个本地视频。

### Do not activate when

- 用户只要求发布微信公众号文章或调用公众号接口。
- 用户只要求本地媒体预检或转码，不涉及平台页面发布。
- 用户要求剪辑、渲染或生成视频素材本身（那是 `lov-media-creator` 的职责）。

## 读取输入

开始前取得以下输入：

- 必填：**目标平台**、目标动作 `draft` / `schedule` / `publish` / `status`，以及预期账号名称。
- `draft` / `schedule` / `publish` 必填：本地视频路径；`schedule` 还需带时区的发布时间。
- `status` 必填：条目链接、条目 ID / BV 号，或足以唯一定位的文案前缀、文件名和提交时间窗口。
- 可选：文案、话题/标签、封面、位置、合集、原创声明、分区、类型等当前页面支持的设置。

`publish` 还隐含一项**不可省的输入**：提交前用户对终稿的明确同意。会话开头的「发吧」
只授权启动流程，见下节。

把当前会话中用户已确认的主题、标题、描述、话题、封面和原创要求视为本次输入；用户说
「基于上一版」「重新发一遍」或指出某字段漏填时，沿用本会话最后一版已确认的内容，并把
纠正后的要求持续应用到后续步骤。不要补写与主题无关的事实或宣传语。

描述来源按以下优先级解析，命中高优先级后不得再用低优先级覆盖：

1. 用户在当前平台页面手动修改后的实际文本；
2. 用户在当前会话逐字给出的描述；
3. 同一项目中最近一次明确批准的发布文案；
4. 仅在以上均不存在且编辑区为空时，才按 `references/description-template.md` 生成草稿。

用户说“你的描述不行”、已经接管页面修改，或页面文本与 agent 上次写入值不同，都视为
`description_source=user-edited`。此时立即冻结全文：后续封面、合集、原创、位置和话题操作
不得重建描述编辑区，也不得为了“符合模板”润色用户版本。模板是生成草稿的约束，不是覆盖
用户终稿的许可证。

系列名默认用**全称**。撞到字数上限时不要直接降级用短名——先分别读创建表单和编辑表单
的 `maxLength`，两者不一定相同（B 站创建弹窗 20 字但编辑表单 50 字，先建短名再改名即可）。
只有两个表单都装不下时才用短名，并在报告里说明。

### 提交前必须由用户确认终稿（`publish` 硬门禁，两平台都适用）

`publish` 不是「字段齐了就发」。所有必填项 `pass` 之后，状态先进 `awaiting_confirmation`，
把终稿交给用户过目，得到明确同意才点主提交按钮。用户说过「发吧」「可以」属于**启动**
授权，不替代**终稿**确认——两者之间发生了上传、平台改写和字段回读，用户当时还没看见
最终结果。

确认动作分三步，缺一步都不算通知到：

1. 一次 `js()` 读完整个字段表（做法见对应平台的 page-anatomy），读真实 `value` /
   `checked` / 标签节点，不用外观推断。
2. 发系统通知 + 语音播报——用户可能不在终端前：

   ```bash
   python3 $SKILL_DIR/scripts/notify_user.py \
     --title "视频号发布 · 终稿待确认" \
     --message "标题/描述/话题/封面已就位，确认后我再提交" \
     --speech "终稿已准备好，请确认后我再发布"
   ```

3. 把字段表连同**平台改写过的地方**一起呈现，然后停下等用户回话。这里与「等控制权」
   不同：控制权是轮询等待，终稿确认必须等到用户真的答复，不设超时、不默认同意、
   不自行放行。

被平台改写的字段要单独标出来，这是用户最需要看见的部分。观测过的例子：视频号短标题
因 16 字上限或禁用符号被改写、封面槽位数与预期不一致、位置被平台自动带入；B 站标签被
「当前tag为话题专用，不允许自定义添加」拒掉后换了同义词、平台按账号名自动塞进
`手工`/`生活记录` 之类的标签。用户只批准过原始文案，没批准过这些改写。

`draft` / `schedule` / `status` 不要求终稿确认：草稿可改，定时在到点前可撤，`status` 只读。

### 发布完整性门禁

对 `publish`，默认要求标题、非空描述、至少一个话题/标签，以及**当前页面实际存在的
每一个封面槽**都有可用内容。用户未逐字给出这些文本时，可从当前会话、同一项目的明确
文案和视频可见主题中整理克制版本；证据不足时停在提交前指出缺项，不发布空白或半成品
内容。`draft` 可保留用户明确允许的未完成字段。

**封面槽位数按页面实测，不写死。** 视频号 2026-08-17 实测创建页只有一个槽（标签
「个人主页和分享卡片(3:4)」），两个预览由同一张 3:4 裁出；B 站有两个**互相独立**的槽
（4:3 首页推荐 / 16:9 个人空间），而列表、空间和信息流用的都是 16:9 那个——只传 4:3
等于没传。读到几个槽就传几张，并在报告里说明未用到的备用件。

**封面存在不等于合格。** 对页面上实际存在的每一个槽逐个打开编辑器，通过截图或效果
预览确认标题主体位于该槽的裁切安全区内。B 站的 16:9 槽尤其不能凭弹窗内预览判定，
唯一可信验证是投稿后拉一次公开接口读 `pic`（做法见
[B 站投稿页结构](references/bilibili/page-anatomy.md)「封面是两个独立的槽」）。
详见 [发布门禁清单](references/publish-gates.md)。

**封面素材有既定来源，不许现场生成。** 每期的封面在 `output/covers/<ep>/`
（`lov-channels-cover` 产出的 `cover_3x4.png` / `cover_4x3.png`）和
`output/deliverables/…-封面-<宽x高>-v*.png` 里，找不到就去问，不要自己造。
那套只产 3:4 和 4:3，**B 站的 16:9 槽是已知缺口**：缺就明说缺，让用户补一张，
绝不用 `ffmpeg -vframes 1` 抽首帧顶替——首帧是片头静帧或正片第一帧，当封面
等于没有封面。2026-08-18 EP.03 我抽了首帧当两个槽的素材，用户手动换成了正式
封面（线上 16:9 实际是 1440×810 的人像+标题版）。

原创（视频号）：**创作者账号默认勾选**，除非用户在同一任务里明确说「不勾原创」「非原创」
等否定词。教训来源（2026-08-21 EP.01）：用户连续多期要求勾原创，agent 一直停在
「用户没明说就不勾」的被动逻辑，每期都要用户提醒。原创是创作者账号的常驻权益，不是
需要用户重申才启用的选项。勾选原创时**必须完成原创权益弹窗中的须知/条款勾选**，并在
弹窗关闭后读取主复选框的真实 `checked=true`，不能只凭视觉样式或点击动作判断。终稿
确认时把「原创：已勾选」列入字段表，让用户看到而不是默认隐藏。

合集：**默认询问用户归属哪个合集**，尤其当账号已有系列合集时（创作者按系列发内容）。
用户新建合集后，在字段表里记录合集名并确认已选择。教训来源（2026-08-21 EP.01）：
agent 把合集当「未提出则默认」的可选项跳过，用户说「合集显然要加，我自己新建了一个」。

描述/标题等文案：**以用户手改的最终版本为准**。用户修改过描述、标题、话题后，不要
用 agent 自己整理的版本覆盖，也不要在终稿确认时展示旧版。教训来源（2026-08-21 EP.01）：
agent 展示了旧版描述，用户已自行改好。

实现上不能只靠记住这句话：接手或恢复发布页后的**第一个只读动作**是读取描述全文，并记录
`description_source`、精确文本和长度。每次与描述无关的页面写操作后重新读取一次；若与冻结
快照不同，立即停止并恢复前先让用户确认。终稿字段表的 `expected` 取冻结后的用户文本，不能
仍引用本地 `publish-copy.md` 或 agent 旧草稿。用户编辑后不要再调用描述区的清空、`fill`、
`execCommand` 或 Quill `setText()`。

广告、可见范围、位置、分区、类型和评论属于会改变发布语义的选项：用户明确提出
时执行，未提出时保持平台默认。位置若由平台自动带入，也要在提交前回读并列入字段表。

如果用户只说「我的号」，且页面仅展示一个清晰的已登录账号名称，记录该名称并在提交前
回报；出现账号选择器、多个账号或模糊头像时先确认。若用户要求的标题、描述、话题或原创
声明在页面回读中缺失，停在提交前，指出具体缺失字段，不点击发布。

## 执行流程

1. 对 `draft` / `schedule` / `publish`，先读该平台的 platform-constraints，再运行只读视频预检
   （**必须带 `--platform`**，两个平台的硬限制差一个数量级）：

   ```bash
   python3 $SKILL_DIR/scripts/check_video.py <视频路径> --platform wechat-channels --json
   python3 $SKILL_DIR/scripts/check_video.py <视频路径> --platform bilibili --json
   ```

   硬错误出现时停在预检阶段并回报；警告不阻断，但需在上传前明确列出。若页面明确显示
   账号已放宽（视频号 20 GiB / 8 小时），记录页面原文后以 `--max-gb` / `--max-hours` 覆盖重跑。
   实时页面限制更低时服从更低限制。`status` 是只读查询，跳过视频预检与上传。

   长视频（约 30 分钟以上）或大文件例外：此时把 `warnings` 当作实际门槛，先按
   [重压方案](references/encoding-recipes.md) 消除偏离项再上传，尤其是 `video_bitrate_high`。
   平台的转码拒绝只在整包传完、服务端解析后才返回，长视频一次失败就是数小时代价。

2. **同一时刻把文案也预检掉**，不要等页面来拒。这些全是纯字符串判定，撞一次就多一个
   「填入 → 失焦 → 读校验 → 改写」的页面往返：

   ```bash
   # 视频号：短标题 16 字上限 + 禁用逗号 + 合集 10 字且创建后不可改
   python3 $SKILL_DIR/scripts/check_copy.py --platform wechat-channels \
     --short-title "第一时间读 Harness 架构" --description "$(cat 描述.txt)" \
     --topic DeepSeek --collection "学 Harness" --json

   # B 站：标题 80 / 简介 2000 / 标签 ≤10 且部分名字是话题专用
   python3 $SKILL_DIR/scripts/check_copy.py --platform bilibili \
     --title "如何快速上手一个新项目" --description "$(cat 简介.txt)" \
     --topic 架构设计 --topic 开源 \
     --collection "手工川与你一起学 DeepSeek Harness" --collection-stage edit --json
   ```

   `status: fail` 时先改文案再开浏览器。它只覆盖能离线判定的部分，**通过不等于平台一定
   接受**，仍要在填入后读一次校验提示与 toast。两条要看清：视频号合集限 10 字且创建后
   不可修改，超长时停下来重新起名而不是截断照建；B 站的话题专用标签名单只覆盖已实测被
   拒的名字，新名字仍会在页面上被拒。

3. 读 [浏览器工作流](references/browser-workflow.md)，按 `ego-browser` 任务空间流程打开该
   平台入口：

   ```bash
   ego-browser nodejs <<'EOF'
   const task = await useOrCreateTaskSpace('media publish')
   await openOrReuseTab(ENTRY_URL, { wait: true })
   await snapshotText()
   cliLog('task space: ' + task.id)
   EOF
   ```

   helper 一律**传 id 不传 task 对象**（`handOffTaskSpace(task)` 会抛
   `task space not found: [object Object]`），完整签名坑表在同一份文档里。

4. 先 `snapshotText()` 再定位账号、账号状态与页面可见控件；每次关键操作后回读快照。
5. 若出现登录二维码或账号选择，`handOffTaskSpace(task.id)` 交给用户，待用户返回后再续接，
   不代填凭据、也不绕过登录。视频号本机微信已登录时优先点「快捷登录」，不要为了扫码
   多做一次交接。交出控制权后只能通过 `waitForAgentControl(task.id)` 等待归还，**任何情况
   下都不得自行 `takeOverTaskSpace()`**——包括只想读一次快照、或页面看起来已经就绪时。
   交出时同步发系统通知并语音播报（`$SKILL_DIR/scripts/notify_user.py`），然后在后台轮询
   控制权状态，不要停下来要求用户回话。用户明确说「已登录」「好了」之后才接手。
6. 读取页面展示的账号名并与预期逐字核对。只有一个清晰账号且用户说「我的号」时记录该
   名称；账号不符、多个候选或只有模糊头像时停下确认。
7. 汇总本会话已确认的字段，形成提交前字段表，每项记录
   `required / source / expected / actual / result`。接手页面先读取描述；页面已有非空描述且
   与 agent 上次写入值不同，`source=user-edited`，当场冻结，不进入描述写入分支。
   通用项：账号、标题、描述、话题/标签、封面（按页面实际槽位数逐槽一行）、合集、定时。
   视频号加：短标题、位置、原创、视频标注、评论、可见范围。B 站加：分区、类型、
   16:9 封面的 `pic` 回读结果。字段表全文见 [发布门禁清单](references/publish-gates.md)。
   账号、目标动作及所需输入均明确后，把状态记为 `prepared`。
8. `status` 直接打开内容管理列表并跳到第 12 步。其他动作通过**主上传区**关联的文件输入
   提交原视频——两个平台都存在多个 `accept` 相同的 file input，必须按平台文档里的判据
   锚定，不能取第一个。观察到上传进度后记为 `uploading`；进度 100% 后仍等待转码、缩略图
   或素材解析，记为 `processing`。
9. 只在素材解析完成且页面未显示视频错误后填写文案、封面与可选项。写入前先按来源优先级
   判断每个字段；`description_source=user-edited` 时跳过描述写入，只做只读回读。按各平台 page-anatomy
   的写值方式操作，并逐项回读实际值：
   - **视频号**：短标题读输入框 `value`，平台报特殊字符时按提示做最小修正并确认警告消失；
     描述读编辑区实际文本；话题必须读平台已识别的独立标签节点，不只检查原始 `#文本`；
     封面的图片文件输入只在编辑器打开后才存在；原创要生效需先勾须知再读主复选框属性。
     合集默认询问用户归属哪个合集（创作者按系列发内容时尤其如此）；原创默认勾选并
     完成须知弹窗确认（见上文「原创」段），不要等用户重申。
   - **B 站**：输入框用原生 setter 写值，`inputType` 不能省；写完的计数器**必须下一轮
     `js()` 再读**（同一轮读到的是 `nextTick` 前的旧值，`0/20` 是假失败）；简介用
     `.ql-container.__quill.setText()`；填标签前先清掉平台自动猜的那几个；`svg.close` 与
     `.hot-tag-item` 只吃 CDP 真鼠标。
   每个与描述无关的写操作后都要断言冻结描述逐字未变。任何必填字段未持久化、用户描述被改动
   或校验仍有警告时，停在提交前。
10. **`publish` 在此停下等用户确认终稿**：一次 `js()` 读完整个字段表，跑 `notify_user.py`
    发通知与播报，把终稿（尤其被平台改写过的字段）交给用户过目，状态记为
    `awaiting_confirmation`。得到明确同意才继续。`draft` / `schedule` / `status` 跳过本步。
11. 按目标动作进入对应分支：
    - `draft`：选「保存草稿」语义动作，确认完成后进入草稿列表并重载。
    - `schedule`：启用定时发布，核对页面时区、日期和时间，确认提交后进入内容列表并重载。
    - `publish`：所有必填项 `pass` 且用户已确认终稿后，点主提交动作（视频号「发表」/
      B 站「立即投稿」），**只提交一次**，再进入内容列表并重载。未得到确认时不要点击，
      也不要因为等待而改用 `draft` 绕过。主提交按钮常落在视口外（视频号观测 y=726 / 视口高
      727，B 站观测 y=1380 / 视口高 731），坐标点击会静默丢；先 `scrollIntoView` 再**按新
      rect** 操作，点完回读页面状态确认真的生效。
    - `status`：不修改条目；打开与目标标识匹配的内容列表并重载，读取平台状态原文。
12. 用账号及当前可用的条目 ID / BV 号、链接、视频文件名、文案前缀、提交时间、封面等组合
    指纹查找唯一条目。记录列表状态、条目链接或 ID、回读时间与页面 URL。B 站的管理页拒绝
    直链（`upload-manager/all` 与 `/season` 会跳回首页），要从侧边栏点进去。

## 状态契约

状态只使用下列值；出错时保留最后一个已确认状态，把错误另列：

| 状态 | 成立证据 |
| --- | --- |
| `prepared` | 平台与账号已核对，动作所需输入完成；写入动作也已通过只读预检 |
| `uploading` | 页面出现本次文件的实际上传进度 |
| `processing` | 上传完成，平台仍在转码、解析或生成封面 |
| `awaiting_confirmation` | 仅 `publish`：字段表全项 `pass`，已通知用户并把终稿交他过目，等待明确同意 |
| `draft_saved` | 草稿列表重载后找到唯一匹配条目 |
| `scheduled` | 内容列表重载后找到匹配条目和正确定时时间/状态 |
| `platform_pending` | 内容列表重载后找到唯一条目，状态明确为转码、审核或发布处理中 |
| `published` | 内容列表重载后找到匹配条目，且列表明确显示已发布/已发表状态 |
| `publish_failed` | 内容列表重载后找到唯一条目，且列表明确显示失败、拒绝或终止状态 |

成功提示、按钮消失、请求返回或页面跳转都不单独证明 `published`。没有列表重载回读时，
停留在最后一个已证实状态。B 站另有一条独立证据：`pic` 字段能证明 16:9 封面是否真的换了，
不要用表单内预览代替。

## 收口错误

- 提交后超时或状态不明时，不要再次点击发布；先去内容列表重载回读，防止重复内容。
- 主提交按钮进入加载态、页面跳转或出现成功提示后，均视为已经尝试提交；后续只做列表回读。
- 上传或处理失败时记录页面原文、文件名、预检摘要、账号、当前 URL、最后状态与发生时间；
  保留源视频不变。
- 页面显示转码失败（如「视频转码失败，调整视频导出参数后重试」）时，这是服务端拒绝，
  不是网络问题：重传同一文件只会再失败一次。先核对文件大小与时长确实在页面显示的限制内，
  再按 [重压方案](references/encoding-recipes.md) 消除预检偏离项、重压并重新验证。
- 重发同一素材时列表里会同时存在旧条目与新条目，且文件名、时长、首帧完全相同。此时禁止
  用这些字段做指纹，改用提交时间、标题、话题组合、新封面或条目 ID / BV 号；无法区分时保留
  最后确认状态并报告全部候选。旧条目是否删除由用户决定。
- 页面结构变化时重新获取快照，以角色、可见名称、标签和邻近文案定位；多个候选时停止点击
  并报告候选。
- **控件查不到不代表控件不存在**：视频号的表单在 `<wujie-app>` 的 shadow root 里，
  `document.querySelector` 一律返回空且不报错，容易被读成「还没渲染完」而白等几轮；文件输入
  连 shadow root 都查不到，只有 `DOM.getDocument({ pierce: true })` 能看见。B 站没有这个问题，
  但换成了「Vue 不吃合成事件」。各自的定位表与可复用探针在两份 page-anatomy 里。
- **视频号的弹窗是预渲染的，按可见性过滤而非存在性**：约 35 个弹窗全部在 DOM 里，只靠尺寸
  隐藏。读 `.weui-desktop-dialog` 的 `innerText` 会拿到与当前状态无关的文案（观测实例：上传
  封面后读到「将此次编辑保留?」，屏幕上并无此弹窗），据此点按钮就是点空。用
  `getBoundingClientRect().width > 100` 过滤。
- **点了没反应且没报错**时不要重试同样的点法：先判断是落在视口外（滚进来再按新 rect 点），
  还是控件只吃真鼠标（用 `cdp('Input.dispatchMouseEvent', ...)`）。观测实例：B 站报告删了 12 个
  标签，实际一个没少。
- 列表若显示「原创审核中」「审核中」「转码中」等处理中原文，状态用 `platform_pending`，即使
  已出现成功提示或已跳转列表也不要写成 `published`。
- 用户未答复终稿确认时，`awaiting_confirmation` 就是本次任务的最终状态，照实回报——这不是
  失败，也不要为了收尾而改走 `draft` 或自行放行。此时保留任务空间与页面。
- 用户在任务任意阶段明确要求「发布后不要关闭」「保留页面 review」时，该要求持续生效直到
  明确撤销。收尾必须使用 `completeTaskSpace(task.id, { keep: true })` 并检查返回的 `done`。
- 最终回报平台、目标动作、账号、最后状态、平台状态原文、列表回读证据、警告和未完成项。
  只有满足状态契约时才使用 `draft_saved` / `scheduled` / `platform_pending` / `published` /
  `publish_failed`。B 站的报告要额外说明稿件仍可编辑，视频号的不要这么说。

与相邻 Skill 的分工与不做什么，见 [组合决策](references/skill-composition.md)。
