Take from the VoiceDrop bookshelfthe books I wrote, one thread per book to X. First post is a hook + cover image, the middle follows the book's thread of thought,the last post explains how this book was written with VoiceDrop。
Core Principle
One book = one thread. The book is already a finished product; the thread is its trailer — from the chapter summaries and the main textextractthe hardest content, don't re-conceive it, don't write marketing tone.
Only post my books. The bookshelf is a community bookshelf, and it has books written by other users on it. Posting them would be speaking on someone else's behalf, absolutely not allowed.
Workflow
Step 1: List my books, exclude ones already posted
mcp__voicedrop__list_books # 公开书架,免登录
- hidden books (in book.json
hidden: true) are already filtered out server-side,no need to handle them。 - my book determination:
author == "王建硕"or empty(early books have no byline, they're all mine) → mine. Other bylines (蜜蜡 / Dennis / sissi / …) → someone else's book,don't post。 - Cross-check against
state/history.jsonlto exclude ones already posted, list the remaining ones and let the user pick. User names a specific book → use that one directly.
Step 2: Read the book
mcp__voicedrop__read_book <slug> # 目录 + 每章梗概 + 一句话简介
mcp__voicedrop__read_book_chapter <slug> intro # 序章;再挑 1-2 个最有料的章读正文
The summary gives the skeleton, the main text gives quotable details (numbers, metaphors, counterintuitive assertions).
Step 3: Split into a thread (structural contract)
| posts | content |
|---|---|
| 1 | Hook: the book's core assertion or question + book title,with cover image https://voicedrop.cn/books/<slug>/cover.jpg |
| 2 … N-2 | Follow the book's thread of thought, each post fully covers one point, extracted from the summary/main text. 6–10 posts total |
| N-1 | Read the whole book free:https://voicedrop.cn/books/<slug>/ |
| N (required) | How this book was written: the real process — dropped a line of what central idea (seed) into VoiceDrop, the book grew on its own, chapters were filled in one by one, anything unsatisfactory was fixed with one sentence. Ends with voicedrop.cn |
Hard constraints: weighted length per post ≤280 (each Chinese character counts as 2),buffer to ~120 Chinese characters. 王建硕's tone: plain, homey metaphors. No hashtags, no emoji, no @. Links only appear in the N-1 and N posts. Pangu spacing.
Step 4: Review
List the whole thread for the user to confirm (number each post + character count). Only skip if the user has said --yes or explicitly "post directly".
Step 5: Post (xurl chained)
SLUG=<slug>
curl -s -o /tmp/cover.jpg "https://voicedrop.cn/books/$SLUG/cover.jpg"
MEDIA_ID=$(xurl media upload --category tweet_image --media-type image/jpeg /tmp/cover.jpg \
| grep -oE '"media_id_string":"[0-9]+"' | grep -oE '[0-9]+')
# 注意:不带 --category tweet_image 会按视频处理,media id 无效
# 首条(带图)
resp=$(xurl -X POST -d "$(jq -nc --arg t "$T1" --arg m "$MEDIA_ID" \
'{text:$t, media:{media_ids:[$m]}}')" /2/tweets)
# X 返回的 text 里有裸换行,jq 会拒;grep 抠 id
PREV=$(printf '%s' "$resp" | grep -oE '"id":"[0-9]+"' | head -1 | grep -oE '[0-9]+')
[[ -n "$PREV" ]] || { echo "POST failed: $resp"; exit 1; }
# 2…N 条依次 reply 上一条,条间 sleep 5
resp=$(xurl -X POST -d "$(jq -nc --arg t "$TN" --arg r "$PREV" \
'{text:$t, reply:{in_reply_to_tweet_id:$r}}')" /2/tweets)
If any post fails:stop where you are, give the user which post number you got to + the original failure text, don't skip posts and continue (broken chain). First post URL:https://x.com/jianshuo/status/<首条id>。
Step 6: Record
jq -nc --arg date "$(date +%F)" --arg slug "$SLUG" --arg id "$FIRST_ID" --argjson n $N \
'{date:$date,slug:$slug,thread_first_id:$id,tweets:$n,status:"posted"}' \
>> ~/.claude/skills/wjs-publishing-books-to-x/state/history.jsonl
At the end tell the user: thread URL, which book, how many posts, how many books are left unposted.
Multiple books in a row
By default one session postsone book. If the user wants to "post all the books" → schedule each book ≥4 hours apart (posting multiple threads in one go will get flagged by X as spamming), copy the mechanism from wjs-tweeting-from-articles 's batch scheduling pattern (queue + cron self-throttling).
Daily automation (already installed)
launchd runs every day at 10:10 runs daily.sh: automatically pick the latest book that hasn't been posted yet (skip old books with chapters=0) → claude -p draft according to the structure contract above → validate weighted length → post in a chain → record history. Once the books are all posted, it automatically rests.
DRY_RUN=1 bash ~/.claude/skills/wjs-publishing-books-to-x/daily.sh # 起草不发
FORCE_SLUG=<slug> bash .../daily.sh # 指定书
launchctl bootout gui/$(id -u)/com.jianshuo.wjs-publishing-books-to-x # 停掉
Log:~/Library/Logs/wjs-publishing-books-to-x/. To change the time: edit ~/Library/LaunchAgents/com.jianshuo.wjs-publishing-books-to-x.plist then bootout + bootstrap.
Anti-Patterns
| Don't | Reason |
|---|---|
| Post a book signed by someone else | It's a community user's book, not mine |
| Write the thread as a table of contents (chapter one covers X, chapter two covers Y…) | Dig into the content, don't just list dishes |
| Stuff a link into every post | Links only in the N-1 and N posts |
| Write the last post as a VoiceDrop ad copy | Talk aboutthis book's real writing process, the product is incidental |
jq -r '.data.id' Parse the tweet-posting return |
The returned text contains bare newlines, jq reports control characters; use grep |
| media upload without --category | It's treated as video by default, images won't attach |
File Layout
~/.claude/skills/wjs-publishing-books-to-x/
├── SKILL.md
├── daily.sh # launchd 每日入口(挑书→起草→校验→发→记录)
├── com.jianshuo.wjs-publishing-books-to-x.plist.template
├── scripts/
│ ├── pick-next-book.sh # 下一本没发过的我的书(新→旧)
│ └── fetch-book.py # 抓书素材(目录梗概+序章+前两章)→ 纯文本
└── state/
├── .gitignore # history.jsonl / today-* 不进公开 repo
├── history.jsonl # 每发一本记一行
└── today-*.{txt,json,jpg} # 当天工作文件
Dependencies
- xurl:
xurl whoamican return the username - voicedrop MCP: list_books / read_book / read_book_chapter (public content, no login required)
- jq