fix: birdx → bird CLI (npm @steipete/bird)

birdx 从来不是 PyPI 包,pip install birdx 必然失败。
实际工具是 npm 包 @steipete/bird,一个 Twitter GraphQL CLI。

变更:
- 安装器改用 npm install -g @steipete/bird
- twitter.py 直接调 bird,通过环境变量传 AUTH_TOKEN/CT0
- 兼容已有的 birdx wrapper(shutil.which 回退)
- 更新所有文档引用
- 重写 setup-twitter.md 指南
This commit is contained in:
Panniantong
2026-02-25 04:02:42 +01:00
parent 17970b2789
commit 93ad9c5722
8 changed files with 104 additions and 75 deletions
+42 -19
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""Twitter/X — via birdx CLI (free) or Jina Reader fallback.
"""Twitter/X — via bird CLI (free) or Jina Reader fallback.
Backend: birdx (https://github.com/runesleo/birdx) for search/timeline
Backend: bird (@steipete/bird npm package) for search/timeline
Jina Reader for single tweets
Swap to: any Twitter access tool
"""
@@ -14,10 +14,29 @@ from typing import List
import requests
def _bird_cmd():
"""Find bird CLI binary."""
return shutil.which("bird") or shutil.which("birdx")
def _bird_env(config=None):
"""Build env dict with Twitter cookies for bird CLI."""
import os
env = os.environ.copy()
if config:
auth_token = config.get("twitter_auth_token")
ct0 = config.get("twitter_ct0")
if auth_token:
env["AUTH_TOKEN"] = auth_token
if ct0:
env["CT0"] = ct0
return env
class TwitterChannel(Channel):
name = "twitter"
description = "Twitter/X 推文"
backends = ["birdx", "Jina Reader"]
backends = ["bird", "Jina Reader"]
tier = 0 # Single tweet reading is zero-config
def can_handle(self, url: str) -> bool:
@@ -26,21 +45,23 @@ class TwitterChannel(Channel):
def check(self, config=None):
# Basic reading always works (Jina fallback)
if shutil.which("birdx"):
if _bird_cmd():
return "ok", "搜索、时间线、发推全部可用"
return "ok", "可读取推文。安装 birdx + 配置 Cookie 可解锁搜索和发推"
return "ok", "可读取推文。安装 bird + 配置 Cookie 可解锁搜索和发推"
async def read(self, url: str, config=None) -> ReadResult:
# Try birdx first
if shutil.which("birdx"):
return await self._read_birdx(url)
# Try bird first
bird = _bird_cmd()
if bird:
return await self._read_bird(url, bird, config)
# Fallback: Jina Reader
return await self._read_jina(url)
async def _read_birdx(self, url: str) -> ReadResult:
async def _read_bird(self, url: str, bird: str, config=None) -> ReadResult:
result = subprocess.run(
["birdx", "read", url],
[bird, "read", url],
capture_output=True, text=True, timeout=30,
env=_bird_env(config),
)
if result.returncode != 0:
return await self._read_jina(url)
@@ -84,7 +105,7 @@ class TwitterChannel(Channel):
"The tweet may have been deleted, or the account is private.\n\n"
"Tips:\n"
"- Make sure the URL is correct\n"
"- Try: birdx read <url> (if birdx is installed)\n"
"- Try: bird read <url> (if bird CLI is installed)\n"
"- For protected tweets, configure Twitter cookies: "
"agent-reach configure twitter-cookies AUTH_TOKEN CT0",
url=url,
@@ -105,7 +126,7 @@ class TwitterChannel(Channel):
"The tweet may have been deleted, or the account is private.\n\n"
"Tips:\n"
"- Make sure the URL is correct\n"
"- Try: birdx read <url> (if birdx is installed)\n"
"- Try: bird read <url> (if bird CLI is installed)\n"
"- For protected tweets, configure Twitter cookies: "
"agent-reach configure twitter-cookies AUTH_TOKEN CT0",
url=url,
@@ -115,27 +136,29 @@ class TwitterChannel(Channel):
async def search(self, query: str, config=None, **kwargs) -> List[SearchResult]:
limit = kwargs.get("limit", 10)
if shutil.which("birdx"):
return await self._search_birdx(query, limit)
bird = _bird_cmd()
if bird:
return await self._search_bird(query, limit, bird, config)
# Fallback to Exa
return await self._search_exa(query, limit, config)
async def _search_birdx(self, query: str, limit: int) -> List[SearchResult]:
async def _search_bird(self, query: str, limit: int, bird: str, config=None) -> List[SearchResult]:
try:
result = subprocess.run(
["birdx", "search", query, "-n", str(limit)],
[bird, "search", query, "-n", str(limit)],
capture_output=True, text=True, timeout=30,
env=_bird_env(config),
)
if result.returncode != 0:
return []
return self._parse_birdx_output(result.stdout)
return self._parse_bird_output(result.stdout)
except (subprocess.TimeoutExpired, FileNotFoundError):
return []
def _parse_birdx_output(self, text: str) -> List[SearchResult]:
"""Parse birdx text output into SearchResults."""
def _parse_bird_output(self, text: str) -> List[SearchResult]:
"""Parse bird text output into SearchResults."""
results = []
current = {}
text_lines = []
+28 -20
View File
@@ -305,23 +305,24 @@ def _install_system_deps():
except Exception:
print(" ⚠️ Node.js install failed. Try: apt install nodejs npm, or nvm install 22, or download from https://nodejs.org")
# ── birdx (for Twitter search) ──
if shutil.which("birdx"):
print(" ✅ birdx already installed")
# ── bird CLI (for Twitter search) ──
if shutil.which("bird") or shutil.which("birdx"):
print(" ✅ bird CLI already installed")
else:
if shutil.which("pip3") or shutil.which("pip"):
pip_cmd = "pip3" if shutil.which("pip3") else "pip"
if shutil.which("npm"):
try:
subprocess.run(
[pip_cmd, "install", "-q", "birdx"],
["npm", "install", "-g", "@steipete/bird"],
capture_output=True, text=True, timeout=120,
)
if shutil.which("birdx"):
print(" ✅ birdx installed (Twitter search + timeline)")
if shutil.which("bird"):
print(" ✅ bird CLI installed (Twitter search + timeline)")
else:
print(" ⬜ birdx install failed (optional — Twitter reading still works via Jina)")
print(" ⬜ bird CLI install failed (optional — Twitter reading still works via Jina)")
except Exception:
print(" ⬜ birdx install failed (optional — Twitter reading still works via Jina)")
print(" ⬜ bird CLI install failed (optional — Twitter reading still works via Jina)")
else:
print(" ⬜ bird CLI requires Node.js (optional — Twitter reading still works via Jina)")
def _install_mcporter():
@@ -435,6 +436,7 @@ def _detect_environment():
def _cmd_configure(args):
"""Set a config value and test it, or auto-extract from browser."""
import shutil
from agent_reach.config import Config
config = Config()
@@ -525,17 +527,23 @@ def _cmd_configure(args):
print("Testing Twitter access...", end=" ")
try:
import subprocess
result = subprocess.run(
["birdx", "search", "test", "-n", "1",
"--auth-token", auth_token, "--ct0", ct0],
capture_output=True, text=True, timeout=15,
)
if result.returncode == 0 and result.stdout.strip():
print("✅ Twitter Advanced works!")
bird = shutil.which("bird") or shutil.which("birdx")
if not bird:
print("⚠️ bird CLI not installed. Run: npm install -g @steipete/bird")
else:
print(f"⚠️ Test returned no results (cookies might be wrong)")
except FileNotFoundError:
print("⚠️ birdx not installed. Run: pip install birdx")
import os
env = os.environ.copy()
env["AUTH_TOKEN"] = auth_token
env["CT0"] = ct0
result = subprocess.run(
[bird, "search", "test", "-n", "1"],
capture_output=True, text=True, timeout=15,
env=env,
)
if result.returncode == 0 and result.stdout.strip():
print("✅ Twitter Advanced works!")
else:
print(f"⚠️ Test returned no results (cookies might be wrong)")
except Exception as e:
print(f"❌ Failed: {e}")
else:
+1 -1
View File
@@ -22,7 +22,7 @@ class Config:
FEATURE_REQUIREMENTS = {
"exa_search": ["exa_api_key"],
"reddit_proxy": ["reddit_proxy"],
"twitter_birdx": ["twitter_auth_token", "twitter_ct0"],
"twitter_bird": ["twitter_auth_token", "twitter_ct0"],
"groq_whisper": ["groq_api_key"],
"github_token": ["github_token"],
}
+1 -1
View File
@@ -78,7 +78,7 @@ class AgentReach:
return [r.to_dict() for r in results]
async def search_twitter(self, query: str, limit: int = 10) -> List[Dict[str, Any]]:
"""Search Twitter. Uses birdx if available, else Exa."""
"""Search Twitter. Uses bird CLI if available, else Exa."""
ch = get_channel("twitter")
results = await ch.search(query, config=self.config, limit=limit)
return [r.to_dict() for r in results]
+22 -24
View File
@@ -1,44 +1,36 @@
# Twitter 高级功能配置指南(birdx
# Twitter 高级功能配置指南(bird CLI
## 功能说明
基础 Twitter 功能(搜索+读单条推文)无需配置,开箱即用。
高级功能需要 birdx
高级功能需要 bird CLI
- 查看用户时间线
- 深度搜索(更精确、更多结果)
- 读取完整线程(thread
- 查看关注列表推文
birdx 是免费开源工具,但需要你的 Twitter 账号 cookie。
bird 是免费开源工具npm 包 @steipete/bird,但需要你的 Twitter 账号 cookie。
## Agent 可自动完成的步骤
1. 检查 birdx 是否安装:
1. 检查 bird 是否安装:
```bash
which birdx && echo "installed" || echo "not installed"
which bird && echo "installed" || echo "not installed"
```
2. 安装 birdx
2. 安装 bird
```bash
pip install birdx
npm install -g @steipete/bird
```
3. 检查是否已配置 cookie
3. 测试(需要先配置 cookie
```bash
birdx whoami 2>&1
AUTH_TOKEN="xxx" CT0="yyy" bird search "test" -n 1
```
4. 如果用户提供了 cookie,配置 birdx
4. 用 agent-reach 配置 cookie(推荐)
```bash
# birdx 的 cookie 配置文件位置
# 通常在 ~/.birdx/cookies.json 或通过环境变量
export TWITTER_AUTH_TOKEN="用户提供的auth_token"
export TWITTER_CT0="用户提供的ct0"
```
5. 测试:
```bash
birdx search "test" -n 1
agent-reach configure twitter-cookies "auth_token=xxx; ct0=yyy"
```
## 需要用户手动做的步骤
@@ -47,7 +39,13 @@ birdx search "test" -n 1
> Twitter 高级功能需要你的 Twitter 账号 cookie(完全免费)。
>
> 步骤:
> **最简单的方式:**
> 1. 安装 Chrome 插件 [Cookie-Editor](https://chromewebstore.google.com/detail/cookie-editor/hlkenndednhfkekhgcdicdfddnkalmdm)
> 2. 打开 https://x.com 并确保已登录
> 3. 点击 Cookie-Editor 插件图标 → Export → Header String
> 4. 把导出的内容发给我
>
> **手动方式:**
> 1. 用 Chrome 打开 https://x.com 并确保你已登录
> 2. 按 **F12** 打开开发者工具(Mac 按 Cmd+Option+I
> 3. 点击顶部的 **Application**(应用)标签
@@ -62,8 +60,8 @@ birdx search "test" -n 1
## Agent 收到 cookie 后的操作
1. 安装 birdx(如果没装):`pip install birdx`
2. 配置 cookie写入 birdx 配置
3. 测试:`birdx whoami` 确认身份
4. 反馈:"✅ Twitter 高级功能已开启!你的账号是 @xxx现在可以查看时间线、读取线程了。"
1. 安装 bird(如果没装):`npm install -g @steipete/bird`
2. 配置 cookie`agent-reach configure twitter-cookies "粘贴的内容"`
3. 测试:运行 `agent-reach doctor` 确认 Twitter 状态
4. 反馈:"✅ Twitter 高级功能已开启!现在可以搜索推文、查看时间线了。"
5. 如果失败:"❌ Cookie 无效或已过期,请重新导出。"