description: "Automatically publish Markdown articles to the Xiaohongshu (XHS) drafts box. Supports multi-image upload (cover + materials), title/body filling, tag matching, and layout optimization checks. Based on Playwright file_chooser API + CDP operations." license: MIT metadata: version: 1.5.0 author: xiaocode + CCO Ives domains: [content, publishing, automation, xiaohongshu] type: automation requires: [playwright, openclaw-browser]
XHS Publisher — Xiaohongshu auto-publish v1.5Publish Markdown articles to the Xiaohongshu creator platform drafts box with one click. Supports multi-image upload.
When to use
- You have a finished XHS article (markdown) that needs to be uploaded to the drafts box
- You need to upload a cover + multiple material images (up to 18)
- Publish content to XHS in batches
- As part of an automated content distribution pipeline
- When another agent needs to publish XHS content
📋 Please confirm content compliance before publishing:
~/clawd/projects/MediaClaw/references/platforms/xiaohongshu.md
Not applicable
- ❌ Direct official publishing is required (not a draft) — currently only saving to drafts is supported, with final publishing done manually
- ❌ Video content — only image-and-text notes are supported
- ❌ Editing already-published notes is needed
Prerequisites
| Condition | Description | How to check |
|---|---|---|
| Playwright | Python playwright library | pip show playwright |
| OpenClaw Browser | Browser service running | curl -s http://127.0.0.1:18800/json/version |
| XHS Cookie | Playwright state file valid | ~/.playwright-data/xiaohongshu/state-default.json |
| Cookie not expired | cookies.expires > current time | Check the expires field of a1 in the cookie |
| unset proxy | The CDP WebSocket will be blocked by the proxy | unset ALL_PROXY all_proxy https_proxy http_proxy |
Input
输入参数:
article_path: string # Markdown 文件路径
cover_path: string # 封面图路径 (jpg/png/webp, 推荐 3:4 ≥720x960)
images: list[string] # 追加素材图片 (可选, 最多17张, 总计≤18)
decision: draft # 当前仅支持 draft
Markdown format requirements
📌 标题:你的标题(≤20字)
🏷️ 标签:#标签1 #标签2 #标签3
📝 正文:
正文内容...
(≤1000字)
Output
输出:
status: ok | error
draft_saved: true/false
draft_count: "草稿箱(N)"
title: string
title_truncated: boolean
content_length: number
images_uploaded: number # 上传图片总数 (含封面)
tags_added: list[string]
Usage
Single image (cover only)
unset ALL_PROXY all_proxy https_proxy http_proxy
python3 ~/clawd/projects/MediaClaw/skills/xhs-publisher/scripts/publish.py \
--article /path/to/article.md \
--cover /path/to/cover.jpg \
--decision draft
Multiple images (cover + material images)
unset ALL_PROXY all_proxy https_proxy http_proxy
python3 ~/clawd/projects/MediaClaw/skills/xhs-publisher/scripts/publish.py \
--article /path/to/article.md \
--cover /path/to/cover.jpg \
--images img1.jpg img2.png img3.jpg \
--decision draft
Shell glob batch materials
python3 publish.py \
--article article.md \
--cover cover.jpg \
--images 素材/*.png 素材/*.jpg
Call as a module
import asyncio
from scripts.publish import publish_to_xhs
result = asyncio.run(publish_to_xhs(
article_path="/path/to/article.md",
cover_path="/path/to/cover.jpg",
images=["img1.jpg", "img2.png"], # 可选
decision="draft",
))
print(result)
Agent call (recommended)
请发布文章到 XHS 草稿箱:
- 文章: ~/clawd/projects/MediaClaw/output/articles/2026-04-16/claude-vs-codex/xhs/article.md
- 封面: ~/clawd/projects/MediaClaw/output/articles/2026-04-16/claude-vs-codex/xhs/cover-3x4.jpg
- 素材: ~/clawd/projects/MediaClaw/output/articles/2026-04-16/claude-vs-codex/素材/*.png
Core flow
解析 Markdown → 验证图片 → 启动浏览器 → 加载 Cookie → 打开创作者页面
→ 切换到"上传图文"tab(最后一个可见tab)
→ file_chooser 上传封面 → 等待编辑器出现
→ 逐张追加素材图(通过编辑器内 file input)
→ 填充标题 → 填充正文 → 匹配标签 → 截图 → 存草稿
Key technical changes in v1.5
❌ Old approach (v1.4, deprecated)
# 不工作:set_input_files 不触发 XHS React change 事件
await file_input.set_input_files(paths)
✅ New approach (v1.5, verified)
# 1. 用 file_chooser API(Playwright 标准文件上传方式)
async with page.expect_file_chooser() as fc:
await file_input.evaluate("el => el.click()")
file_chooser = await fc.value
await file_chooser.set_files(path)
# 2. 点击最后一个可见的"上传图文"tab(XHS 有3个重复 tab)
tabs = document.querySelectorAll('div.creator-tab')
imageTabs = tabs.filter(t => t.text === '上传图文' && visible)
imageTabs[imageTabs.length - 1].click() # 最后一个!
Multi-image upload flow
1. 封面 → file_chooser.set_files(cover) → 等待8s → 编辑器出现
2. 素材图1 → 编辑器内 file_input.click() → file_chooser.set_files(img1) → 等待4s
3. 素材图2 → 同上
...
N. 最多封面+17张追加(XHS 总上限18张)
Key findings:
- The XHS file input has no
[multiple]attribute (Playwright reads multiple=false) - Even if
multiple=true,set_input_files([file1, file2])is injected, it does not trigger React file_chooser.set_files()is the only reliable upload method- Appending images within the editor is triggered via a hidden
input[type='file']trigger
Key Selectors
| Element | CSS Selector | Description |
|---|---|---|
| Tab switch | div.creator-tab (text="Upload image and text") |
Click the last visible one |
| Image upload | input.upload-input[type="file"] |
Hidden element, use file_chooser |
| In-editor image | input[type="file"] |
Hidden file input inside the editor |
| Title input | input.d-text[type="text"] |
placeholder="Fill in a title to get more likes" |
| Body editor | div.tiptap.ProseMirror[contenteditable="true"] |
TipTap ProseMirror |
| Topic button | button.contentBtn.topic-btn |
Open the tag panel |
| Save draft button | "Save and leave" (button text) | JS click by textContent |
| Publish button | "Publish" (button text) | JS click by textContent |
Boundary handling
1. Title 20-character limit
XHS strictly limits titles to 20 characters (including punctuation); anything over is automatically truncated and flagged.
2. Body 1000-character limit
XHS limits the body to 1000 characters (including spaces and emoji); anything over is automatically truncated.
3. Image limit of 18
Cover + additional images must not exceed 18 in total. Anything over is automatically truncated with a warning.
4. ProseMirror filling
await page.evaluate('''([el, html]) => {
el.innerHTML = html;
el.dispatchEvent(new Event("input", { bubbles: true }));
}''', [editor, body_html])
5. Hashtag strategy
Use strategy 1: append hashtags with #标签1 #标签2 to the end of the body. XHS automatically recognizes and highlights them.
6. Upload image-text Tab switching (pitfall)
XHS has 3 duplicate div.creator-tab(text="Upload image-text").
- First one: may point to the video area (clicking it enters an error state)
- Last one: the correct image-text upload entry
# 点击最后一个可见的"上传图文"tab
tabs = document.querySelectorAll('div.creator-tab')
imageTabs = Array.from(tabs).filter(t => t.textContent.trim() === '上传图文' && t.getBoundingClientRect().x > 0)
imageTabs[imageTabs.length - 1].click()
7. Proxy issue
ALL_PROXY / socks:// environment variables will block the Playwright CDP WebSocket connection.
Before running the script you must:
unset ALL_PROXY all_proxy https_proxy http_proxy
Safety mechanisms
Pre-flight Health Check
Before each publish, automatically check: browser connection, Cookie validity, file existence, image format.
Safety Screenshots
打开页面 → [截图#1] → 上传封面 → [截图#2]
→ 填标题 → [截图#3] → 填正文 → [截图#4]
→ 存草稿 → [截图#5] → 草稿确认 → [截图#6]
Screenshot directory:/tmp/xhs_safety/
Retry mechanism
| Failure scenario | Retry count | Backoff strategy |
|---|---|---|
| Network timeout | 3 times | 10s → 20s → 40s |
| Selector not found | 2 times | 5s → 10s |
| File upload failed | 3 times | 10s → 20s → 40s |
| Page load failed | 2 times | 15s → 30s |
FAQ
Q: The editor doesn't appear after uploading images?
A: You may have clicked the wrong tab. XHS has 3 duplicate tabs; you must click the last one. Fixed in v1.5.
Q: Uploading multiple images reports "Non-multiple file input"?
A: v1.5 has switched to the file_chooser API and no longer relies on the input[multiple] attribute.
Q: CDP connection timeout?
A: Check whether proxy environment variables are set:unset ALL_PROXY all_proxy https_proxy http_proxy
Q: What to do if the Cookie expires?
A: Daniel logs in to the XHS creator platform through the browser, and the Cookie is saved automatically.
Dependencies
playwright>=1.40.0
Pillow (可选, 图片尺寸验证)
Trigger words
- "Publish to Xiaohongshu", "Post to XHS", "Upload to XHS drafts"
- "Xiaohongshu publish", "XHS publish", "Xiaohongshu draft"
- "publish xhs"、"xhs draft"、"MediaClaw XHS"
- "XHS multi-image publish", "Xiaohongshu multi-image upload"
Changelog
- v1.5.0 (2026-04-16): Multi-image upload + core fixes
- ✅ Multi-image upload: cover + additional asset images (up to 18)
- ✅ Fix: use
file_chooserAPI instead ofset_input_files(React does not respond to the latter) - ✅ Fix: click the last visible "Upload image-text" tab (XHS has 3 duplicate tabs)
- ✅ Added
--imagesparameter (optional, supports shell glob) - ✅ Added image validation (existence, format, limit of 18)
- ✅ Added reminder to unset proxy environment variables
- 🧪 Verified by actual testing: single-image draft (24) ✅ multi-image (1 cover + 2 assets) draft (26) ✅
- v1.2.0 (2026-04-16): Security enhancements
- Pre-flight health check
- Secure screenshot mechanism
- Retry mechanism + exponential backoff
- Refined error handling strategy
- v1.1.0 (2026-04-14): Initial version
- Full selector verification
- ProseMirror filling solution
- Title/body limit handling
- Tag embedding strategy