Twitter 技能 是做什么的?
通过 Chrome 计算机使用模式在 X (Twitter) 上发布文本和图片。
脚本目录
重要:所有脚本都位于此技能的 scripts/ 子目录中。
代理执行说明:
- 确定此 SKILL.md 文件所在的目录路径为
{baseDir} - 脚本路径:
- 剪贴板辅助脚本:
{baseDir}/scripts/copy-to-clipboard.ts - 推文导出/抓取脚本:
{baseDir}/scripts/export-tweets.ts - 官方 X API 辅助脚本:
{baseDir}/scripts/x-api.ts
- 剪贴板辅助脚本:
- 解析
${BUN_X}运行时:如果已安装bun→ 使用bun;如果npx可用 → 使用npx -y bun;否则建议安装 bun - 将本文档中的所有
{baseDir}和${BUN_X}替换为实际值
执行模式
此技能遵循混合执行模型:
- 优先 API(建议用于文本发布):尝试使用官方 X API 脚本(
x-api.ts)发布推文。此方式快速、稳定,且不消耗浏览器资源。 - 浏览器回退:如果 API 调用失败(例如,每月 1500 条推文的免费层配额已超出、达到速率限制)或
subscription.yaml中未配置 API 凭据,则自动回退到 Chrome 计算机使用模式 / CLI 桥接 来模拟浏览器操作。
前提条件
- 对于 API 模式:在你的本地
subscription.yaml文件(该文件已被 git 忽略)中配置你的 X API 凭据(OAuth 1.0a 用户上下文令牌):x_api: api_key: "YOUR_API_KEY" api_key_secret: "YOUR_API_KEY_SECRET" access_token: "YOUR_ACCESS_TOKEN" access_token_secret: "YOUR_ACCESS_TOKEN_SECRET" - 对于浏览器回退:已安装 Google Chrome,已在 Chrome 中登录 X (Twitter),并且已(在需要时)授予 macOS 辅助功能权限。
常规发布工作流(文本和图片)
执行发布时:
- 通过调用
get_app_state(或等效工具)启动代理轮次,获取Google Chrome的状态。 - 打开或导航 Google Chrome 到
https://x.com/compose/post。 - 找到推文编辑器的输入框。
- 使用计算机使用的键盘输入将推文文本输入到编辑器中。
- 如果要附加任何图片(最多 4 张):
对于每张图片:
a. 运行剪贴板辅助脚本,将图片复制到剪贴板:
bash ${BUN_X} {baseDir}/scripts/copy-to-clipboard.ts image /absolute/path/to/image.pngb. 使用粘贴快捷键(macOS 为super+v,Windows/Linux 为control+v)将图片粘贴到编辑器中。 c. 等待 2-3 秒,直到 X 完成媒体上传。 - 发布安全:在获得当前对话中用户的明确最终确认之前,切勿点击
Publish、Post或任何等效按钮来发布推文。 - 一旦用户确认,点击
Post按钮进行发布。 - 发布后,关闭编辑器模态框,以使界面不会停留在撰写对话框。使用关闭按钮或 Escape 键:
- DOM 选择器:
[data-testid="app-bar-close"]或[aria-label="Close"] - 回退方案:触发一个
Escape键按下事件
- DOM 选择器:
- 自动刷新信息流(可选):如果用户打开了其他标签页显示其个人资料(例如,
x.com/[username])或首页信息流(x.com/home),请刷新它们,以便新推文立即可见。
var closeBtn = document.querySelector('[data-testid="app-bar-close"], [aria-label="Close"]');
if (closeBtn) { closeBtn.click(); }
else { document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', code: 'Escape', keyCode: 27, which: 27, bubbles: true })); }
CLI 桥接(无计算机使用工具)
当环境缺少计算机使用的键盘/鼠标工具时,使用特定于平台的方法来打开 Chrome 并将 JavaScript 注入页面。
macOS (AppleScript)
打开撰写页面:
open -a "Google Chrome" "https://x.com/compose/post"
在 Chrome 中执行 JavaScript — 首先将 JS 写入临时文件(避免 shell 转义问题),然后通过 AppleScript 运行:
cat > /tmp/tweet.js << 'EOF'
(function() { /* 你的代码 */ })();
EOF
osascript -e '
tell application "Google Chrome"
activate
set js to read "/tmp/tweet.js"
set result to execute front window''''s active tab javascript js
return result
end tell'
模式:始终先将 JS 负载写入临时文件。不要尝试内联的 osascript -e 嵌入式 JS — 引号/转义冲突会导致解析错误。
发布后关闭编辑器(否则模态框会保持打开):
var closeBtn = document.querySelector('[data-testid="app-bar-close"], [aria-label="Close"]');
if (closeBtn) { closeBtn.click(); }
else { document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', code: 'Escape', keyCode: 27, which: 27, bubbles: true })); }
刷新个人资料/首页标签页以立即反映新发布内容(可选):
osascript -e '
tell application "Google Chrome"
tell window 1
set tabList to every tab whose URL contains "x.com/hankunpeng" or URL contains "x.com/home"
repeat with t in tabList
reload t
end repeat
end tell
end tell'
Linux
google-chrome "https://x.com/compose/post"
# 使用 chrome-remote-interface 或类似的 CDP 客户端来执行 JS
Windows
Start-Process "chrome" "https://x.com/compose/post"
# 通过 --remote-debugging-port 或 PowerShell 自动化使用 CDP
JavaScript / DOM 自动化指南(回退)
如果无法使用直接的计算机使用键盘/鼠标事件,或者你在浏览器脚本/CDP 模式下运行,请遵循以下指南与 X 的 React/Draft.js 编辑器进行交互:
- 多个编辑器检测: X 页面通常包含多个撰写文本区域(例如,首页信息流上的内联合辑器和活动的模态对话框)。始终查询所有编辑器:
var els = document.querySelectorAll('[data-testid="tweetTextarea_0"]');
var el = els.length > 1 ? els[els.length - 1] : els[0];
始终定位活动的模态编辑器(通常是列表中的最后一个元素)。
- 绑定选择与聚焦: 在插入文本之前,必须先点击元素以触发 Draft.js 的选择绑定,然后聚焦:
el.click();
el.focus();
- 保留编辑器结构:
- 不要在空的编辑器上使用
el.innerHTML = ''或document.execCommand('delete')。擦除 DOM 节点会破坏 Draft.js 的内部包装结构(例如,public-DraftStyleDefault-block跨度),这会使 React 组件崩溃并使发布按钮永久禁用。 - 只需直接在空的聚焦编辑器中使用
document.execCommand('insertText', false, text)。
- 触发 React 状态更新:
文本插入后,触发一个冒泡的
input事件以通知 React:
el.dispatchEvent(new Event('input', { bubbles: true }));
- 定位正确的发布按钮:
按钮的 testid(
tweetButtonInline和tweetButton)可能会根据上下文互换。始终扫描可见且已启用的按钮:
var btns = document.querySelectorAll('[data-testid="tweetButtonInline"], [data-testid="tweetButton"]');
var activeBtn = Array.from(btns).find(function(btn) {
var isVisible = btn.offsetWidth > 0 && btn.offsetHeight > 0;
var isDisabled = btn.disabled || btn.getAttribute('aria-disabled') === 'true';
return isVisible && !isDisabled;
});
if (activeBtn) activeBtn.click();
```
## 删除推文工作流
执行删除时:
1. 打开或导航 Google Chrome 到用户的个人资料页面(`https://x.com/[username]`)或推文的直接 URL(`https://x.com/[username]/status/[tweetId]`)。
2. 搜索包含要删除文本的目标推文 `<article>` 容器。
3. 点击推文上的选项菜单按钮:
- **DOM 选择器**:`[data-testid="caret"]`
4. 等待 1-2 秒,然后点击“删除”菜单项:
- **DOM 选择器**:一个 `[role="menuitem"]` 元素,其文本包含 "Delete" 或 "删除"。
5. 等待 1-2 秒,然后在对话框表单中点击确认删除按钮:
- **DOM 选择器**:`[data-testid="confirmationSheetConfirm"]`(或者回退到任何文本包含 "Delete" 或 "删除" 的对话框按钮)。
### CLI 桥接示例 (macOS)
使用与发布相同的临时文件 + AppleScript 模式。将 `TWEET_TEXT_HERE` 替换为目标推文内容。
**步骤 1 — 查找推文并点击菜单按钮:**
```bash
cat > /tmp/del-1.js << 'EOF'
(function() {
var articles = document.querySelectorAll('article');
var target = null;
var needle = 'TWEET_TEXT_HERE';
for (var i = 0; i < articles.length; i++) {
var textEl = articles[i].querySelector('[data-testid="tweetText"]');
if (textEl && textEl.textContent.trim().indexOf(needle) !== -1) {
target = articles[i];
break;
}
}
if (!target) return 'ERROR: tweet not found';
var caret = target.querySelector('[data-testid="caret"]');
if (!caret) return 'ERROR: caret not found';
caret.click();
return 'OK: caret clicked';
})();
EOF
osascript -e '
tell application "Google Chrome"
set js to read "/tmp/del-1.js"
set result to execute front window''''s active tab javascript js
return result
end tell'
步骤 2 — 点击删除菜单项(步骤 1 后等待 1-2 秒):
cat > /tmp/del-2.js << 'EOF'
(function() {
var items = document.querySelectorAll('[role="menuitem"]');
for (var i = 0; i < items.length; i++) {
var txt = items[i].textContent.trim();
if (txt === 'Delete' || txt === '删除') {
// 点击菜单项本身,或者如果它包含一个嵌套的可点击元素
var clickTarget = items[i].querySelector('[role="menuitem"]') || items[i];
clickTarget.click();
return 'OK: delete clicked (index ' + i + ')';
}
}
// 回退:点击第一个菜单项(下拉菜单中总是 "Delete")
var first = document.querySelectorAll('[role="menuitem"]')[0];
if (first) { first.click(); return 'OK: first menuitem clicked'; }
return 'ERROR: no menuitems';
})();
EOF
osascript -e '
tell application "Google Chrome"
set js to read "/tmp/del-2.js"
set result to execute front window''''s active tab javascript js
return result
end tell'
步骤 3 — 确认删除(步骤 2 后等待 1-2 秒):
cat > /tmp/del-3.js << 'EOF'
(function() {
var confirmBtn = document.querySelector('[data-testid="confirmationSheetConfirm"]');
if (!confirmBtn) {
var buttons = document.querySelectorAll('[role="button"], button');
for (var i = 0; i < buttons.length; i++) {
var txt = buttons[i].textContent.trim();
if (txt === 'Delete' || txt === '删除') {
confirmBtn = buttons[i];
break;
}
}
}
if (!confirmBtn) return 'ERROR: confirm button not found';
confirmBtn.click();
return 'OK: confirm clicked';
})();
EOF
osascript -e '
tell application "Google Chrome"
set js to read "/tmp/del-3.js"
set result to execute front window''''s active tab javascript js
return result
end tell'
导出推文工作流
要从你的个人资料页面导出/保存全部或过滤后的推文:
- 运行导出脚本:
${BUN_X} {baseDir}/scripts/export-tweets.ts [startDate] [endDate]- 可选的日期过滤器:你可以传入
startDate(例如2026-06-01)和endDate(例如2026-06-30)来按日期范围过滤输出。如果省略,将导出所有抓取到的推文。 - 输出文件:推文将保存在
/Users/alex/twitter/twitter.yaml中,其中推文 URL 作为键,推文文本内容作为值。
- 可选的日期过滤器:你可以传入