SuperAPI AI Directory

douyin-publisher

Marketing & growth

View repository

Updated 10-15 10:21

About this skill

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 automation

Core tech stack: OpenClaw browser tools(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

  1. Default mode=draft — the endpoint of every publishing process is "Save and leave", not "Publish"
  2. Publish confirmation gate — only after receiving Daniel's explicit reply "可以发/发布/ok" is switching to publish allowed
  3. Screenshot every step — take screenshots at key nodes as evidence, so there is a record to check when failures occur
  4. Stop after 3 failures — if any step fails 3 times in a row, take a screenshot + report + switch to "awaiting Daniel's takeover"
  5. 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 = xxx will not trigger a state update. You must use the nativeInputValueSetter or type action.

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') → Slate preventDefault() 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 / 1000 to 实际字数 / 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 policy
  • evaluate in DataTransfer + File construction 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