| 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.6.1","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 | 创建页结构 | 视频号约束 |
| Bilibili | bilibili | https://member.bilibili.com/platform/upload/video/frame | 普通 Vue 应用 | 投稿页结构 | B 站约束 |
四条会反复咬人的平台差异,先记住再动手:
- 可逆性不同:视频号发布后不可撤改;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 站的分区、合集、封面三项全填不进去,最后由用户手填。
两个平台共用的部分只有:浏览器工作流(helper 签名、
控制权、任务空间生命周期)、发布门禁清单、
重压方案、视频描述结构。
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 还隐含一项不可省的输入:提交前用户对终稿的明确同意。会话开头的「发吧」
只授权启动流程,见下节。
把当前会话中用户已确认的主题、标题、描述、话题、封面和原创要求视为本次输入;用户说
「基于上一版」「重新发一遍」或指出某字段漏填时,沿用本会话最后一版已确认的内容,并把
纠正后的要求持续应用到后续步骤。不要补写与主题无关的事实或宣传语。
描述来源按以下优先级解析,命中高优先级后不得再用低优先级覆盖:
- 用户在当前平台页面手动修改后的实际文本;
- 用户在当前会话逐字给出的描述;
- 同一项目中最近一次明确批准的发布文案;
- 仅在以上均不存在且编辑区为空时,才按
references/description-template.md 生成草稿。
用户说“你的描述不行”、已经接管页面修改,或页面文本与 agent 上次写入值不同,都视为
description_source=user-edited。此时立即冻结全文:后续封面、合集、原创、位置和话题操作
不得重建描述编辑区,也不得为了“符合模板”润色用户版本。模板是生成草稿的约束,不是覆盖
用户终稿的许可证。
系列名默认用全称。撞到字数上限时不要直接降级用短名——先分别读创建表单和编辑表单
的 maxLength,两者不一定相同(B 站创建弹窗 20 字但编辑表单 50 字,先建短名再改名即可)。
只有两个表单都装不下时才用短名,并在报告里说明。
提交前必须由用户确认终稿(publish 硬门禁,两平台都适用)
publish 不是「字段齐了就发」。所有必填项 pass 之后,状态先进 awaiting_confirmation,
把终稿交给用户过目,得到明确同意才点主提交按钮。用户说过「发吧」「可以」属于启动
授权,不替代终稿确认——两者之间发生了上传、平台改写和字段回读,用户当时还没看见
最终结果。
确认动作分三步,缺一步都不算通知到:
-
一次 js() 读完整个字段表(做法见对应平台的 page-anatomy),读真实 value /
checked / 标签节点,不用外观推断。
-
发系统通知 + 语音播报——用户可能不在终端前:
python3 $SKILL_DIR/scripts/notify_user.py \
--title "视频号发布 · 终稿待确认" \
--message "标题/描述/话题/封面已就位,确认后我再提交" \
--speech "终稿已准备好,请确认后我再发布"
-
把字段表连同平台改写过的地方一起呈现,然后停下等用户回话。这里与「等控制权」
不同:控制权是轮询等待,终稿确认必须等到用户真的答复,不设超时、不默认同意、
不自行放行。
被平台改写的字段要单独标出来,这是用户最需要看见的部分。观测过的例子:视频号短标题
因 16 字上限或禁用符号被改写、封面槽位数与预期不一致、位置被平台自动带入;B 站标签被
「当前tag为话题专用,不允许自定义添加」拒掉后换了同义词、平台按账号名自动塞进
手工/生活记录 之类的标签。用户只批准过原始文案,没批准过这些改写。
draft / schedule / status 不要求终稿确认:草稿可改,定时在到点前可撤,status 只读。
发布完整性门禁
对 publish,默认要求标题、非空描述、至少一个话题/标签,以及当前页面实际存在的
每一个封面槽都有可用内容。用户未逐字给出这些文本时,可从当前会话、同一项目的明确
文案和视频可见主题中整理克制版本;证据不足时停在提交前指出缺项,不发布空白或半成品
内容。draft 可保留用户明确允许的未完成字段。
B 站有已核验章节就必须在简介加入可点击时间轴。 时间码只能取自最终成片时间轴,格式为
每行一个 MM:SS 章节名(超过一小时用 HH:MM:SS),不得从源录屏、旧剪辑版或人工记忆
抄写。若项目已有已批准章节而简介缺少时间轴,完整性门禁为 fail。对于已经冻结的
description_source=user-edited,不得静默覆盖全文:按用户明确要求做最小追加,或停下展示
缺项;修改后重新冻结全文并重跑文案预检、字段回读和终稿确认。
封面槽位数按页面实测,不写死。 视频号 2026-08-17 实测创建页只有一个槽(标签
「个人主页和分享卡片(3:4)」),两个预览由同一张 3:4 裁出;B 站有两个互相独立的槽
(4:3 首页推荐 / 16:9 个人空间),而列表、空间和信息流用的都是 16:9 那个——只传 4:3
等于没传。读到几个槽就传几张,并在报告里说明未用到的备用件。
封面存在不等于合格。 对页面上实际存在的每一个槽逐个打开编辑器,通过截图或效果
预览确认标题主体位于该槽的裁切安全区内。B 站的 16:9 槽尤其不能凭弹窗内预览判定,
唯一可信验证是投稿后拉一次公开接口读 pic(做法见
B 站投稿页结构「封面是两个独立的槽」)。
详见 发布门禁清单。
封面素材有既定来源,不许现场生成。 每期的封面在 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()。
广告、可见范围、位置、分区、类型和评论属于会改变发布语义的选项:用户明确提出
时执行,未提出时保持平台默认。位置若由平台自动带入,也要在提交前回读并列入字段表。
如果用户只说「我的号」,且页面仅展示一个清晰的已登录账号名称,记录该名称并在提交前
回报;出现账号选择器、多个账号或模糊头像时先确认。若用户要求的标题、描述、话题或原创
声明在页面回读中缺失,停在提交前,指出具体缺失字段,不点击发布。
执行流程
-
对 draft / schedule / publish,先读该平台的 platform-constraints,再运行只读视频预检
(必须带 --platform,两个平台的硬限制差一个数量级):
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 当作实际门槛,先按
重压方案 消除偏离项再上传,尤其是 video_bitrate_high。
平台的转码拒绝只在整包传完、服务端解析后才返回,长视频一次失败就是数小时代价。
-
同一时刻把文案也预检掉,不要等页面来拒。这些全是纯字符串判定,撞一次就多一个
「填入 → 失焦 → 读校验 → 改写」的页面往返:
python3 $SKILL_DIR/scripts/check_copy.py --platform wechat-channels \
--short-title "第一时间读 Harness 架构" --description "$(cat 描述.txt)" \
--topic DeepSeek --collection "学 Harness" --json
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 站的话题专用标签名单只覆盖已实测被
拒的名字,新名字仍会在页面上被拒。
-
读 浏览器工作流,按 ego-browser 任务空间流程打开该
平台入口:
ego-browser nodejs <<'EOF'
const task = await useOrCreateTaskSpace('media publish')
await openOrReuseTab(ENTRY_URL, { : })
await snapshotText()
cliLog( + task.id)
EOF
状态契约
状态只使用下列值;出错时保留最后一个已确认状态,把错误另列:
| 状态 | 成立证据 |
|---|
prepared | 平台与账号已核对,动作所需输入完成;写入动作也已通过只读预检 |
uploading | 页面出现本次文件的实际上传进度 |
processing | 上传完成,平台仍在转码、解析或生成封面 |
awaiting_confirmation | 仅 publish:字段表全项 pass,已通知用户并把终稿交他过目,等待明确同意 |
draft_saved | 草稿列表重载后找到唯一匹配条目 |
scheduled | 内容列表重载后找到匹配条目和正确定时时间/状态 |
platform_pending | 内容列表重载后找到唯一条目,状态明确为转码、审核或发布处理中 |
published | 内容列表重载后找到匹配条目,且列表明确显示已发布/已发表状态 |
publish_failed | 内容列表重载后找到唯一条目,且列表明确显示失败、拒绝或终止状态 |
成功提示、按钮消失、请求返回或页面跳转都不单独证明 published。没有列表重载回读时,
停留在最后一个已证实状态。B 站另有一条独立证据:pic 字段能证明 16:9 封面是否真的换了,
不要用表单内预览代替。
收口错误
- 提交后超时或状态不明时,不要再次点击发布;先去内容列表重载回读,防止重复内容。
- 主提交按钮进入加载态、页面跳转或出现成功提示后,均视为已经尝试提交;后续只做列表回读。
- 上传或处理失败时记录页面原文、文件名、预检摘要、账号、当前 URL、最后状态与发生时间;
保留源视频不变。
- 页面显示转码失败(如「视频转码失败,调整视频导出参数后重试」)时,这是服务端拒绝,
不是网络问题:重传同一文件只会再失败一次。先核对文件大小与时长确实在页面显示的限制内,
再按 重压方案 消除预检偏离项、重压并重新验证。
- 重发同一素材时列表里会同时存在旧条目与新条目,且文件名、时长、首帧完全相同。此时禁止
用这些字段做指纹,改用提交时间、标题、话题组合、新封面或条目 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 的分工与不做什么,见 组合决策。
通用反馈闭环
用户在 Skill 驱动任务中提出修改意见时,继续当前产物前必须执行:
- 先判断意见是
task-specific(仅本次)还是 reusable(可跨任务复用)。
task-specific 只修改当前任务,不改 Skill。
reusable 先确定作用域:领域规则先更新对应 canonical Skill;适用于所有 Skill 的规则先更新共享规范。
- 完成规则更新、版本、lint 与分发核验后,再把修改应用到当前任务。
reusable 修改会使此前的“确认”“继续”“发吧”失效;完成当前产物修改和回读后必须停下,等待用户下一步指示,不自动进入发布、提交或其他外部写入。