lov-media-publisher · v0.9.1 · 2026-09-07 · sha256 4f9ae47b3a3b7b87

lov-media-publisher v0.9.1A

Immutable. This exact content is served forever at /api/v1/blob/4f9ae47b3a3b7b87.

---
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,也不需要额外平台凭据。
  交接语音可选使用 VOLC_APPID / VOLC_TOKEN 调用火山 TTS,未配置时降级到 macOS say。
depends_on:
  - lov-branding-consistency
metadata:
  author: lovstudio
  version: "0.9.1"
  tags:
    - wechat-channels
    - bilibili
    - video-publishing
    - browser-automation
---

# 视频分发助手 · Video Publisher

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

## 平台路由

**默认依次发布到微信视频号,再发布到 B 站**,平台队列为
`[wechat-channels, bilibili]`。用户只说「发布」「发出去」且没有指定平台时,直接采用这份
默认队列,不再泛问发布目的地。

- 当前请求明确指定平台时,仅处理指定的平台;明确指定顺序时按该顺序执行。条目链接或
  BV 号已能确定平台的 `status` 请求,只查询该条目,不扩展到默认双平台队列。
- 选择两平台但未另指定顺序时,先完成视频号本次动作并回读列表状态,再进入 B 站流程。
  视频号出现阻塞时先处理并报告具体阻塞,不自行跳过它、提前改发 B 站。
- 素材、账号或必要字段缺失时,先按本 Skill 的输入和预检流程自行查明;只有确实无法解决
  的具体缺项才询问,不把缺项重新解释成平台选择问题。平台限制、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 站的分区、合集、封面三项全填不进去,最后由用户手填。

### 平台成片映射门禁

平台允许某种画幅,不代表本期就该选那一版。打开发布页前,先从 Creator 交付报告、平台文件名、
Composition 或本期清单确定**本期目标平台对应的已批准成片**:例如本期明确有
`Episode-Horizontal` 与 `Episode-Vertical`,且竖版文件名为 `wechat-channels` 时,B 站应先导出
横版平台 MP4,不能拿竖版“先发了再说”。Studio 里的 Composition 和 `review-ready` 预览都不是
可上传文件;缺少目标平台 MP4 时,状态记为 `blocked-on-platform-variant` 并交回
`lov-media-creator` 渲染、解码和质检。

预检用两层硬门禁:

```bash
python3 $SKILL_DIR/scripts/check_video.py PLATFORM_MP4 \
  --platform bilibili --expected-orientation horizontal --json
```

- 文件名明确含另一平台标识(例如 B 站任务选中 `wechat-channels`)时默认失败;只有用户明确要求
  跨平台复用,才可加 `--allow-cross-platform-name`。
- `--expected-orientation` 来自本期交付契约,不从“B 站一定横版”之类平台刻板规则擅自推断;
  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` 的职责)。

## 读取输入

开始前取得以下输入:

- 必填:按上节解析的**目标平台队列**(未指定时为视频号 → B 站)、目标动作
  `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`。此时立即冻结全文:后续封面、合集、原创、位置和话题操作
不得重建描述编辑区,也不得为了“符合模板”润色用户版本。模板是生成草稿的约束,不是覆盖
用户终稿的许可证。

用户进一步给出**实际发布文本**并说明“已经修改并发布”时,这份文本立即成为本期最高优先级
gold reference,同时也是后续平台的表达策略来源:保留它的钩子、语气、大小写和关键词,只做
平台机制所需的最小适配(例如把视频号 `#话题` 迁移到 B 站标签字段、在 B 站钩子后追加批准
章节),不得恢复 agent 旧稿、系列介绍或冗长摘要。把用户报告与平台验证分开记录;未从内容
列表回读前,不把“用户说已发布”写成已验证的 `published`。

知识技巧短视频(通常约 10 分钟以内)没有用户终稿时,默认用“真实痛点问句 → 能力承诺问句
→ 5–10 个精准关键词”,而不是强制“系列第 N 期 + 内容说明 + 客套收尾”。长访谈、长教程或
资源密集内容才启用完整章节与链接骨架。具体判据见 `references/description-template.md`。

系列名默认用**全称**。撞到字数上限时不要直接降级用短名——先分别读创建表单和编辑表单
的 `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` 可保留用户明确允许的未完成字段。

**B 站有已核验章节就必须在简介加入可点击时间轴。** 时间码只能取自最终成片时间轴,格式为
每行一个 `MM:SS 章节名`(超过一小时用 `HH:MM:SS`),不得从源录屏、旧剪辑版或人工记忆
抄写。若项目已有已批准章节而简介缺少时间轴,完整性门禁为 `fail`。对于已经冻结的
`description_source=user-edited`,不得静默覆盖全文:按用户明确要求做最小追加,或停下展示
缺项;修改后重新冻结全文并重跑文案预检、字段回读和终稿确认。

**B 站 tags 要做发布后公开回读。** 投稿表单里的标签节点只能证明提交前状态;发布后额外调用
`/x/tag/archive/tags?bvid=BV...` 读取 `data[].tag_name`,并在公开页核对
`.video-tag-container a.tag-link`。`/x/web-interface/view` 本身不返回 tags,简介里没有
`#标签` 也不代表 tags 丢失。平台会重排标签顺序,所以按精确名称集合比较,不按顺序比较;
接口与公开页都命中预期集合时,不要为了展示误判重新编辑或投稿。

**封面槽位数按页面实测,不写死。** 视频号 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`,不能只凭视觉样式或点击动作判断。终稿
确认时把「原创:已勾选」列入字段表,让用户看到而不是默认隐藏。

合集:先读取页面实际选中值与现有候选;创建新合集仍需用户明确同意,因为视频号合集创建后
不可改名。用户已在接管期间选择合集,或明确说“合集已勾选并已发布”时,不再询问、不清空、
不重新创建;能回读就记录实际合集名,页面已跳转而名称不可见时只记录 `user-selected`,不得
从本地候选猜名字。教训来源(2026-08-21 EP.01):agent 把合集当「未提出则默认」的可选项
跳过;2026-08-26 EP.01 则在用户已自行勾选后仍不能继续沿用旧的未选择字段表。

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

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

**生成内容标注必须依据素材来源,不能依据视频话题。** 讲解 AI 产品、演示 AI 案例,以及使用
AI 辅助剪辑、字幕或调色,都不能单独证明成片含生成/合成素材。先核对用户对素材来源的明确
说明与可验证的制作证据,不臆测屏幕演示片段的来源;按
[素材来源与生成内容标注](references/publish-gates.md#素材来源与生成内容标注)记录依据并回读字段。

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

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

### 发布协作节奏(硬约束)

- **开页前冻结素材清单**:先把平台成片和所有封面解析为存在、可读的绝对路径,
  并记录版本、尺寸和平台槽位。不能只把路径藏在文件输入或工具日志里。
- **只按依赖等待**:视频输入一旦开始上传,标题、简介、标签、合集、原创和其他
  已挂载的独立字段立即进入写入/回读通道。只有封面编辑器、视频解析结果和提交动作
  等真实依赖继续等待;不得为了轮询上传进度把全表串行化。
- **人工交接必须可执行**:只有登录、平台阻断或真实遮挡无法自动化时才交接。交接同时:
  1. 在对话中给出可点击的成片与封面绝对路径;
  2. 系统通知写清平台、当前阻塞和唯一需要用户完成的动作;
  3. 优先用火山 TTS 播报同一条指令,并明说「不要点发布」或「可以点发布」的边界;
  4. 交出控制权后只等待归还,不再读页、点击或自行接管。
- **失败立即降级**:自动上传或封面写入遇到确定阻塞后,不重复同一无效动作。
  先保留已填字段,再交付精确文件路径和单一人工步骤;用户操作后只回读变更区域。

## 执行流程

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 \
     --expected-orientation "$EXPECTED_ORIENTATION" --json
   ```

   `EXPECTED_ORIENTATION` 必须先从本期交付契约读取并设为 `horizontal` / `vertical` / `square`;
   没有项目证据时不得仅凭平台名称猜值。

   硬错误出现时停在预检阶段并回报;警告不阻断,但需在上传前明确列出。若页面明确显示
   账号已放宽(视频号 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 --tts-provider auto`),然后在后台轮询控制权状态,不要停下来
   要求用户回话。用户明确说「已登录」「好了」之后才接手。
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` 是假失败);简介用
     Quill 用 `.ql-container.__quill.setText(text, 'user')` 后 `blur()`,下一轮从 `.ql-editor`
     或 `getText()` 逐字回读;填标签前先清掉平台自动猜的那几个;`svg.close` 与
     `.hot-tag-item` 只吃 CDP 真鼠标。项目存在已核验章节时,从最终成片时间轴生成完整的
     `MM:SS 章节名` 列表并写进简介;下一轮逐条回读全部时间码和标题,缺一条都不能提交。
     封面右侧「首页推荐 / 个人空间」按钮只切换效果预览;真正上传槽是包住 `#editor_4_3` /
     `#editor_16_9` 的 canvas wrapper,必须逐槽回读 `active`、上传并截图,且关闭双比例同步。
   每个与描述无关的写操作后都要断言冻结描述逐字未变。任何必填字段未持久化、用户描述被改动
   或校验仍有警告时,停在提交前。
10. **`publish` 在此停下等用户确认终稿**:一次 `js()` 读完整个字段表,跑 `notify_user.py`
    发通知与播报,把终稿(尤其被平台改写过的字段)交给用户过目,状态记为
    `awaiting_confirmation`。得到明确同意才继续。`draft` / `schedule` / `status` 跳过本步。
    如果用户接管页面后返回并说“我已改好并发布”,不要把这句话当成让 agent 再点一次的确认:
    立即冻结用户给出的实际文本,停止所有表单写入,直接进入内容列表只读回读;回读前保留上个
    已证实状态,并另记 `user_reported_published=true`。
11. 按目标动作进入对应分支:
    - `draft`:选「保存草稿」语义动作,确认完成后进入草稿列表并重载。
    - `schedule`:启用定时发布,核对页面时区、日期和时间,确认提交后进入内容列表并重载。
    - `publish`:所有必填项 `pass` 且用户已确认终稿后,点主提交动作(视频号「发表」/
      B 站「立即投稿」),**只提交一次**,再进入内容列表并重载。未得到确认时不要点击,
      也不要因为等待而改用 `draft` 绕过。主提交按钮常落在视口外(视频号观测 y=726 / 视口高
      727,B 站观测 y=1380 / 视口高 731),坐标点击会静默丢;先 `scrollIntoView` 再**按新
      rect** 操作,点完回读页面状态确认真的生效。B 站先做一次语义/直接点击;仅在确认真实遮挡且
      没有任何提交态时交用户操作,禁止隐藏遮挡层或盲点第二次。
    - `status`:不修改条目;打开与目标标识匹配的内容列表并重载,读取平台状态原文。
12. 用账号及当前可用的条目 ID / BV 号、链接、视频文件名、文案前缀、提交时间、封面等组合
    指纹查找唯一条目。记录列表状态、条目链接或 ID、回读时间与页面 URL。B 站的管理页拒绝
    直链(`upload-manager/all` 与 `/season` 会跳回首页),要从侧边栏点进去。B 站还要分别
    回读 `view.data.title`、`view.data.desc`、`view.data.pic` 与
    `x/tag/archive/tags.data[].tag_name`;前三项逐字/目视核对冻结终稿,公开 tags 按集合验收,
    不能从简介里是否出现 `#` 推断。公开字段缺失时只编辑同一 BV 并重投一次,不创建重复稿件。

## 状态契约

状态只使用下列值;出错时保留最后一个已确认状态,把错误另列:

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

成功提示、按钮消失、请求返回或页面跳转都不单独证明 `published`。没有列表重载回读时,
停留在最后一个已证实状态。B 站还必须用公开 `view` 的 `title / desc / 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` 或自行放行。此时保留任务空间与页面。
- 用户报告自己已修改并发布时,禁止再次点击主提交按钮,也禁止用旧字段表覆盖页面;后续只做
  内容列表回读。列表尚未回读时说明“用户报告已发布、平台状态未验证”,不要误报 `published`。
- 用户在任务任意阶段明确要求「发布后不要关闭」「保留页面 review」时,该要求持续生效直到
  明确撤销。收尾必须使用 `completeTaskSpace(task.id, { keep: true })` 并检查返回的 `done`。
- 最终回报平台、目标动作、账号、最后状态、平台状态原文、列表回读证据、警告和未完成项。
  只有满足状态契约时才使用 `draft_saved` / `scheduled` / `platform_pending` / `published` /
  `publish_failed`。B 站的报告要额外说明稿件仍可编辑,视频号的不要这么说。

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