mkubicek/xTap
GitHub: mkubicek/xTap
一款浏览器扩展,在用户浏览 X/Twitter 时静默拦截 GraphQL 响应,将推文被动归档为按日期组织的 JSONL 文件。
Stars: 93 | Forks: 12
xTap
在您浏览 X/Twitter 时被动捕获推文
安装 ·
工作原理 ·
隐蔽性 ·
输出格式 ·
配置 ·
许可证
xTap 是一个浏览器扩展(支持 Chrome + Firefox),它会静默拦截 X/Twitter 已经发送给您浏览器的 GraphQL API 响应,并将您看到的每条推文保存为结构化的 JSONL。无需抓取,没有额外的请求——只是对已经在传输中的数据轻轻“敲击”一下。
## 功能
- **零足迹** —— 没有额外的网络请求;只捕获浏览器已经接收到的数据
- **结构化输出** —— 每条推文都保存为干净的 JSON 对象,包含作者、指标、媒体等信息
- **文章支持** —— 当 X 返回完整正文时,X 的长篇文章也会被捕获,包含内联图片引用和 Draft.js 区块结构
- **视频下载** —— 通过扩展弹窗使用 yt-dlp(或直接回退到 MP4)下载推文中的视频。需要 HTTP daemon。**注意:** 与被动捕获不同,视频下载会向 X 发出额外的网络请求,不具备隐蔽性。
- **图片下载** —— 弹窗中的可选开关(“自动下载图片”)会在您浏览时从 `pbs.twimg.com` 获取照片并保存到 `
/media//`。由 daemon 端处理;有速率限制;日志记录到 `media-manifest.jsonl`。**注意:** 同样不具备隐蔽性——会增加对 Twitter CDN 的请求。
- **暂停 / 恢复** —— 点击扩展图标可即时切换捕获状态
- **实时计数器** —— 扩展图标上的徽章显示本次会话捕获的推文数量
- **多标签页感知** —— 多个 X 标签页的数据汇聚到同一个 service worker,并进行共享去重
- **调试日志** —— 可选开关,用于将带有时间戳的 service worker 日志写入按日期轮转的文件中
- **调试仪表板** —— 内部扩展页面,显示实时捕获事件、传输健康状况,以及用于测试 GraphQL 响应解析的 parser 沙盒
- **跨平台** —— 支持 macOS、Linux 和 Windows
## 工作原理
```
X/Twitter GraphQL responses
│
▼
┌────────────────────────────┐
│ content-main.js │ MAIN world
│ patches fetch & XHR │
└──────────────┬─────────────┘
│ CustomEvent (random name)
▼
┌────────────────────────────┐
│ content-bridge.js │ ISOLATED world
│ relays to service worker │
└──────────────┬─────────────┘
│ chrome.runtime.sendMessage
▼
┌────────────────────────────┐
│ background.js │ Service worker
│ parse, dedup, batch │
└──────────┬─────────┬───────┘
│ │
HTTP │ │ native messaging
(all data) │ │ (token bootstrap only)
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ xtap_daemon │ │ xtap_host.py │
│ (HTTP) │ │ (stdio) │
└──────┬───────┘ └──────────────┘
│
▼
tweets-YYYY-MM-DD.jsonl
```
1. MAIN world 内容脚本修补了 `fetch` 和 `XMLHttpRequest.open()`,以便在 GraphQL 响应到达时进行观察
2. Payload 通过随机命名的 `CustomEvent` 中继到 ISOLATED world 桥接器,再由其转发给 service worker
3. service worker 对推文进行解析、标准化、去重和批处理
4. 批处理后的数据通过 **HTTP daemon**(`xtap_daemon.py`)写入磁盘。这是一个运行在 `127.0.0.1:17381` 的独立进程,由 launchd (macOS)、systemd (Linux) 或 Scheduled Task (Windows) 管理。在 macOS 上,它运行在浏览器 TCC 沙盒之外,可以写入受保护的路径,如 `~/Documents` 和 iCloud Drive
5. 启动时,扩展程序通过 **native messaging**(通过 Chrome/Firefox native messaging 的 `xtap_host.py`)获取 daemon 的 auth token。这是一次性的引导程序——之后所有数据都通过 HTTP 流转
## 使用安全吗?
X 正在[逐步推出针对自动化和机器人的更严格检测](https://x.com/nikitabier/status/2022496540275937525)。关键的一句话是:*“如果没有人点击屏幕,该账户及所有关联账户很可能会被封禁。”*
**xTap 不是机器人。** 它不会代您发帖、点赞、关注、滚动或进行任何 API 调用。它只是静默在后台,在*您*正常浏览时读取 X 已经发送到您浏览器的响应。从 X 服务器的角度来看,您的账户与任何其他用户完全一样——因为您*就是*一个普通用户。根本没有任何可供检测的额外流量。
自动化封禁的风险适用于那些*代替*您行动的工具(自动点赞、自动关注、自动滚动、无头浏览器)。xTap 完全没有这些行为。它就像开着 DevTools 并保存 Network 标签页里的内容一样——只是自动化成了结构化的 JSONL。
### 隐蔽措施
尽管被动拦截本身就是低风险的,但 xTap 仍尽量避免留下不必要的痕迹:
- **无额外网络请求** —— 仅读取浏览器已经接收到的响应;在网络日志中无迹可寻
- **看起来原生的 API 补丁** —— `fetch` 和 `XMLHttpRequest.prototype.open` 通过重写 `toString()` 返回 `[native code]` 进行了修补,可通过最常见的 runtime 完整性检查
- **无 expando properties** —— XHR URL 追踪使用 `WeakMap`,而不是将属性挂载到 XHR 实例上(后者极易被检测到)
- **随机事件通道** —— MAIN↔ISOLATED world 桥接器使用带有每次页面加载随机名称的 `CustomEvent`;用于传递该名称的 `` 信标在桥接器读取后会立即移除
- **零 DOM 足迹** —— 没有注入的 UI,没有页面修改;一切都存在于弹窗和 service worker 中
- **页面上下文中零控制台输出** —— 所有日志记录都发生在运行于页面 JavaScript 环境之外的 service worker 和 parser 中
- **最小权限** —— 仅包含 `storage` 和 `nativeMessaging`;除了 `x.com` / `twitter.com` / `127.0.0.1` 之外,没有任何 `webRequest` 或主机权限
- **带有抖动的刷新时机** —— 批数据以随机的时间间隔刷新,以避免出现如同时钟般极其规律的模式
这些措施并不能完全杜绝被检测——恶意的页面脚本依然可以比对 prototype 引用或探测被修补后的行为——但它们避免了指纹脚本通常会检查的常见信号。更重要的是,服务器端根本没有任何可供检测的东西,因为 xTap 本身不会产生任何网络活动。
## 安装
### 前置条件
| | 需求 |
|---|---|
| **浏览器** | Google Chrome 或 Mozilla Firefox (128+) |
| **运行时** | Python 3 |
| **操作系统** | macOS、Linux 或 Windows |
| [`yt-dlp`](https://github.com/yt-dlp/yt-dlp#installation) (可选) | 用于获取最高画质的视频下载 |
### 1. 加载扩展程序
**Chrome:**
1. 打开 `chrome://extensions`
2. 启用右上角的 **开发者模式**
3. 点击 **加载已解压的扩展程序** 并选择 `xtap/` 目录
4. 复制卡片上显示的扩展 ID(用于安装 native host)
**Firefox (128+):**
1. 创建一份扩展目录的 Firefox 副本(这样您 Chrome 的 manifest 可以保持不变)
2. 在该副本中,将 `manifest.json` 替换为 `manifest.firefox.json`(将其重命名为 `manifest.json`)
3. 打开 `about:debugging#/runtime/this-firefox`
4. 点击 **临时加载附加组件...**
5. 选择该 Firefox 副本的 `manifest.json`
Firefox 使用 `manifest.firefox.json` 中的固定扩展 ID:`xtap@mkubicek.dev`。
感谢 [Vincent Koc](https://github.com/vincentkoc) 对 Firefox 支持所做出的贡献。
### 2. 安装 native host
macOS
```
cd native-host
./install.sh chrome
```
对于 Firefox:
```
cd native-host
./install.sh firefox
```
这将安装 native messaging host(用于 auth token 引导)以及一个通过 launchd 运行的 HTTP daemon(`xtap_daemon.py`)。该 daemon 独立于浏览器进程树运行,并拥有自己的 TCC 权限,因此它可以写入受保护的路径,如 `~/Documents` 和 iCloud Drive。安装程序会捕获您当前的 `PATH`,以便 daemon 能够找到 `yt-dlp` 等工具。
扩展程序会通过 native host 的 auth token 自动检测该 daemon。如果 daemon 未运行,扩展程序将会显示红色的“!”徽章并在弹窗中提示错误。
Linux
```
cd native-host
./install.sh chrome
```
对于 Firefox:
```
cd native-host
./install.sh firefox
```
这将安装 native messaging host 以及一个作为 systemd 用户服务运行的 HTTP daemon(`xtap_daemon.py`)。该 daemon 可启用视频下载,并提供与 macOS 相同的 HTTP 传输方式。
Windows (PowerShell)
```
cd native-host
.\install.ps1 -ExtensionId -Browser chrome
```
对于 Firefox:
```
cd native-host
.\install.ps1 -Browser firefox
```
这将安装 native messaging host 以及一个作为 Windows Scheduled Task(在登录时启动)运行的 HTTP daemon(`xtap_daemon.py`)。该 daemon 可启用视频下载,并提供与 macOS/Linux 相同的 HTTP 传输方式。
### 3. 浏览 X
打开 [x.com](https://x.com) 并正常浏览。扩展图标上的徽章计数器会显示本次会话中已捕获的推文数量。点击图标可查看统计数据,以及暂停/恢复捕获。
### 从之前的版本升级
更新扩展文件后:
1. 重新运行安装程序(macOS/Linux 上运行 `install.sh`,Windows 上运行 `install.ps1`)——这会更新 daemon 配置并加载新的 Python 代码
2. 在您的浏览器扩展管理器中重新加载该扩展程序
3. 强制重新加载任何打开的 X 标签页(`Cmd+Shift+R` / `Ctrl+Shift+R`)
**对于 macOS/Linux 上 v0.20.0 之前的版本:** 重新运行 `install.sh` 是**必需的**——native messaging manifest 现在指向一个包装脚本(`~/.xtap/xtap_host_wrapper.sh`),该脚本使用绝对 Python 路径,修复了 macOS 上由于 Chrome 最小化环境找不到 `python3` 而导致 native host 启动失败的问题。
**对于 v0.19.0 之前的版本:** native messaging host(`xtap_host.py`)不再处理推文写入——现在所有数据都完全通过 HTTP daemon 流转。必须重新运行 `install.sh` 来更新 daemon 的服务配置(增加对 `XTAP_LOG_LEVEL` 的支持)。如果 daemon 未运行,扩展程序将显示红色的“!”徽章,而不会静默回退到 native messaging。
**对于 macOS 上 v0.13.0 之前的版本:** 重新运行 `install.sh` 是支持视频下载**所必需的**——daemon 需要更新后的 launchd 配置才能在您的 PATH 中找到 yt-dlp。
### 故障排除
如果扩展程序显示“未连接”或红色的“!”徽章:
1. **检查 daemon 是否正在运行:**
curl http://127.0.0.1:17381/status
# 应该返回: {"ok": true, "version": "..."}
2. **检查 daemon 日志:**
cat ~/.xtap/daemon-stderr.log
daemon 在每次启动时都会记录启动诊断信息(Python 版本、输出目录、token 状态)。常见问题:
- `FATAL: ~/.xtap/secret not found` —— 请先运行 `install.sh`
- `FATAL: Cannot bind to 127.0.0.1:17381` —— 已经有另一个实例在运行了
- 导入错误 —— 检查 Python 版本(`python3 --version`,需要 3.x)
3. **检查 native host 错误**(token 引导失败):
cat ~/.xtap/host-error.log
该文件是在 native messaging host 崩溃时创建的。它包含了 Python 版本、脚本路径和完整的 traceback。
4. **启用调试日志**,获取详细的请求级别 daemon 日志:
export XTAP_LOG_LEVEL=debug
cd native-host && ./install.sh chrome
然后检查 `~/.xtap/daemon-stderr.log` 以获取每个请求的详细信息(方法、路径、持续时间、推文计数)。
5. **验证 native messaging manifest** 是否指向正确的路径:
# Chrome (macOS):
cat ~/Library/Application\ Support/Google/Chrome/NativeMessagingHosts/com.xtap.host.json
# Firefox (macOS):
cat ~/Library/Application\ Support/Mozilla/NativeMessagingHosts/com.xtap.host.json
`path` 字段应指向 `~/.xtap/xtap_host_wrapper.sh` (macOS/Linux) 或 `xtap_host.bat` (Windows)。该包装器使用绝对 Python 路径,因此即使在 Chrome 最小化环境中 native messaging 也能正常工作。如果它仍然直接指向 `xtap_host.py`,请重新运行 `install.sh`。
## 配置
### 输出目录
更改推文保存位置的最简单方法是通过扩展弹窗——点击 xTap 图标,并在 **输出目录** 字段中输入您首选的路径。
或者,在启动浏览器之前设置 `XTAP_OUTPUT_DIR` 环境变量:
```
export XTAP_OUTPUT_DIR="$HOME/Documents/xtap-data"
```
| 设置 |默认值 | 描述 |
|---|---|---|
| 弹窗“输出目录” | *(空 —— 使用默认值)* | 按会话覆盖输出路径 |
| `XTAP_OUTPUT_DIR` 环境变量 | `~/Downloads/xtap` | 在未配置弹窗设置时的回退值 |
| 调试仪表板 | — | 可通过弹窗链接访问;显示实时捕获事件、传输健康状况、调试日志和发现模式开关,以及 parser 沙盒 |
## 输出格式
输出内容将写入按日期划分的文件(`tweets-YYYY-MM-DD.jsonl`)。每一行都是一个独立的 JSON 对象:
```
{
"id": "1234567890",
"url": "https://x.com/handle/status/1234567890",
"created_at": "2024-01-01T00:00:00.000Z",
"author": {
"id": "987654321",
"username": "handle",
"display_name": "Display Name",
"verified": false,
"is_blue_verified": true,
"follower_count": 1234
},
"text": "Full tweet text...",
"lang": "en",
"metrics": {
"likes": 10,
"retweets": 5,
"replies": 2,
"views": 1000,
"bookmarks": 1,
"quotes": 0
},
"media": [],
"urls": [],
"hashtags": [],
"mentions": [],
"in_reply_to": null,
"quoted_tweet_id": null,
"conversation_id": "1234567890",
"is_retweet": false,
"retweeted_tweet_id": null,
"is_subscriber_only": false, // true for subscriber-only tweets
"is_article": true, // present only for long-form articles
"article": { // present only for long-form articles
"title": "Article Title",
"text": "Rendered plain text with  refs",
"blocks": [], // raw Draft.js content_state blocks
"media": [{ // article image references
"id": "...",
"url": "https://pbs.twimg.com/...", // original CDN URL
"filename": "image.png",
"local_path": "media//image.png",
"width": 1200,
"height": 800
}]
},
"source_endpoint": "HomeTimeline", // which GraphQL endpoint
"captured_at": "2024-01-01T00:00:00.000Z"
}
```
对于常规推文,`is_article` 和 `article` 字段是不存在的。对于文章,`text` 包含了带有内联图片引用(指向 `media//`)的 markdown 风格文章渲染。没有 `content_state.blocks` 的文章存根将被跳过,而不会作为不完整的行被保存。
### 媒体文件约定
当“自动下载图片”开关打开时,daemon 会将照片写入:
```
/media//
```
顶层的普通图片 `media[]` 条目**不**包含 `local_path` 字段——该路径是通过约定推导出来的,以便使用者可以直接根据 `tweet.id` + URL 文件名重新构建它。文章媒体(`tweet.article.media[]`)确实包含 `local_path`,因为该路径也嵌入在渲染的文章 markdown(``)中,从而使文章正文成为一个自包含的文档。下载状态(成功 / 404 / 配额 / 超大 / 被阻止)会被追加到 `/media-manifest.jsonl`。
## 项目结构
```
xTap/
├── manifest.json # Chrome MV3 extension manifest
├── manifest.firefox.json # Firefox MV3 extension manifest (generated — do not edit)
├── background.js # Service worker — parsing, dedup, transport
├── content-main.js # MAIN world — patches fetch/XHR, emits events
├── content-bridge.js # ISOLATED world — relays events to service worker
├── popup.html/js/css # Extension popup UI
├── debug.html/js/css # Debug dashboard (live events, transport health, parser sandbox)
├── icons/ # Extension icons
├── lib/ # Shared utilities
└── native-host/
├── xtap_core.py # Shared file I/O logic
├── xtap_host.py # Native messaging host — token bootstrap only (Python, stdio)
├── xtap_daemon.py # HTTP daemon
├── com.xtap.daemon.plist # launchd plist template (macOS)
├── com.xtap.daemon.service # systemd unit template (Linux)
├── com.xtap.host.json # Native host manifest template (Chrome)
├── com.xtap.host.firefox.json # Native host manifest template (Firefox)
├── install.sh # Installer for macOS / Linux
├── install.ps1 # Installer for Windows
├── xtap_host.bat # Windows native host wrapper
└── xtap_daemon.bat # Windows daemon wrapper
```
## 开发
修改扩展文件(`background.js`、`lib/`、`content-*.js`、`popup.*`)后,请在浏览器(`chrome://extensions` 或 `about:debugging#/runtime/this-firefox`)中重新加载扩展程序,并强制重新加载任何已打开的 X 标签页。
**调试仪表板:** 点击弹窗中的“调试仪表板”以打开实时视图,查看捕获事件、传输健康状况,以及用于针对原始 GraphQL JSON 测试 `extractTweets` 的 parser 沙盒。调试日志记录和发现模式开关也在这里——启用调试日志可将带时间戳的 service worker 日志写入 `debug-YYYY-MM-DD.log`,或者启用发现模式以将 endpoint 响应结构记录到控制台。
**开发模式:** 当未打包加载(开发者模式)时,扩展程序优先使用 `chrome.storage.session` 作为 `seenIds` 去重缓存,如果会话存储 API 不可用,则回退到 `chrome.storage.local`。当会话存储可用时,重新加载扩展程序会自动清除缓存——在测试运行之间无需手动清除存储。
修改 Python host 文件(`xtap_core.py`、`xtap_host.py`、`xtap_daemon.py`)后,native host 会在下次浏览器重启时自动获取更改。要立即重启 HTTP daemon:
**macOS (launchd):**
```
launchctl kickstart -k gui/$(id -u)/com.xtap.daemon # restart
launchctl bootout gui/$(id -u)/com.xtap.daemon # stop
launchctl print gui/$(id -u)/com.xtap.daemon # status
tail -f ~/.xtap/daemon-stderr.log # logs
```
**Linux (systemd):**
```
systemctl --user restart com.xtap.daemon # restart
systemctl --user stop com.xtap.daemon # stop
systemctl --user status com.xtap.daemon # status
journalctl --user -u com.xtap.daemon -f # logs
```
**Windows (Scheduled Task, PowerShell):**
```
Stop-ScheduledTask -TaskName xTapDaemon; Start-ScheduledTask -TaskName xTapDaemon # restart
Stop-ScheduledTask -TaskName xTapDaemon # stop
Get-ScheduledTask -TaskName xTapDaemon # status
Get-Content ~\.xtap\daemon-stderr.log -Tail 50 -Wait # logs
```
## 测试
```
python3 -m pytest tests/ -v
node --test tests/*.test.mjs
```
每次向 `main` 分支推送代码时,CI 都会运行这些测试,并将覆盖率上传至 [Codecov](https://codecov.io/gh/mkubicek/xTap)。
Parser 测试 fixture 包位于 `tests/fixtures/` 目录下。原始捕获文件保留在本地
`tests/fixtures/private-raw/`(已被 gitignored),而提交的匿名化包
位于 `tests/fixtures/sanitized/`。匿名化方法和审查
清单记录在 `tests/fixtures/FIXTURES.md` 中。
## 许可证
[MIT](LICENSE) —— 随您喜欢怎么用就怎么用。标签:API拦截, Chrome插件, JSONL, Twitter, 命令控制, 数据可视化, 数据采集, 浏览器扩展, 自定义脚本