一、前言
手动发推文的过程其实挺繁琐的:打开浏览器 → 登录 → 写内容 → 调格式 → 发布。如果你需要定期发布 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
| taskkill //F //IM chrome.exe
chrome.exe --remote-debugging-port=9222 \ --user-data-dir="C:/Users/你的用户名/AppData/Local/Temp/chrome-debug-profile"
|
为什么用独立 --user-data-dir?
Chrome 不允许默认用户目录开启远程调试(安全限制)。必须指定一个不同的目录。首次启动后需要手动登录一次 X,后续会保持会话。
3.3 验证 CDP 连接
1 2
| curl http://localhost:9222/json/version
|
四、自动发布的核心代码
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 () => { const browser = await chromium.connectOverCDP('http://localhost:9222'); const page = browser.contexts()[0].pages()[0];
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);
const editor = page.locator('[data-testid="tweetTextarea_0"]').first(); await editor.click({ force: true, timeout: 5000 }); await page.waitForTimeout(300);
await page.keyboard.type(TWEET, { delay: 3 }); await page.waitForTimeout(2000);
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
|
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 "今天学到了什么新东西"
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
| await page.keyboard.press('Escape'); await page.waitForTimeout(500); await page.keyboard.press('Escape'); await page.waitForTimeout(500);
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.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
| curl -s http://127.0.0.1:8080/api/v1/tasks/{task_id}
|
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. ...';
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);
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 }); }
const fileInput = page.locator('input[type="file"]').first(); await fileInput.setInputFiles(VIDEO_PATH);
await page.waitForTimeout(120000);
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
| taskkill //F //IM chrome.exe chrome.exe --remote-debugging-port=9222 --user-data-dir="chrome-debug-profile"
"搜集最新 AI 资讯,发布一条推文"
|
Claude Code 会自动:
- 搜索最新资讯(WebSearch / DuckDuckGo)
- 整理成推文格式
- 通过 Playwright CDP 发布
7.2 批量发布
1 2 3 4 5 6 7 8 9 10 11
| const tweets = [ '第一条推文内容', '第二条推文内容', '第三条推文内容', ];
for (const tweet of tweets) { await publishTweet(tweet); await page.waitForTimeout(30000); }
|
八、总结
| 场景 |
推荐方案 |
| 偶尔发推 |
Claude Code 直接说”发一条推文” |
| AI 日报(文字) |
Claude 搜集 + CDP 自动发布 |
| AI 日报(视频) |
MoneyPrinterTurbo 生成 + CDP 上传 |
| 批量发布 |
批量脚本 + 间隔控制 |
| X Article 长文 |
x-article-publisher + Chrome 扩展 |
核心要点:
- Chrome CDP 是桥梁——复用已有登录状态
force: true 是关键——X 页面遮罩层太多
- 逐行输入比剪贴板更可靠
- 多种按钮选择器回退——X 前端经常变化
相关工具