wechat-channels-video · v0.2.1 · 2026-11-03 · sha256 197e06b2b14c9128

wechat-channels-video v0.2.1B

Immutable. This exact content never changes and is served at /api/v1/blob/197e06b2b14c9128.

---
name: wechat-channels-video
description: Publish a WeChat Channels video draft with Ego Lite via ego-browser — login-check first (sessions expire fast), upload video, fill description/short title/topics/location/cover, save draft only. Soft-fails if Channels is logged out and tells the human to scan QR inside Ego. Personal use. Use when the user asks to post to 视频号 / Channels assistant.
metadata:
  version: "0.2.1"
  requires:
    bins:
      - ego-browser
      - node
  ego:
    min_skill_guide: "2.1.0"
---

# wechat-channels-video

用 Ego Lite 会话在 [视频号助手](https://channels.weixin.qq.com) **保存视频草稿**。不点「发表」(扫码发表留给人)。

Save a WeChat Channels **draft** through Ego Lite. Never click Publish — QR publish is human-only.

## 先装 Ego

先装 **Ego Lite**,保证 `ego-browser` 在 `PATH`。缺失则 soft-fail / 告知用户去装,**不要**硬调浏览器。

## 每次发布前:登录闸门(必做)

视频号登录态**过期很快**。Agent **在上传或填表之前**必须跑:

```bash
node scripts/check-login.mjs
# 需要把窗口留给人扫码时:
node scripts/check-login.mjs --keep
```

| 退出码 | 含义 | Agent 做什么 |
|--------|------|----------------|
| `0` | 已登录 | 把 `login_tip` 告诉用户,继续流程 |
| `2` | 未登录 / 无 ego-browser | 展示 `how_to_login` / `how_to_login_zh`,**停**。请人在 Ego 扫码后再跑 check-login |
| `1` | 其它错误 | 报告 JSON `error`,不要假装已发布 |

详见 `references/login.md`。不要另做 cookie-healthcheck 技能。

## 输入(通用,无本机产品路径)

调用方提供视频与文案字段,见 `references/inputs.md`。本技能**不**写死任何 `~/…` 业务目录。

最少需要:本机 `video` 路径、`description`、`short_title`。可选:`tags`、`cover`、位置/原创/活动。

## CLI 薄脚本(稳定部分)

`ego-browser nodejs` 不转发 shell argv;`check-login` 已用临时 entry + `ego-browser nodejs -e` 包好。

```bash
# 仓库根目录
node skills/wechat-channels-video/scripts/check-login.mjs
node skills/wechat-channels-video/scripts/check-login.mjs --keep
node skills/wechat-channels-video/scripts/serve-file.mjs --file /absolute/path/to/video.mp4 --port 18765

# 已安装的技能目录里也可以用相对路径
node scripts/check-login.mjs
node scripts/serve-file.mjs --file /absolute/path/to/video.mp4 --port 18765
```

- **check-login**:唯一登录硬闸门;结束默认 `finish({ keep: [] })`(`--keep` 除外)。
- **serve-file**:给页面 `fetch` 用的本机 CORS 文件服务;打印 `{ url, bytes, port }`,SIGINT 结束。大视频不要 base64。

**不**把整段发表 UI 打成一个巨型脚本:wujie、坐标点击、封面 `waitForFileChooser` 需要 Agent 看 snapshot/截图。脚本负责可判定的登录与传文件;表单仍按下面 Agent 步骤。

## ego-browser API 速查

每次 `ego-browser nodejs` 是新进程;TaskSpace / Page 跨调用持久,JS 变量不持久。

| API | 说明 |
| --- | --- |
| `taskSpace(name\|id)` | 一个用户目标一个 space;记下 `spaceId` 复用 |
| `task.page("p1")` | 首屏用 goto 复用,别乱开页 |
| `page.snapshot()` | 观察(能看穿 open shadow);**点击**在 wujie 内常被拦截 |
| `page.evaluate(fn, arg)` | 只收**一个** JSON 可序列化参数(长文案走这里) |
| `page.mouse.click(x, y)` | wujie 内弹层常用 |
| `page.waitForFileChooser()` | 封面唯一稳妥上传路径 |
| `task.handOff()` | 交给人扫码发表 / 当面登录 |
| `task.finish({ keep: [] })` | 收尾关 space;需人接着看时用 handOff,不要长期占机器 |

**关键**:snapshot 看得见 wujie shadow;ref/text 点击会报 intercepts pointer events。wujie 内动作用 `evaluate` 里 `.click()` 或坐标 `mouse.click`。

## Agent 流程(登录通过之后)

### 0. 路由到发表页

**不要**直接 goto `/platform/post/create`(懒加载白屏)。走 UI:

`首页 → 左栏「内容管理」→「视频」→ 右侧「发表视频」`

就绪:wujie 内文案出现「视频描述」「短标题」,且有隐藏 `input[type=file][accept*="video"]` 与「保存草稿」。

```js
const state = await page.evaluate(() => {
  const app = document.querySelector('wujie-app');
  if (!app?.shadowRoot) return { wujie: false };
  const t = app.shadowRoot.textContent || '';
  return {
    wujie: true,
    hasDesc: t.includes('视频描述'),
    hasShort: t.includes('短标题'),
    hasUpload: !!app.shadowRoot.querySelector('input[type=file][accept*="video"]'),
    hasSave: t.includes('保存草稿'),
  };
});
```

### 1. 上传视频

先起 `serve-file`(端口默认 18765),再在页面里 fetch + DataTransfer 注入隐藏 file input(视频组件未做 isTrusted 加固;封面做了):

```js
const r = await page.evaluate(async (p) => {
  const root = document.querySelector('wujie-app').shadowRoot;
  const input = root.querySelector('input[type=file][accept*="video"]');
  const resp = await fetch(p.url);
  const dt = new DataTransfer();
  dt.items.add(new File([await resp.blob()], p.name, { type: 'video/mp4' }));
  input.files = dt.files;
  input.dispatchEvent(new Event('input', { bubbles: true }));
  input.dispatchEvent(new Event('change', { bubbles: true }));
  return 'injected ' + dt.files[0].size;
}, { url: 'http://127.0.0.1:18765/video.mp4', name: 'video.mp4' });
```

验证只看 DOM:出现「转发/赞/评论」预览卡、进度稳定 100%。`performance` resource **抓不到**上传 POST。上传中途不要填表;wujie 短暂 null 时等一会再探,**不要刷新**。

### 1.5 检查点:先存草稿

上传完成后**立刻**点一次「保存草稿」,再填描述/短标题/位置,最后再存。避免中途翻车重传大文件。

### 2. 描述 + 话题

描述是 `.input-editor`(非 contenteditable)。`execCommand`;文案从 evaluate 第二参数传入:

```js
await page.evaluate((p) => {
  const ed = document.querySelector('wujie-app').shadowRoot.querySelector('.input-editor');
  ed.focus();
  document.execCommand('selectAll', false, null);
  document.execCommand('delete', false, null);
  document.execCommand('insertText', false, p.desc);
}, { desc });
await page.evaluate((p) => {
  const ed = document.querySelector('wujie-app').shadowRoot.querySelector('.input-editor');
  ed.focus();
  document.execCommand('insertParagraph');
  document.execCommand('insertText', false, p.tags);
  ed.blur();
}, { tags });
```

### 3. 短标题

真 `<input>`,必须 native setter;验证读 `input.value`(innerText 不含 input 值)。

**服务端硬限制**(实测多期,触发即红字警告,保存被拒):

- **≤16 字**:超长直接拒(17 字实测被拒)。
- **符号白名单**:只有书名号、引号、冒号、加号、问号、百分号、摄氏度。
  - 全角逗号「,」→ 换空格(如「烂推荐看够了,自己搭一个」→「烂推荐看够了 自己搭一个」)。
  - 小数点会触发「特殊字符」警告:版本号里的 `6.1` 这类写法要改说法规避。
  - `|`、`#`、`@` 都不行:自带标题里的分隔符取主句(如「标题文字|栏目#066」→「标题文字」)。

填完后检查输入框下方有没有红字警告;有就改写后再保存。

### 4. 位置

默认常带城市。稳妥:`mouse.click` 打开 → snapshot/截图 → 点「不显示位置」→ **回读字段**确认。evaluate 坐标可能点到隐藏层。

### 5. 保存草稿(终点)

点「保存草稿」。成功信号:「已保存」+ 底部按钮变灰。再 goto 草稿箱核对。**草稿箱可见前不要离开编辑器**。

可选:声明原创、活动搜索(native setter 写搜索词;搜不到就「不参与活动」)。

#### 声明原创

真 checkbox(授权钥匙串行)。勾选后弹协议层:勾「我已阅读并同意…」→ 点层内「声明原创」确认按钮。两步都要坐标点击并回读 `input[type=checkbox].checked` 确认。

#### 活动搜索弹层

- 打开弹层点「不参与活动」字段;**弹层可能不弹**(点击被吞),探测 `input[placeholder*="搜索活动"]` 失败就重试 2–3 次。
- 搜索词用 **native setter + input/change 事件**(`keyboard.insertText` 间歇性失效);**二次搜索前先清空**——native setter 是覆盖,但焦点点击后残留词会被追加成「AI科普AI科普」。
- 列表渲染后**截图引导点击**:弹层行序和 y 坐标每期都可能变(还见过偏移一行的渲染 bug),按截图上的可见坐标点,点完**回读活动字段**确认选中项。点错行(相邻活动)若主题也合适可接受,不必强改。
- 视口尺寸变化会让「滚到顶部再点开」的既有坐标失效——每次都要重新探测触发器坐标。

### 6. 封面(可选)

唯一稳妥:`waitForFileChooser()` + 可信点击「上传封面」+ `chooser.setFiles(<绝对路径>)`。合成 `input.files` / OS 文件框方案已实测失败。确认按钮转圈结束、预览不再回退默认帧后再存草稿。

### 7. 收尾

- 停掉 `serve-file`(释放端口)。
- 需要人发表:草稿箱 `handOff()`。
- 否则 `finish({ keep: [] })`,**不要**长期占 space。

## 避坑清单

1. wujie shadow:查询从 `wujie-app.shadowRoot` 起。
2. 直链 create 白屏:必须 UI 路径进入发表页。
3. wujie 内 ref/text 点击被拦截 → evaluate 或坐标点击。
4. 长文案只经 evaluate 参数传入。
5. 描述用 execCommand;短标题用 native setter。
6. 位置两段式坐标 + 回读验证。
7. 封面只能 waitForFileChooser。
8. 上传验证只看 DOM;上传后先存草稿。
9. 草稿落盘前不导航离开。
10. 登录页「加载失败」是红鲱鱼;handOff 让人扫码。
11. **每次发布前 check-login**;exit 2 必须停。
12. 发表是人工终点;本技能只到草稿。
13. 短标题 ≤16 字 + 符号白名单(见 §3);触发红字就改写,别硬保存。
14. 原创两段式(勾选 + 协议层确认),活动弹层截图引导 + 回读(见 §5);「已保存」标签偶尔错过时机,草稿箱核对是最终事实。

## 测试

```bash
npm test
```