用 Claude Code 自动发推文:X (Twitter) 发布工作流完全指南(实战版)

一、前言

手动发推文的过程其实挺繁琐的:打开浏览器 → 登录 → 写内容 → 调格式 → 发布。如果你需要定期发布 AI 日报、技术分享等内容,这个重复劳动会消耗大量时间。

本文基于实际跑通的完整工作流,介绍如何用 Claude Code + Playwright + Chrome CDP 实现从素材收集 → 内容创作 → 自动发布的全流程自动化。所有代码和配置都经过验证,踩过的坑也一并记录。

二、工具链总览

1
2
3
4
5
6
7
8
9
10
11
用户说"发一条推文"
↓
Claude Code 搜索最新资讯 (WebSearch / DuckDuckGo)
↓
Claude Code 创作推文内容
↓
Playwright 通过 Chrome CDP 连接浏览器
↓
自动输入内容 + 点击发布
↓
推文出现在 @你的账号
工具 用途 安装方式
Playwright 浏览器自动化(连接 Chrome) npm install playwright
Chrome CDP 远程调试协议(控制浏览器) Chrome 启动参数
x-skills(可选) 素材收集、A/B 变体创作 git clone

三、核心原理:Chrome CDP

Chrome DevTools Protocol (CDP) 是整个方案的核心。它允许程序通过 WebSocket 连接到 Chrome 浏览器,像用户一样操作页面。

3.1 为什么用 CDP 而不是新浏览器?

方案 优点 缺点
Playwright 新浏览器 干净、可控 没有 X 登录状态,每次要重新登录
Chrome CDP 复用已有登录状态 需要关闭 Chrome 后用特殊参数重启

CDP 方案是最实用的——关闭 Chrome,用带调试端口的参数重启,就能在不丢失登录状态的情况下控制浏览器。

3.2 启动 Chrome 带 CDP

1
2
3
4
5
6
7
8
9
# 1. 关闭所有 Chrome 进程
taskkill //F //IM chrome.exe

# 2. 用独立配置文件启动(关键!)
chrome.exe --remote-debugging-port=9222 \
--user-data-dir="C:/Users/你的用户名/AppData/Local/Temp/chrome-debug-profile"

# 3. 首次需要在弹出的 Chrome 中手动登录 X
# 后续重启会保持登录状态

为什么用独立 --user-data-dir?

Chrome 不允许默认用户目录开启远程调试(安全限制)。必须指定一个不同的目录。首次启动后需要手动登录一次 X,后续会保持会话。

3.3 验证 CDP 连接

1
2
curl http://localhost:9222/json/version
# 返回 Browser 信息 = 连接成功

四、自动发布的核心代码

4.1 基本流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const { chromium } = require('playwright');

const TWEET = '你的推文内容';

(async () => {
// 1. 连接到已有 Chrome
const browser = await chromium.connectOverCDP('http://localhost:9222');
const page = browser.contexts()[0].pages()[0];

// 2. 导航到发推页面
await page.goto('https://x.com/compose/post', {
waitUntil: 'domcontentloaded',
timeout: 30000
});
await page.waitForTimeout(5000);

// 3. 关闭弹窗(X 经常弹出各种遮罩层)
await page.keyboard.press('Escape');
await page.waitForTimeout(500);

// 4. 点击编辑器(force: true 绕过遮挡)
const editor = page.locator('[data-testid="tweetTextarea_0"]').first();
await editor.click({ force: true, timeout: 5000 });
await page.waitForTimeout(300);

// 5. 输入内容
await page.keyboard.type(TWEET, { delay: 3 });
await page.waitForTimeout(2000);

// 6. 点击发布
await page.getByText('发帖').click({ force: true, timeout: 5000 });
console.log('POSTED!');
})().catch(e => console.error('Error:', e.message));

4.2 多行推文(逐行输入 + Enter)

1
2
3
4
5
6
7
8
9
10
11
12
const lines = [
'第一行内容',
'',
'第二行内容',
'- 要点一',
'- 要点二',
];

for (let i = 0; i < lines.length; i++) {
if (i > 0) await page.keyboard.press('Enter');
await page.keyboard.type(lines[i], { delay: 2 });
}

4.3 多种发布按钮回退

X 的发布按钮有多种选择器,需要逐个尝试:

1
2
3
4
5
6
7
8
9
10
11
const methods = [
async () => { await page.getByText('发帖').click({ force: true, timeout: 3000 }); },
async () => { await page.locator('[data-testid="tweetButton"]').first().click({ force: true, timeout: 3000 }); },
async () => { await page.locator('[data-testid="tweetButtonInline"]').first().click({ force: true, timeout: 3000 }); },
async () => { await page.getByRole('button', { name: '发帖' }).click({ force: true, timeout: 3000 }); }
];

for (const method of methods) {
try { await method(); console.log('POSTED!'); break; }
catch(e) { continue; }
}

五、完整的一键发布脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// publish-tweet.js
// 用法: node publish-tweet.js "你的推文内容"
const { chromium } = require('playwright');

const TWEET = process.argv[2] || 'Hello from Claude Code!';

(async () => {
console.log('🔗 Connecting to Chrome...');
const browser = await chromium.connectOverCDP('http://localhost:9222');
const page = browser.contexts()[0].pages()[0];

// 检查是否已登录
const url = page.url();
if (url.includes('/login')) {
console.log('❌ Not logged in! Please log in to X in the Chrome window.');
return;
}

console.log('🌐 Opening compose...');
await page.goto('https://x.com/compose/post', {
waitUntil: 'domcontentloaded',
timeout: 30000
});
await page.waitForTimeout(5000);

// 关闭弹窗
for (let i = 0; i < 3; i++) {
await page.keyboard.press('Escape');
await page.waitForTimeout(300);
}
await page.waitForTimeout(1000);

// 点击编辑器
console.log('📝 Typing tweet...');
const editor = page.locator('[data-testid="tweetTextarea_0"]').first();
await editor.click({ force: true, timeout: 5000 });
await page.waitForTimeout(300);

// 逐行输入
const lines = TWEET.split('\n');
for (let i = 0; i < lines.length; i++) {
if (i > 0) await page.keyboard.press('Enter');
await page.keyboard.type(lines[i], { delay: 2 });
}
await page.waitForTimeout(2000);

// 发布(多种方式回退)
const methods = [
async () => { await page.getByText('发帖').click({ force: true, timeout: 3000 }); },
async () => { await page.locator('[data-testid="tweetButton"]').first().click({ force: true, timeout: 3000 }); },
];

let posted = false;
for (const method of methods) {
try { await method(); posted = true; break; } catch(e) { continue; }
}

await page.waitForTimeout(3000);
console.log(posted ? '✅ POSTED!' : '❌ Post failed');
console.log('URL:', page.url());
})().catch(e => console.error('❌ Error:', e.message));

使用方式:

1
2
3
4
5
# 单行推文
node publish-tweet.js "今天学到了什么新东西"

# 多行推文(用 \n 分隔)
node publish-tweet.js "第一行\n\n第二行\n- 要点一\n- 要点二"

六、踩坑记录

6.1 弹窗遮挡(最常见)

现象:编辑器点击超时,提示 “element is intercepted by pointer events”

原因:X 页面有各种遮罩层(登录提示、功能引导、通知弹窗等)

解决:

1
2
3
4
5
6
7
8
// 发布前先按几次 Escape
await page.keyboard.press('Escape');
await page.waitForTimeout(500);
await page.keyboard.press('Escape');
await page.waitForTimeout(500);

// 编辑器点击用 force: true
await editor.click({ force: true });

6.2 发布按钮找不到

现象:[data-testid="tweetButton"] 匹配到 2 个元素

原因:X 页面有主编辑器和回复编辑器,两个都有 tweetTextarea_0

解决:用 getByTestId('primaryColumn').getByTestId('tweetTextarea_0') 限定范围,或用 .first() 取第一个

6.3 Chrome CDP 连接失败

现象:connect ECONNREFUSED ::1:9222

原因:Chrome 未启动或端口被占用

解决:

1
2
3
4
5
6
7
8
# 检查端口
netstat -ano | grep 9222

# 杀掉占用进程
taskkill //F //PID <进程ID>

# 重启 Chrome
chrome.exe --remote-debugging-port=9222 --user-data-dir="..."

6.4 登录状态丢失

现象:页面跳转到登录页

原因:使用了新的 user-data-dir 或 Cookie 过期

解决:在弹出的 Chrome 窗口中手动登录一次,后续会保持

6.5 PowerShell 剪贴板超时

现象:execSync powershell ETIMEDOUT

原因:Windows PowerShell 启动慢或被安全软件拦截

解决:直接用 page.keyboard.type() 逐行输入,不用剪贴板

七、视频推文发布

除了文字推文,视频推文的传播效果通常更好。以下是用 MoneyPrinterTurbo 生成 AI 资讯视频并自动发布到 X 的完整流程。

7.1 生成视频

通过 MoneyPrinterTurbo API 生成短视频:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
curl -s -X POST "http://127.0.0.1:8080/api/v1/videos" \
-H "Content-Type: application/json" \
-d '{
"video_subject": "AI最新资讯 Claude Fable 5 Gemini MiMo technology news",
"video_aspect": "9:16",
"video_concat_mode": "random",
"video_clip_duration": 3,
"video_count": 1,
"video_source": "pexels",
"voice_name": "zh-CN-XiaoxiaoNeural",
"voice_volume": 1.0,
"voice_rate": 1.0,
"bgm_type": "random",
"bgm_volume": 0.2,
"subtitle_enabled": true,
"font_name": "STHeitiMedium.ttc",
"paragraph_number": 1,
"video_language": "zh",
"font_size": 36,
"text_background_color": false,
"subtitle_position": "bottom"
}'

字幕参数要点(实战验证):

  • font_size: 36 — 9:16 竖屏刚好,60 会溢出
  • text_background_color: false — 去掉黑底,更清爽
  • subtitle_position: "bottom" — 底部显示

视频素材关键:video_subject 中必须混入 Pexels 通用搜索词(technology、coding、programming 等),否则搜不到素材会黑屏。

7.2 等待生成完成

1
2
3
4
5
# 轮询任务状态(state: 4=处理中, 1=完成, -1=失败)
curl -s http://127.0.0.1:8080/api/v1/tasks/{task_id}

# 视频输出位置
# storage/tasks/{task_id}/final-1.mp4

7.3 上传视频到 X

视频推文比文字推文复杂——需要通过 input[type="file"] 上传文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const VIDEO_PATH = 'E:/视频制作/MoneyPrinterTurbo/output/日报/final.mp4';
const TWEET_TEXT = '2026.6.16 AI 日报视频版\n\n本周最炸裂的 5 条资讯:\n1. ...\n2. ...';

// 1. 导航到发推页面
await page.goto('https://x.com/compose/post', { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(5000);
await page.keyboard.press('Escape');
await page.waitForTimeout(500);

// 2. 先输入文字
const editor = page.locator('[data-testid="tweetTextarea_0"]').first();
await editor.click({ force: true, timeout: 5000 });
const lines = TWEET_TEXT.split('\n');
for (let i = 0; i < lines.length; i++) {
if (i > 0) await page.keyboard.press('Enter');
await page.keyboard.type(lines[i], { delay: 2 });
}

// 3. 上传视频文件
const fileInput = page.locator('input[type="file"]').first();
await fileInput.setInputFiles(VIDEO_PATH);

// 4. 等待上传完成(大文件需要较长时间)
await page.waitForTimeout(120000); // 2 分钟

// 5. 点击发布
await page.getByText('发帖').click({ force: true, timeout: 8000 });

关键点:

  • 文件 input 的 accept 属性包含 video/mp4,video/quicktime
  • 视频上传时间取决于文件大小(12MB 约需 1-2 分钟)
  • 必须先输入文字再上传视频,否则文字可能丢失
  • 上传完成后需要等待 X 处理视频,不要急于点击发布

7.4 视频推文 vs 文字推文

对比 文字推文 视频推文
发布速度 即时 需等上传(1-2分钟)
传播效果 一般 视觉冲击力强
技术难度 低(键盘输入) 中(文件上传)
素材来源 无 Pexels 免费素材
适用场景 日报、观点、技巧 AI 资讯、产品展示、教程

八、完整工作流示例

7.1 AI 日报发布

1
2
3
4
5
6
7
# 1. 启动 Chrome(首次)
taskkill //F //IM chrome.exe
chrome.exe --remote-debugging-port=9222 --user-data-dir="chrome-debug-profile"
# 手动登录 X

# 2. Claude Code 中直接说:
"搜集最新 AI 资讯,发布一条推文"

Claude Code 会自动:

  1. 搜索最新资讯(WebSearch / DuckDuckGo)
  2. 整理成推文格式
  3. 通过 Playwright CDP 发布

7.2 批量发布

1
2
3
4
5
6
7
8
9
10
11
// batch-publish.js
const tweets = [
'第一条推文内容',
'第二条推文内容',
'第三条推文内容',
];

for (const tweet of tweets) {
await publishTweet(tweet);
await page.waitForTimeout(30000); // 间隔30秒避免频率限制
}

八、总结

场景 推荐方案
偶尔发推 Claude Code 直接说”发一条推文”
AI 日报(文字) Claude 搜集 + CDP 自动发布
AI 日报(视频) MoneyPrinterTurbo 生成 + CDP 上传
批量发布 批量脚本 + 间隔控制
X Article 长文 x-article-publisher + Chrome 扩展

核心要点:

  • Chrome CDP 是桥梁——复用已有登录状态
  • force: true 是关键——X 页面遮罩层太多
  • 逐行输入比剪贴板更可靠
  • 多种按钮选择器回退——X 前端经常变化

相关工具