description: "Douyin image-text/video publishing (OpenClaw Browser automation): login detection → content validation → upload page navigation → fill in title/body → upload cover + assets → save and leave. By default, only save as draft; publish only after Daniel confirms. Triggers: '发抖音', '抖音发布', 'douyin publish', '抖音图文', '抖音视频', '发布短视频'." author: Daniel Li version: 2.0.0
Douyin Publishing v2.0 — OpenClaw Browser automationCore tech stack: OpenClaw
browsertools(does not rely on standalone Playwright scripts) Security principle: by default, only save as draft; only after Daniel explicitly confirms is clicking publish allowed
⚠️ Ironclad security rules
- Default
mode=draft— the endpoint of every publishing process is "Save and leave", not "Publish" - Publish confirmation gate — only after receiving Daniel's explicit reply "可以发/发布/ok" is switching to publish allowed
- Screenshot every step — take screenshots at key nodes as evidence, so there is a record to check when failures occur
- Stop after 3 failures — if any step fails 3 times in a row, take a screenshot + report + switch to "awaiting Daniel's takeover"
- If encountering a CAPTCHA/slider/rate limit — stop immediately, take a screenshot, and wait for Daniel to handle it
📋 Pre-publish compliance check
Before publishing, refer to references/platform-rules.md to confirm content compliance:
- 11 major categories of violating content in the Community Guidelines
- AIGC labeling requirements ("Content generated by AI")
- Account health score requirements
- Prohibition of reuploads/watermarks/low-quality content
🔄 Complete Workflow (6 steps)
Step 1: 登录检测 ──→ Step 2: 内容检测 ──→ Step 3: 进入发布页
│
Step 6: 暂存离开 ←── Step 5: 上传素材 ←── Step 4: 填写内容
│
▼
确认回传给 Daniel
Step 1: Login detection
Goal: Confirm that the browser is logged in to the Douyin Creator Platform; if not logged in, send the QR code to Daniel
1.1 Open the browser
browser(action="open", url="https://creator.douyin.com/creator-micro/home", profile="user")
profile="user"Use Daniel's local browser (highest probability of already having a login session) If the user profile is unavailable, use the default profile (OpenClaw-managed browser)
1.2 Wait for the page to load & detect login status
browser(action="snapshot")
Determine whether already logged in:
- ✅ Logged in: URL contains
creator-micro, and creator backend content appears on the page - ❌ Not logged in: URL contains
login/passport, or the page shows "Scan QR code to log in" "Verification code login"
1.3 Not logged in handling: send QR code
If not logged in:
# 截取二维码区域
browser(action="screenshot", path="/tmp/douyin_qr_login.png")
Then send a message to Daniel:
message(action="send", message="⚠️ 抖音未登录,请扫描二维码登录。登录完成后回复"ok"",
media="/tmp/douyin_qr_login.png")
After waiting for Daniel's reply, re-check:
browser(action="navigate", url="https://creator.douyin.com/creator-micro/home")
# 等待 5 秒让页面加载
browser(action="snapshot")
# 确认已进入创作者后台
1.4 Success indicator
Take a screenshot to confirm and record:
browser(action="screenshot", path="/tmp/douyin_step1_logged_in.png")
Step 2: Content and asset detection
Goal: Before starting, confirm that all files are ready, formats are correct, and content is compliant
2.1 Check asset files
# 确认文件存在、格式、大小
ls -lh /path/to/images/*.jpg /path/to/cover.png
file /path/to/images/*.jpg
Image-text requirements:
| Item | Requirement |
|---|---|
| Number of images | 2-35 images |
| Format | JPG, PNG, WebP(GIF not supported) |
| Single image size | ≤50MB |
| Recommended ratio | 3:4 (1080×1440) or 4:3 |
Video requirements:
| Item | Requirement |
|---|---|
| Format | MP4 (H.264) |
| Resolution | ≥720×1280, recommended 1080×1920 |
| Size | ≤16GB |
| Duration | 15 seconds - 60 minutes |
2.2 Check text content
# 确认标题和描述文件
cat /path/to/content.md
Validation rules:
- Title ≤20characters (measured image-text page title box limit is 20 characters)
- Description ≤200 characters (including hashtags)
- 3-5 hashtags, format
#话题名(no spaces) - AI content marked "Content generated by AI"
2.3 Output inspection report
Confirm with Daniel:
📋 内容检测报告
━━━━━━━━━━━━━━
📌 标题:{title}({len}字)
📝 描述:{desc[:50]}...({len}字)
🏷️ 话题:{tags}
🖼️ 素材:{count} 张图片 / {video} 视频
📐 格式:{formats}
📦 大小:{total_size}
✅ 全部就绪,开始发布流程
Step 3: Enter the Douyin image-text publishing page
Goal: Navigate to the correct publishing page and handle possible pop-ups
3.1 Navigation
Image-text publishing:
browser(action="navigate", url="https://creator.douyin.com/creator-micro/content/upload?default-tab=3")
Video publishing:
browser(action="navigate", url="https://creator.douyin.com/creator-micro/content/upload")
3.2 Wait for the page to be ready
# 等待文件上传控件出现(证明页面已完全加载)
browser(action="snapshot")
Check whether the snapshot contains input[type="file"]. If not:
- Wait 5 seconds and try again
- If it still does not appear within 30 seconds, it may be blocked by a pop-up → handle the pop-up
3.3 Handle interfering pop-ups
Douyin often pops up a "last unpublished content" prompt. Detect and close it:
browser(action="act", kind="evaluate", fn="""() => {
const els = document.querySelectorAll('*');
for (const el of els) {
const t = el.textContent.trim();
if ((t === '放弃' || t === '不再提醒' || t === '新建图文')
&& el.offsetParent !== null && el.children.length === 0) {
el.click();
return 'dismissed: ' + t;
}
}
return 'no popup';
}""")
3.4 Screenshot confirmation
browser(action="screenshot", path="/tmp/douyin_step3_publish_page.png")
Step 4: Fill in the title and body text
Goal: Fill in the title and description (including hashtags) on the publishing page
4.1 Fill in the title
Find the title input area (prefer the ref in the snapshot):
# 方法1:通过 placeholder 定位(推荐)
browser(action="act", kind="fill", ref="{title_input_ref}", text="{标题文字}")
# 方法2:通过 evaluate 直接操作
browser(action="act", kind="evaluate", fn="""() => {
const sel = 'input[placeholder*="标题"]';
const el = document.querySelector(sel);
if (el) {
el.focus();
el.select();
el.value = '';
// 触发 React 的 onChange
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
nativeInputValueSetter.call(el, arguments[0]);
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
return 'title filled';
}
return 'title input not found';
}""")
Note: Douyin is a React SPA; directly
.value = xxxwill not trigger a state update. You must use thenativeInputValueSetterortypeaction.
4.2 Fill in the description (body text) ⚠️ Key step
2026-04-17 practical verification conclusion: The Douyin description area uses Slate.js (
data-slate-editor="true"), not a normal textarea.
- ❌
innerText = xxx→ visually displayed but the Slate state is not updated, and it is lost after saving as draft- ❌
document.execCommand('insertText')called multiple times → text stacks, and existing content cannot be cleared- ❌
ClipboardEvent('paste')→ blocked by browser security policy, Slate cannot read the data- ❌
new InputEvent('beforeinput')→ SlatepreventDefault()after which the actual insertion is not performed- ❌
browser act kind=type→ contenteditable div has no Playwright ref, timeout- ❌
browser act kind=fill→ does not support contenteditable- ✅ The only reliable method:from a completely fresh page in one go
execCommand('insertText'), and it can only be called once
Correct process (must be executed on a completely fresh blank editor):
# Step 1: 聚焦描述区
browser(action="act", kind="evaluate", fn="""() => {
const desc = document.querySelector('[data-slate-editor="true"]');
if (!desc) return 'not found';
desc.focus();
return 'focused';
}""")
# Step 2: 一次性插入文本(只能执行一次!)
browser(action="act", kind="evaluate", fn="""() => {
const desc = document.querySelector('[data-slate-editor="true"]');
if (!desc) return 'not found';
const text = '你的描述文字(建议≤200字,含#话题标签)';
document.execCommand('insertText', false, text);
// 触发 blur 确保 Slate 最终提交状态
desc.blur();
return 'inserted: ' + desc.innerText.length + ' chars';
}""")
# Step 3: 验证字数计数器已更新(从 0/N 变为实际字数/N)
browser(action="snapshot")
# 检查描述区域文本中是否显示 "实际字数 / 1000"
Verification points:
- The description area character counter changes from
0 / 1000to实际字数 / 1000→ ✅ React state has been updated - If the counter is still
0 / 1000→ ❌ the text was not written into the Slate state and will be lost after saving as draft
Notes:
- When the description contains
#, Douyin may pop up a hashtag search panel; in this case, you need to first click outside the panel to close it execCommand('insertText')on a Slate editor that already has contentcannot correctly clear(selectAll+delete is ineffective)- If the description is filled in incorrectly, you mustclear and re-upload(click the "Clear and re-upload" button) and go through the entire process again
- Do not try to call
execCommandmultiple times; each call will append to the existing text
4.3 Topic Tags
Include it directly in the description text: #话题1 #话题2 , and Slate will recognize it automatically.
Do notappend topics separately; this will cause text overlap issues.
4.4 Screenshot Confirmation
browser(action="screenshot", path="/tmp/douyin_step4_content_filled.png")
Step 5: Upload Cover and Media Images
Goal: Upload image assets to the publishing page
5.1 Locate the File Upload Control
# 通过 snapshot 获取 file input 的 ref
browser(action="snapshot")
Find the ref of input[type="file"][accept*="image"] .
5.2 Upload Media Images
2026-04-17 Field-verified:
file://fetch is blocked by browser security policyevaluateinDataTransfer+Fileconstruction is also blocked- Files must be placed in the
/tmp/openclaw/uploads/directory
Correct method (verified):
# Step 1: 复制文件到 uploads 目录
# cp /path/to/images/*.jpg /tmp/openclaw/uploads/
# Step 2: 点击上传按钮触发 file chooser
browser(action="snapshot") # 获取上传按钮 ref
browser(action="act", kind="click", ref="{upload_btn_ref}") # e.g. ref=e8 "上传图文"
# Step 3: 通过 browser upload 上传文件
browser(action="upload", paths=[
"/tmp/openclaw/uploads/img1.jpg",
"/tmp/openclaw/uploads/img2.jpg",
"/tmp/openclaw/uploads/img3.jpg"
])
# Step 4: 等待上传处理完成(页面显示"加载中"→编辑器就绪)
browser(action="act", kind="wait", timeMs=10000)
# Step 5: 验证 — snapshot 应显示"已添加N张图片"和编辑区按钮
browser(action="snapshot")
⚠️ Failed method (do not use):
Method B (deprecated): Set file input directly via evaluate
browser(action="act", kind="evaluate", fn="""async () => {
const input = document.querySelector('input[type="file"][accept*="image"]');
if (!input) return 'file input not found';
const files = [
'/path/to/img1.jpg',
'/path/to/img2.jpg',
'/path/to/img3.jpg',
];
// 通过 Playwright 的 setInputFiles(如果可用)
// 或通过 DataTransfer API
const dt = new DataTransfer();
for (const path of files) {
const resp = await fetch('file://' + path);
const blob = await resp.blob();
const file = new File([blob], path.split('/').pop());
dt.items.add(file);
}
input.files = dt.files;
input.dispatchEvent(new Event('change', { bubbles: true }));
return 'files set: ' + files.length;
}""")
Note: Method B may be blocked under certain browser security policies. If Method B fails, use Method A.
5.3 Wait for Upload to Complete
After images are uploaded, Douyin needs to process them (compress, generate thumbnails, etc.). Wait for signals:
# 等待编辑器按钮出现(表示上传处理完成)
browser(action="act", kind="wait", timeMs=5000)
# 检查是否出现了"暂存离开"/"发布"按钮
browser(action="snapshot")
Upload completion indicators:
- The "Save Draft and Leave" and "Publish" buttons appear at the bottom of the page
- The image preview area displays thumbnails of uploaded images
- No "Uploading" or progress bar
5.4 Adjust the Cover
If you need to specify a cover (select from uploaded images):
# 点击封面设置区域
browser(action="act", kind="click", ref="{cover_area_ref}")
# 等待封面选择弹窗
browser(action="act", kind="wait", timeMs=2000)
# 选择指定图片作为封面(通常点击图片缩略图即可)
browser(action="act", kind="click", ref="{target_image_ref}")
# 确认选择
browser(action="act", kind="click", ref="{confirm_btn_ref}")
5.5 Retry Mechanism
If upload fails:
- 1st failure: Wait 10 seconds and retry
- 2nd failure: Wait 30 seconds and retry
- 3rd failure:Stop, take a screenshot, report, and switch to "pending Daniel's takeover"
5.6 Screenshot Confirmation
browser(action="screenshot", path="/tmp/douyin_step5_assets_uploaded.png")
Step 6: Save Draft and Leave (Default Endpoint)
Goal: Click "Save Draft and Leave" to save the content to the drafts box
6.1 Close Blocking Pop-ups
Handle possible pop-ups before submission (co-creation center promotion, etc.):
browser(action="act", kind="evaluate", fn="""() => {
const els = document.querySelectorAll('*');
for (const el of els) {
const t = el.textContent.trim();
// 只关闭"我知道了"类提示,不要误关"确认"类弹窗
if (t === '我知道了' && el.offsetParent !== null && el.children.length === 0) {
el.click();
return 'closed: ' + t;
}
}
return 'no popup';
}""")
⚠️ Safety principle: Only close clearly harmless notification pop-ups ("I know"). Neverclose pop-ups containing ambiguous wording such as "Confirm" or "Discard" — take a screenshot and send it to Daniel for confirmation first.
6.2 Find the Save Draft Button
browser(action="snapshot")
Search the snapshot for any of the following buttons:
| Wording | selector |
|---|---|
暂存离开 |
Most common on image-text pages |
存草稿 |
Common on video pages |
草稿 |
Some versions |
6.3 Click Save Draft
# 使用 snapshot 中获取的 ref
browser(action="act", kind="click", ref="{draft_button_ref}")
6.4 Handle Post-submission Pop-ups
A confirmation pop-up may appear after clicking:
browser(action="act", kind="wait", timeMs=3000)
# 检查是否有"暂存"确认按钮
browser(action="act", kind="evaluate", fn="""() => {
const els = document.querySelectorAll('*');
for (const el of els) {
const t = el.textContent.trim();
if ((t === '暂存' || t === '确认' || t === '我知道了')
&& el.offsetParent !== null && el.children.length === 0) {
el.click();
return 'clicked: ' + t;
}
}
return 'no post-submit popup';
}""")
6.5 Wait for Success Signals
browser(action="act", kind="wait", timeMs=5000)
Success indicators:
- URL redirects to the draft management page
content/manage - Text such as "Saved successfully" or "Draft saved" appears on the page
- The upload page closes
6.6 Retry Mechanism
If saving the draft fails:
- Check whether the button is disabled → the content may not be fully filled in
- Check whether there is an error pop-up → take a screenshot and send it to Daniel
- 1st failure: Wait 5 seconds and retry
- 2nd failure: Wait 10 seconds and retry
- 3rd failure:Stop, screenshot, report
6.7 Final screenshot confirmation
browser(action="screenshot", path="/tmp/douyin_step6_draft_saved.png")
📤 Confirm return (Step 7: Reporting)
After the publishing process ends, send the standard report to Daniel:
📋 抖音发布报告
━━━━━━━━━━━━━━
📌 标题:{title}
📝 描述:{desc[:80]}...
🏷️ 话题:{tags}
🖼️ 素材:{count} 张图片 / 视频
📐 封面:{cover_status}
✅ 状态:已存草稿 / 待确认发布 / 待接管
📸 截图:{screenshot_path}
🔗 草稿管理:https://creator.douyin.com/creator-micro/content/manage
Attach the final screenshot as confirmation evidence.
🚀 Publishing process (only execute after Daniel confirms)
If Daniel confirms publishing (rather than only saving a draft), replace Step 6:
6-Alt: Click publish
# 找到发布按钮
browser(action="snapshot")
# 查找 "button:has-text('发布')" 的 ref
# 点击发布
browser(action="act", kind="click", ref="{publish_button_ref}")
After publishing, wait for the success signal (same as 6.5), and confirm with a screenshot.
🔧 Error handling summary table
| Failure point | Retry strategy | Stop condition |
|---|---|---|
| Login detection | Reopen the page | Escalate to manual QR code scan |
| QR code login | Wait for Daniel | Timeout 120 seconds |
| File does not exist | Report missing file | Stop immediately |
| Unsupported format | Report and suggest conversion | Stop immediately |
| Page load timeout | Refresh and retry | 3 failures |
| Popup interference | Close the prompt popup | If a serious popup is closed by mistake, stop |
| Title filling failed | Retry | 3 failures |
| Description filling failed | Retry | 3 failures |
| Image upload failed | Exponential backoff retry | 3 failures |
| Cover setting failed | Skip (non-blocking) | — |
| Save draft button not found | Check text variants | 3 failures |
| No success signal after saving draft | Check drafts | 3 failures |
| CAPTCHA/slider | Stop immediately | Wait for Daniel to handle |
| Rate limit | Stop immediately | Wait 30 minutes |
🔗 Platform entry quick reference
| Page | URL |
|---|---|
| Creator homepage | https://creator.douyin.com/creator-micro/home |
| Image-text publishing | https://creator.douyin.com/creator-micro/content/upload?default-tab=3 |
| Video publishing | https://creator.douyin.com/creator-micro/content/upload |
| Draft management | https://creator.douyin.com/creator-micro/content/manage |
📝 Posting time suggestions
| Time slot | Recommendation level |
|---|---|
| 7:00-9:00 | ⭐⭐⭐ |
| 11:30-13:00 | ⭐⭐⭐ |
| 17:30-19:00 | ⭐⭐⭐ |
| 20:00-22:00 | ⭐⭐⭐⭐⭐ |
| 22:00-00:00 | ⭐⭐⭐⭐ |
📁 File structure
douyin-publish/
├── SKILL.md ← 本文件(workflow 主文件)
├── references/
│ ├── platform-rules.md ← 完整平台规则
│ ├── handoff-and-human-trace.md ← 接管规则与人类痕迹规范
│ └── content-style-guide.md ← 内容风格指南
└── templates/
├── desc-template.md ← 描述排版模板
└── video-script-template.md ← 视频脚本模板
📝 Minimal "human trace" standards
Avoid "robotic feel"
- Descriptions should not all use the same sentence pattern: allow 1 short hook + 1-2 explanatory sentences + 1 CTA
- emoji Moderate: 1 per 2-3 sentences
- Topics should not use a fixed template: 1-2 precise + 1-2 scenario + 0-1 broad
- Avoid piling on low-quality topics like "hit the trending page/gain followers"
v2.0.0 (2026-04-17): Full refactor — migrated from Playwright scripts to OpenClaw Browser automation v2.0.1 (2026-04-17): Step 4 description filling fix — Slate.js can only insert once via execCommand; Step 5 upload path fix v1.0.0 (2026-03-27): Initial version, implemented with Playwright scripts