wjs-tweeting-from-articles
This skill automates daily X/Twitter posts by extracting content from a user's most recently published WeChat articles. It identifies the newest unposted article, generates three tweet candidates from different angles (quotable quote, metaphor, or contrasting insight), lets the user select one, posts it directly via the X API, and records the action to a history file to prevent duplication. Use this when the user requests daily tweet generation tied to their article publishing workflow.
git clone --depth 1 https://github.com/jianshuo/claude-skills /tmp/wjs-tweeting-from-articles && cp -r /tmp/wjs-tweeting-from-articles/wjs-tweeting-from-articles ~/.claude/skills/wjs-tweeting-from-articlesSKILL.md
# wjs-tweeting-from-articles
每天一条 X tweet,灵感**直接从最近发的公众号文章**里抠。文章是源头,tweet 是更短的萃取。
## Core Principle
**文章已经过了一轮王建硕式的精炼,tweet 只是再短一格的版本。** 不要重新构思——从 article.md 里直接抠最 quotable 的一句 / 一段,按 X 的节奏切。
**一天最多一条,一篇文章只推一次。** 状态文件 `state/history.jsonl` 记录哪些 article 已经推过;不重复。(**例外:批量排期模式**——一次把多篇排进队列、按每 N 小时一条自动发,见下文。)
**真发,不暂存。** 用户确认后立刻 `xurl POST`。失败原文 dump 给用户重试。
## When This Skill Fires
- 用户说「今天的 tweet」/「发一条 X」/「从文章里发推」
- 用户跑 `/wjs-tweeting-from-articles`
- 用户设了 `/schedule daily /wjs-tweeting-from-articles` 之后每天自动调
## When NOT to use
- 用户要发的内容**和最近的公众号文章无关**——直接 `xurl POST /2/tweets` 即可
- 用户要发的是产品 / skill 推广——用 `/publish-skill` 或 `/wjs-promoting-skills`
## Workflow
### Step 1: 挑今天要推的文章
```bash
scripts/pick-next-article.sh
```
输出最新一篇**还没推过**的文章的 folder 路径(最近一周内,按日期倒序找第一个未推的)。已经全推完 → 退出 0、空输出,告诉用户「最近 7 天的文章都推过了,今天 rest day」。
如果用户想推**特定**那一篇:跳过此脚本,直接用 `<folder>`。
### Step 2: 读文章 + 草拟 3 条 tweet 候选
读 `<folder>/article.md`,按下面**三个角度**各起草一条:
| 角度 | 选材 | 例子 |
|------|------|------|
| **A · 金句** | 从文中挑一句最 quotable 的,可以加一句铺垫 | "笔头钝了,写不出锋——蘸点墨,在砚台上转两圈,重新有了尖。" |
| **B · 比喻** | 文章的核心比喻 + 一句把比喻落到读者头上 | "写 prompt 跟画画一样,是手感活儿。每天都得写点。" |
| **C · 反差** | 「不是 X,是 Y」式的认知翻转 | "不是变聪明,是手感来了。" |
**长度硬约束**:tweet ≤ **280 字符**(X 限制;中文每字算 2)——所以中文 tweet 实际 ≤ **140 字**。**留 buffer 到 120 字以内**比较稳。
**风格**:保留王建硕语气——平实、家常比喻、不写营销腔。**不要**加 hashtags、不要加 @、不要加 emoji(除非原文有)。**不要**加 mp.weixin 链接(默认);如果用户问要不要带链接,提示可以 reply 里附。
### Step 3: 让用户挑一条
用 `AskUserQuestion` 给出 A/B/C 三条候选 + 「四选其他」。用户选一条之后进入 Step 4。
### Step 4: 真发 X
```bash
TWEET_TEXT='<picked text>'
JSON=$(jq -nc --arg text "$TWEET_TEXT" '{text:$text}')
resp=$(xurl -X POST -d "$JSON" /2/tweets)
# Don't use `jq -r '.data.id'` here — X API returns raw newlines in the echoed
# `text` field, which strict jq rejects with "control characters must be escaped".
# Grep the id directly instead.
TWEET_ID=$(printf '%s' "$resp" | grep -oE '"id":"[0-9]+"' | head -1 | sed -E 's/.*"([0-9]+)".*/\1/')
[[ -n "$TWEET_ID" ]] || { echo "POST failed: $resp"; exit 1; }
echo "https://x.com/jianshuo/status/$TWEET_ID"
```
成功 → 拿到 tweet_id + URL,告诉用户。
### Step 5: 记录到 history
```bash
HIST="$HOME/.claude/skills/wjs-tweeting-from-articles/state/history.jsonl"
SLUG=$(basename "$FOLDER")
jq -nc --arg date "$(date +%F)" --arg slug "$SLUG" --arg angle "$ANGLE" \
--arg tweet_id "$TWEET_ID" --arg text "$TWEET_TEXT" \
'{date:$date,slug:$slug,angle:$angle,tweet_id:$tweet_id,text:$text,status:"posted"}' \
>> "$HIST"
```
`angle` ∈ `A` / `B` / `C` / `other`。
### Step 6: 收尾
告诉用户:
- tweet URL
- 哪篇文章(slug)
- 哪个 angle
- 今天的 history 行已经写入
## Inputs
```
/wjs-tweeting-from-articles # 自动挑最近一篇没推过的
/wjs-tweeting-from-articles <article-folder> # 显式指定
/wjs-tweeting-from-articles --dry-run # 草稿不发
```
## File Layout
```
~/.claude/skills/wjs-tweeting-from-articles/
├── SKILL.md
├── scripts/
│ ├── pick-next-article.sh # 找最近一篇未推的 article folder
│ └── post-next-from-queue.sh # 批量排期模式:每次发队列里下一条,自节流
└── state/
├── .gitignore # 屏蔽 history.jsonl 不被推到 public repo
├── history.jsonl # 每条 tweet 一行 JSON record
├── queue-<DATE>.tsv # 批量模式:待发队列 idx/slug/text
├── queue-cursor # 批量模式:下一条序号
└── last-post-epoch # 批量模式:上次发出的时间戳(节流用)
```
## 批量排期模式(多篇一次排,每 N 小时一条)
一次要发**很多篇**(典型来源:`wjs-mining-articles` 从一场长对谈挖出十几篇文章),用这个模式——一次连发会被 X 判刷屏,所以排成队列、按固定间隔自动发。
**为什么不用 `AskUserQuestion` 逐篇选 angle**:十几篇 × A/B/C 太多。每篇直接抠**一条**最 quotable 的(≤120 字、王建硕语气、无 hashtag/emoji/链接、带盘古之白),列全文给用户一次过目 + 定节奏,确认后排期。
**机制**(`scripts/post-next-from-queue.sh` + 每小时 cron,脚本自己节流):
```bash
# 1. 队列文件:state/queue-<DATE>.tsv,每行 idx<TAB>slug<TAB>text
# 2. cursor: state/queue-cursor(下一条序号);last-post-epoch(上次发的时间)
# 3. 立刻发第一条(FORCE 绕过节流,顺便验证 xurl 能发):
FORCE=1 bash scripts/post-next-from-queue.sh
# 4. 装 cron:每小时跑,脚本只在距上次 ≥ MIN_GAP(默认 4h)才真发:
( crontab -l 2>/dev/null | grep -v post-next-from-queue.sh; \
echo "5 * * * * /bin/bash $HOME/.claude/skills/wjs-tweeting-from-articles/scripts/post-next-from-queue.sh >/dev/null 2>&1" ) | crontab -
```
脚本要点:单机锁(防并发)、发失败不推进 cursor(下小时重试)、发成功才写 `history.jsonl` 并推进、**队列发完自动删掉自己的 cron 行**。改间隔用 `MIN_GAP`(秒)。
## Daily 自动化(可选)
要每天自动跑:
```bash
/schedule daily 09:00 /wjs-tweeting-from-articles
```
或写 cron。但**默认不**自动跑——每天人工确认 angle 选哪条更稳。
## Anti-Patterns
| 不要 | 原因 |
|------|------|
| 加 hashtags(#AI #prompt) | 王建硕的 X 风格不用 hashtag,加了变营销腔 |
| 同一篇文章推两条 | 一篇一推;如果文章特别长 / 多核心,下次跑时把它从 history 删一行重推 |
| tweet 里塞 mp.weixin 链接 | 默认不带;想带就放 reply 里 |
| 把 3 条候选都发出去 | 用户挑 1 条;不挑 = 跳过今天 |
| 凭空生造一条不在文章里的 tweet | 灵感**必须**从 article.md 抠;这条 skill 的价值就是「文章是源」 |
| LLM 自己改原文风格做"提炼" | 王建硕原文已经够紧;直接抠 > 重写 |
## Dependencies
- **xurl**:`xurl whoami` 能返回用户名(auth OK)
- **jq**:解析 xurl 返回的 JSON
- **存在 `~/code/wechat-publish/articles/YYYY-MM-DD-*/article.md`**:源文章Repo-wide drift detector for the wjs-* Claude Code skills in this marketplace. Sweeps every SKILL.md, scores it against the repo's own conventions (V-ing naming, trigger-phrase density, companion files, description shape), and returns a grouped punch list ordered by severity. Read-only — never edits files. Use before pushing a batch of skill changes, or whenever you wonder "are these skills still internally consistent?
|
Use when the user asks to audit what's wrong with a project, "make it right", "看看项目出了什么问题", "为什么用户的需求还没上线", "为什么没提交App Store", "为什么没新build", or wants a holistic state-of-the-project check covering unmerged branches, stalled PRs, failed GitHub Actions, stale builds, plan drift (TODOS.md / ROADMAP), unreleased commits, and log errors. Runs read-only investigation, presents a grouped checklist, fixes only after explicit user confirmation. Aware of the Cathier iOS app workflow (Xcode + fastlane + auto-merge @claude PRs from in-app feedback).
Use when the user has a video + an SRT and wants the subtitles either burned into the pixels (libass, always-visible) or soft-muxed as a togglable track. Also handles the final composite step for the localization pipeline — burn subs, mix a dub track, and keep the original audio as a low-volume bed, all in ONE ffmpeg encode (no cascade). Verifies libass availability and auto-downloads a static evermeet ffmpeg build when Homebrew's stripped binary lacks it. Triggers — "烧字幕", "硬字幕", "burn subtitles", "burn-in subs", "embed subtitle", "soft mux SRT", "把字幕烧进视频", "做最终合成".
Use when the user complains about spam on his X/Twitter posts — 同城面付 / 寻固炮 / 线下上门 / 免费破处 这类引流号在他推文下刷的 emoji 垃圾回复 — and wants them removed. Covers the last 7 days (X recent-search window). Triggers — "把这些spam删掉", "清理X垃圾回复", "推文下面好多引流号", "clean spam replies", "/wjs-cleaning-spam".
Use when the user wants a 王建硕-style WeChat article (article.md) turned into a narrated short MP4 video — TTS voiceover via 火山引擎 Volcano TTS, HyperFrames CSS/GSAP animation per scene, subtle SFX, abstract watercolor background, full pipeline rendering to 1080×1920 portrait MP4 (30-90s). Triggers — "把这篇文章做成视频", "做一个解说视频", "讲解视频", "/wjs-converting-text-to-video".
Use when migrating a WordPress site to a Hugo static site on GitHub Pages from a WXR export (.xml) plus the wp-content/uploads folder — preserving /archives/<id>/ URLs, localizing images, and deploying via GitHub Actions. Triggers — "把 WordPress 迁成 Hugo", "wordpress 转静态站", "migrate WordPress to Hugo", "WXR to Hugo", "publish WordPress to GitHub Pages", "/wjs-converting-wp-to-hugo".
Use when the user has a video + a target-language SRT and wants the video to actually speak that language — generates a time-aligned TTS voice dub. Routes by voice ID — Volcano (豆包) TTS for Chinese, edge-tts neural for any language. Defaults to one voice (single-speaker); opt-in multi-speaker via visual diarization. Outputs `*_<lang>_dub.mp4` with the dub audio in place of the original. Final mixing (audio bed + burn-in) is handed off to `/wjs-burning-subtitles`. Triggers — "配音", "中文配音", "Chinese dub", "voice over this", "dub the video", "TTS this SRT", "different voice for each speaker".