小红书长文发布 · 完整技能 v2
基于 5 轮实战迭代(不同字数、不同主题)验证的最佳实践。 区别于旧版,本技能使用「描述区 #话题名 → 等2秒 → Enter(精确匹配)」的话题添加方式, 不按方向键,直接Enter避免选中候选项后缀,成功率 100%。
📋 前置要求
环境要求
| 项目 | 要求 | 说明 |
|------|------|------|
| Playwright MCP | ✅ 必须 | 必需,用于文件上传(fileChooser 事件) |
| Playwright 浏览器模式 | 推荐 headed | 老大建议「有头模式」方便观察,--headless 可不传 |
| 小红书登录态 | ✅ 必须 | 首次需扫码,后续自动持久化 |
| 封面图 | ✅ 推荐准备 | 路径: ~/.openclaw/.playwright-mcp/xhs_cover.png |
浏览器选择(重要)
- 使用 Playwright MCP 工具(
playwright__browser_*),不要用 OpenClaw 内置浏览器(browser.*) - 原因:封面文件上传需要 Playwright 的
fileChooser事件,内置浏览器不支持 - Playwright 浏览器
--headless=false时,老大可以在 Chrome 窗口实时观看操作过程
Playwright MCP 配置
{
"mcp": {
"servers": {
"playwright": {
"command": "npx",
"args": ["-y", "@playwright/mcp"]
}
}
}
}
注:默认 headed。如异常为 headless 模式,排查启动参数中是否被强制加入
--headless。
封面策略
- 默认保持AI生成封面,不替换
- 老大要求换封面时再操作
📝 完整操作流程(10步·两个Block)
核心思路
Block A — 编辑页操作(内容+封面)
Block B — 发布页操作(标题+话题+声明)独立block,避免file chooser中断
Block A: 编辑页(1-5步)
Step 1: 导航到写长文页面
await page.goto('https://creator.xiaohongshu.com/publish/publish?from=menu&target=article')
// 等待 2-3 秒加载
Step 2: 点击"新的创作"
await page.locator('button:has-text("新的创作")').first().click({ force: true })
await page.waitForTimeout(2000)
Step 3: 填写内容
// 3a. 标题填一个空格(防止标题文字出现在 AI 自动生成的封面上)
await page.evaluate(() => {
const inputs = document.querySelectorAll('input, textarea')
for (const inp of inputs) {
if (inp.placeholder === '输入标题') {
inp.value = ' '
inp.dispatchEvent(new Event('input', { bubbles: true }))
break
}
}
})
// 3b. 填写正文(建议 ≥200 字以保证更好的排版效果)
const content = `你的文章内容...`
await page.evaluate((text) => {
const editor = document.querySelector('.tiptap.ProseMirror')
if (editor) {
editor.innerHTML = ''
const paragraphs = text.split('\n\n')
let html = ''
for (const p of paragraphs) {
html += `<p>${p.replace(/\n/g, '<br>')}</p>`
}
editor.innerHTML = html
editor.dispatchEvent(new Event('input', { bubbles: true }))
}
}, content)
// 3c. 验证写入(可选但推荐)
const chars = await page.evaluate(() =>
document.querySelector('.tiptap.ProseMirror')?.textContent.length || 0
)
Step 4: 一键排版 + AI封面生成
await page.locator('button:has-text("一键排版")').first().click({ force: true })
await page.waitForTimeout(6000) // 等排版+封面生成
// 验证页数
const pages = await page.evaluate(() => {
const counts = new Set()
document.querySelectorAll('*').forEach(el => {
const t = el.textContent?.trim() || ''
if (/^\d+\/\d+$/.test(t) && el.children.length === 0) counts.add(t)
})
return [...counts]
})
// pages 示例: ['1/2', '2/2'] 或 ['1/1']
Step 5: 封面检查
// 检查封面页数(仅做可视化确认,不替换封面)
const pages = await page.evaluate(() => {
const counts = new Set()
document.querySelectorAll('*').forEach(el => {
const t = el.textContent?.trim() || ''
if (/^\d+\/\d+$/.test(t) && el.children.length === 0) counts.add(t)
})
return [...counts]
})
// pages 示例: ['1/2', '2/2'] 或 ['1/1']
// ⚠️ 封面默认保持AI生成,老大要求换时再操作
Block B: 发布页(6-10步)
⚠️ 以下步骤必须写在独立的
run_code_unsafeblock 中, 因为 Block A 中的 fileChooser 事件会中断连续执行流。
Step 6: 进入发布页
await page.locator('button:has-text("下一步")').first().click({ force: true })
await page.waitForTimeout(3000)
Step 7: 填写标题(从内容提炼)
await page.evaluate(() => {
const inputs = document.querySelectorAll('input, textarea')
for (const inp of inputs) {
if (inp.placeholder?.includes('填写标题')) {
inp.value = '你的标题(从内容提炼,≤20字)'
inp.dispatchEvent(new Event('input', { bubbles: true }))
break
}
}
})
Step 8: 添加话题(关键核心)
不要使用"话题"按钮弹窗搜索,弹窗搜索框定位不稳定且 keyboard.type 容易打到正文区。
正确方式:在描述区输入 #话题名 后按 Enter 确认,自动变为蓝色链接。
const editor = page.locator('.tiptap.ProseMirror').first()
await editor.click()
// 话题必须从内容提炼,不能机械复用
// ⚠️ 关键:必须等建议弹窗出现后再按Enter,否则可能不变成蓝色
// ⚠️ 不要按 ArrowDown,直接 Enter 才能精确匹配,按方向键会选到候选项
const topics = ['#话题1', '#话题2', '#话题3']
for (const t of topics) {
await page.keyboard.type(t)
await page.waitForTimeout(2000) // 等建议弹窗出现(重要!)
await page.keyboard.press('Enter') // 直接Enter = 精确匹配
await page.waitForTimeout(1000)
}
await page.waitForTimeout(1000)
// 验证话题
const topicCheck = await page.evaluate(() => {
const ed = document.querySelector('.tiptap.ProseMirror')
if (!ed) return { confirmed: 0, pending: 0 }
return {
confirmed: ed.querySelectorAll('.tiptap-topic').length,
pending: ed.querySelectorAll('.suggestion').length
}
})
// ✅ confirmed=3, pending=0 才算成功
// pending>0 说明有话题没确认,需要补按 Enter
Step 9: 内容类型声明(新流程)
⚠️ 与旧版不同:不再有开关按钮。新版流程为:点击「添加内容类型声明」→ 弹窗 → 选「笔记含AI合成内容」
// 点击"添加内容类型声明"
await page.locator('text=添加内容类型声明').first().click({ force: true })
await page.waitForTimeout(1500)
// 在弹窗中选择"笔记含AI合成内容"
await page.evaluate(() => {
const all = document.querySelectorAll('*')
for (const el of all) {
if (el.children.length === 0 && el.textContent?.trim() === '笔记含AI合成内容') {
el.click()
return
}
}
})
await page.waitForTimeout(1000)
Step 10: 验证全部状态
const final = await page.evaluate(() => {
let title = ''
const inputs = document.querySelectorAll('input, textarea')
for (const inp of inputs) {
if (inp.placeholder?.includes('填写标题')) { title = inp.value; break }
}
let decl = false
const allDivs = document.querySelectorAll('div')
for (const div of allDivs) {
if (div.textContent.includes('原创声明')) {
const parent = div.parentElement
if (parent) {
const cb = parent.querySelector('input[type="checkbox"]')
if (cb) decl = cb.checked
}
break
}
}
const ed = document.querySelector('.tiptap.ProseMirror')
const topics = ed ? ed.querySelectorAll('.tiptap-topic').length : 0
const pending = ed ? ed.querySelectorAll('.suggestion').length : 0
return { title, topics, pending, declaration: decl }
})
// ✅ 期望: title="你的标题", topics=3, pending=0, declaration=true
🧪 边界情况处理
字数 vs 封面
| 字数(约) | chars(实测) | 封面情况 | 行为 | |----------|-------------|----------|------| | ≥230字 | ≥180 | ✅ 独立封面 | hover→上传 | | ~200字 | ~133 | ⚠️ 可能无封面(1/1) | 跳过封面 | | <150字 | <100 | ❌ 大概率无封面 | 跳过封面 |
结论:流程必须双分支。 不要假设一定有封面。
话题添加失败
症状: topics=0 或 pending>0
原因: keyboard.type 打到了正文区(发布页的编辑器接受焦点)
修复: 确认先 .click() 了 .tiptap.ProseMirror 再 type;每个话题后用 Enter 确认
原创声明开关找不到
症状: 找不到原创声明 checkbox
原因: 搜索到了页顶导航区的"原创声明"文本,而非内容设置区的开关
修复: 增加 text.includes('内容设置') 条件过滤
文件上传中断执行流
症状: Block B 的步骤不执行(标题空格、话题0)
原因: fileChooser 事件等待异步处理,run_code_unsafe 内的代码不会等待它完成
修复: 上传封面后,用一个单独的 run_code_unsafe block 执行 Block B
⚠️ 使用说明
执行前检查清单
- [ ] 小红书创作平台已登录(Playwright MCP 浏览器中)
- [ ] 封面图已放在
~/.openclaw/.playwright-mcp/xhs_cover.png - [ ] Playwright MCP 服务正常(
playwright__browser_*工具可用) - [ ] 内容正文已准备好(建议 ≥200 字)
- [ ] 标题从内容提炼,不超过 20 字
- [ ] 话题从内容提炼,3 个为佳
执行中关键检查点
- ✅ Block A:一键排版后验证页数(1/N vs 1/1)
- ✅ Block A:封面上传后验证(可选:检查 background-image 是否含 xhscdn URL)
- ✅ Block B:话题验证
confirmed=3, pending=0 - ✅ Block B:原创声明验证
checkbox.checked=true - ✅ Block B:标题验证是否正确写入
常见问题 FAQ
Q: 为什么话题是 pending>0?
A: 输入 #话题 后没按 Enter 确认。它只是建议状态,需要确认才变蓝色链接。
Q: 「从本地上传」不出现怎么办? A: 两种情况:① hover 位置不对——确认坐标约(168,386)在左侧封面预览区;② 小红书本次没提供上传选项——跳过,用 AI 封面。
Q: 原创声明弹窗没出现?
A: 可能是开关没点到正确位置。确认搜索条件 text.includes('原创声明') && text.includes('内容设置')。
Q: 标题显示为空格? A: 后续操作(如话题输入)抢了焦点了。每个 Block 结束时重新验证标题值。
📦 技能文件结构
skills/xhs-publish-v2/
├── SKILL.md ← 本文件:完整操作手册
└── references/
├── workflow.md ← 完整分步执行脚本(可选)
└── faq.md ← 常见问题集(可选)
🔄 版本历史
| 版本 | 日期 | 变更 | |------|------|------| | v2.1.0 | 2026-05-13 | 修正话题确认方式、原创声明新流程、封面默认不替换 | | v2.0.0 | 2026-05-13 | 完整重写:10步双Block流程、描述区话题Enter确认、多边界测试验证 | | v1.0.0 | 2026-05-09 | 初版:旧版流程,使用话题弹窗+ref定位(已弃用) |
💡 经验总结(跨场景可迁移)
这套方法论不限于小红书发布,可适用于其他平台自动化:
- 操作分区 — 多页表单拆分为独立区块,避免跨页状态污染
- 标签确认机制 — 输入→确认→验证三步法,适用于所有自动补全场景
- 兜底分支 — 对"不确定是否出现"的UI元素,预写双路径
- 每步验证 — 不自欺:每个操作后断言状态,留截图现场证据
- Block隔离 — file chooser / dialog 等异步交互,拆分独立执行单元
Scan to join WeChat group