Communitygithub.com

Twitter 技能

通过 Chrome 计算机使用模式在 X (Twitter) 上发布文本和图片。

Twitter 技能 是什么?

该技能为 AI 代理提供了在 X (Twitter) 上发布和删除推文的混合执行模型。它优先使用官方 X API 进行快速的文本发布,若 API 调用失败(如达到免费层配额)或未配置凭据,则自动回退到 Chrome 浏览器模拟。技能支持在发布时附加最多 4 张图片,并包含发布确认机制,确保不会不经用户确认就发布。此外,该技能还支持通过浏览器自动化删除推文和导出推文,所有这些操作都基于包含详细 DOM 选择器和 CLI 桥接指令的全面脚本目录。

兼容平台✓Claude Code~Codex CLI~Cursor✓Antigravity
npx skills add hankunpeng/hkp-skills

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

Twitter 技能 是做什么的?

通过 Chrome 计算机使用模式在 X (Twitter) 上发布文本和图片。

脚本目录

重要:所有脚本都位于此技能的 scripts/ 子目录中。

代理执行说明:

  1. 确定此 SKILL.md 文件所在的目录路径为 {baseDir}
  2. 脚本路径:
    • 剪贴板辅助脚本:{baseDir}/scripts/copy-to-clipboard.ts
    • 推文导出/抓取脚本:{baseDir}/scripts/export-tweets.ts
    • 官方 X API 辅助脚本:{baseDir}/scripts/x-api.ts
  3. 解析 ${BUN_X} 运行时:如果已安装 bun → 使用 bun;如果 npx 可用 → 使用 npx -y bun;否则建议安装 bun
  4. 将本文档中的所有 {baseDir} 和 ${BUN_X} 替换为实际值

执行模式

此技能遵循混合执行模型:

  1. 优先 API(建议用于文本发布):尝试使用官方 X API 脚本(x-api.ts)发布推文。此方式快速、稳定,且不消耗浏览器资源。
  2. 浏览器回退:如果 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 辅助功能权限。

常规发布工作流(文本和图片)

执行发布时:

  1. 通过调用 get_app_state(或等效工具)启动代理轮次,获取 Google Chrome 的状态。
  2. 打开或导航 Google Chrome 到 https://x.com/compose/post。
  3. 找到推文编辑器的输入框。
  4. 使用计算机使用的键盘输入将推文文本输入到编辑器中。
  5. 如果要附加任何图片(最多 4 张): 对于每张图片: a. 运行剪贴板辅助脚本,将图片复制到剪贴板: bash ${BUN_X} {baseDir}/scripts/copy-to-clipboard.ts image /absolute/path/to/image.png b. 使用粘贴快捷键(macOS 为 super+v,Windows/Linux 为 control+v)将图片粘贴到编辑器中。 c. 等待 2-3 秒,直到 X 完成媒体上传。
  6. 发布安全:在获得当前对话中用户的明确最终确认之前,切勿点击 Publish、Post 或任何等效按钮来发布推文。
  7. 一旦用户确认,点击 Post 按钮进行发布。
  8. 发布后,关闭编辑器模态框,以使界面不会停留在撰写对话框。使用关闭按钮或 Escape 键:
    • DOM 选择器:[data-testid="app-bar-close"] 或 [aria-label="Close"]
    • 回退方案:触发一个 Escape 键按下事件
  9. 自动刷新信息流(可选):如果用户打开了其他标签页显示其个人资料(例如,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 编辑器进行交互:

  1. 多个编辑器检测: X 页面通常包含多个撰写文本区域(例如,首页信息流上的内联合辑器和活动的模态对话框)。始终查询所有编辑器:
var els = document.querySelectorAll('[data-testid="tweetTextarea_0"]');
var el = els.length > 1 ? els[els.length - 1] : els[0];

始终定位活动的模态编辑器(通常是列表中的最后一个元素)。

  1. 绑定选择与聚焦: 在插入文本之前,必须先点击元素以触发 Draft.js 的选择绑定,然后聚焦:
el.click();
el.focus();
  1. 保留编辑器结构:
  • 不要在空的编辑器上使用 el.innerHTML = '' 或 document.execCommand('delete')。擦除 DOM 节点会破坏 Draft.js 的内部包装结构(例如,public-DraftStyleDefault-block 跨度),这会使 React 组件崩溃并使发布按钮永久禁用。
  • 只需直接在空的聚焦编辑器中使用 document.execCommand('insertText', false, text)。
  1. 触发 React 状态更新: 文本插入后,触发一个冒泡的 input 事件以通知 React:
el.dispatchEvent(new Event('input', { bubbles: true }));
  1. 定位正确的发布按钮: 按钮的 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'

导出推文工作流

要从你的个人资料页面导出/保存全部或过滤后的推文:

  1. 运行导出脚本:
    ${BUN_X} {baseDir}/scripts/export-tweets.ts [startDate] [endDate]
    
    • 可选的日期过滤器:你可以传入 startDate(例如 2026-06-01)和 endDate(例如 2026-06-30)来按日期范围过滤输出。如果省略,将导出所有抓取到的推文。
    • 输出文件:推文将保存在 /Users/alex/twitter/twitter.yaml 中,其中推文 URL 作为键,推文文本内容作为值。

相关技能