Install
openclaw skills install @songhonglei/xhs-image-note-release小红书图文笔记自动发布技能。通过 ego-browser 自动化完成图片上传、标题填写、正文编辑、 话题标签、发布等全流程。核心解决了小红书发布按钮封装在 closed Shadow DOM 中无法点击的问题。 当用户要求发小红书、发布图文笔记、上传到小红书、小红书发帖或涉及小红书内容发布时触发此技能。 前置依赖:ego-browser (ego-lite) 已安装且正在运行,小红书账号已登录。
openclaw skills install @songhonglei/xhs-image-note-release通过 ego-browser 自动化发布小红书图文笔记。覆盖从打开创作平台到发布成功的完整流程,包含图片批量上传、标题/正文填写、话题标签、以及最关键的发布按钮处理。
本技能依赖以下外部工具/技能,必须在使用前安装并配置:
| 依赖 | 类型 | 用途 | 安装方式 | 验证 |
|---|---|---|---|---|
| ego-browser (ego-lite) | CLI 工具 + Skill | 浏览器自动化引擎,提供 CDP、snapshot、fillInput 等 API | 参考 ego-browser skill 安装 | ego-browser --version 能正常输出版本号 |
| 小红书账号 | 平台账号 | 需在 ego-lite 浏览器中已登录小红书 | 手动在 ego-lite 中登录 creator.xiaohongshu.com | 打开创作平台能看到发布按钮 |
| WorkBuddy 沙箱 | 环境配置 | 沙箱模式会 SIGKILL ego-browser 进程(exit 137) | WorkBuddy 设置 → 关闭沙箱 | 运行 ego-browser --version 不报 sandbox 错误 |
ego-browser API:本技能代码中使用的
useOrCreateTaskSpace、openOrReuseTab、snapshotText、click、fillInput、typeText、pressKey、cdp、js、pageInfo、completeTaskSpace、captureScreenshot、wait、waitForNetworkIdle、cliLog均为 ego-browser 提供的 API,需先Skill("ego-browser")加载后使用。
Skill("ego-browser") 加载浏览器操作技能创建 task space → 打开创作平台 → 等待 SPA 渲染(15s) → 进入图文发布页(自动兼容 tab/下拉)
→ 上传图片(CDP 批量) → 填标题 → 填正文 → 关闭话题弹窗
→ 调用 _onPublish() 发布 → 等待跳转确认 → 清理 task space
ego-browser nodejs <<'EOF'
const task = await useOrCreateTaskSpace('publish xhs note')
await openOrReuseTab('https://creator.xiaohongshu.com/publish/publish', { wait: true, timeout: 25 })
// Vue SPA,必须等待 15 秒渲染
await wait(15)
EOF
关键点:小红书创作平台是 Vue SPA,<div id="app"> 初始为空,waitForNetworkIdle 不够,必须 await wait(15)。
创作平台支持两种入口进入图文编辑页,脚本会自动兼容两种 UI:
脚本逻辑:先试方式 A(按文本遍历点击),检查是否出现 input.upload-input;如果没有,回退到方式 B(snapshotText 匹配 ref 点击)。
// 方式 A:点击顶部「上传图文」tab
const tabClicked = await js(`(() => {
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false)
let node
while (node = walker.nextNode()) {
if (node.textContent.trim() === '上传图文') {
let element = node.parentElement
for (let i = 0; i < 4; i++) {
if (!element) break
element.click()
element.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }))
element = element.parentElement
}
return true
}
}
return false
})()`)
await wait(3)
// 检查是否已进入图文编辑页
if (!await js(`!!document.querySelector('input.upload-input')`)) {
// 方式 B:回退到「发布笔记」下拉菜单
const pageText = await snapshotText()
const matchPublish = pageText.match(/发布笔记.*?\[ref=(\d+)/)
if (matchPublish) {
await click('@' + matchPublish[1])
await wait(2)
const text2 = await snapshotText()
const matchUpload = text2.match(/上传图文.*?\[ref=(\d+)/)
if (matchUpload) {
await click('@' + matchUpload[1])
await waitForNetworkIdle(5)
await wait(3)
}
}
}
注意:两种 UI 可能同时存在或随版本切换,脚本以
input.upload-input是否出现为判断标准,自动选择可用入口。
不能用 uploadFile() 传逗号分隔多文件路径(不生效)。必须用 CDP:
const doc = await cdp('DOM.getDocument', {})
const inputNode = await cdp('DOM.querySelector', {
nodeId: doc.root.nodeId,
selector: 'input.upload-input'
})
await cdp('DOM.setFileInputFiles', {
files: ['/abs/path/img1.png', '/abs/path/img2.png', ...],
nodeId: inputNode.nodeId
})
await wait(8) // 等待图片处理
await fillInput('css:input[placeholder="填写标题会有更多赞哦"]', '标题内容')
正文区域是 contenteditable="true" 的富文本编辑器:
// 先 focus 编辑器
await js(`document.querySelector('[contenteditable="true"]').focus()`)
// 再用 typeText 输入(支持换行和 #话题标签)
await typeText('正文内容...\n\n#话题1 #话题2')
await wait(1)
// 按 Esc 关闭话题建议弹窗
await pressKey('Escape')
小红书的「发布」按钮封装在 <xhs-publish-btn> 自定义组件中,使用 closed Shadow DOM:
<xhs-publish-btn submit-text="发布" submit-disabled="false">
#shadow-root (closed) ← closed 意味着 host.shadowRoot 返回 null
<div class="publish-page-publish-btn">
<button class="ce-btn bg-red">发布</button>
</div>
</xhs-publish-btn>
| 方法 | 结果 |
|---|---|
querySelector('button.ce-btn.bg-red') | 找不到(在 shadow DOM 内) |
host.shadowRoot.querySelector(...) | shadowRoot 为 null(closed) |
click('@N') / 坐标点击 | snapshotText 不穿透 shadow DOM |
cdp('DOM.performSearch', {pierce: true}) | 找到节点但 nodeId=0 |
cdp('Input.dispatchMouseEvent', {x, y}) | 不触发 Vue 事件 |
直接调用组件暴露的内部方法 _onPublish():
const host = document.querySelector('xhs-publish-btn')
// 检查状态
const disabled = host.getAttribute('submit-disabled') // 'false' = 可点
const loading = host.getAttribute('submit-loading') // 'false' = 未加载
// 触发发布
host._onPublish()
原理:<xhs-publish-btn> 是 Web Component,原型链上暴露了 _onPublish 和 _onSave 方法。通过 host._onPublish() 直接调用,绕过 Shadow DOM 封装。
发现方法:Object.getOwnPropertyNames(Object.getPrototypeOf(host)) 列出原型方法找到 _onPublish。
调用 _onPublish() 后按钮进入 loading,约 5-10 秒后页面会重置并出现 published=true URL 参数。验证:
const info = await pageInfo()
if (info.url.includes('published=true') || info.url.includes('note-manage')) {
cliLog('SUCCESS: 发布成功!')
}
await completeTaskSpace(task.id, { keep: false })
修改 scripts/publish_note.sh 中的 4 个参数后直接运行:
IMAGE_DIR="/path/to/images"
IMAGES="img1.png,img2.png,img3.png"
TITLE="标题"
BODY='正文内容\n\n#话题1 #话题2'
bash ~/.workbuddy/skills/xhs-image-note-release/scripts/publish_note.sh
from the default agent sandbox,需在 WorkBuddy 设置中关闭沙箱后重试waitForNetworkIdle 代替#话题 自动被识别,输入后弹建议列表,按 Escape 关闭`)和 ${,否则会中断脚本scripts/publish_note.sh — 一键发布脚本,修改 4 个参数即可复用references/publish-method.md — 完整方法文档,含失败方案对比表和技术原理详解。按需加载:当需要了解发布按钮失败方案的完整对比、或需要排查 closed Shadow DOM 穿透问题时阅读此文件;正常发布流程无需提前加载