极简、零依赖的 Nitter 兼容层:直接用 X 自己的 GraphQL 接口,对外提供 /{user}/rss 与 /{user} HTML,给 RSS 阅读器和 astrbot_plugin_nitter_tweets 这类还需要 Nitter 的下游用。
2026 年 9 月,X 向 Nitter 发出 cease-and-desist,上游停更,公共实例要么 451、要么返回空 feed。xnitter 不是 Nitter 的 fork,而是按下游的解析判据反推出的最小兼容实现:只实现真正被调用的少数端点,数据不再从 Nitter 的抓取管道来,而是直连 X 的 Web GraphQL。
- 零依赖:只用 Python 3.9+ 标准库,无 pip 包、无 venv,一个进程跑完。
- 无需凭据:X 的 GraphQL 只校验 cookie 的「格式」,用一个公开的 Web Bearer + 格式合法的假 cookie 即可,因此不存在登录态过期的问题。
- 轻:常驻内存约 15 MiB,单核 VPS 上无压力。
- 输出严格对齐 Nitter 契约:
timeline-item、tweet-content、Min-Id游标、RFC 822pubDate、无 DOCTYPE、新→旧排序。
| 路径 | 用途 |
|---|---|
/{user}/rss |
用户时间线 RSS,新→旧,默认 20 条 |
/{user}/media/rss |
只含带媒体推文的 RSS(走 UserMedia) |
/{user1,user2}/rss |
多用户合并(逗号连接) |
/i/lists/{id}/rss |
列表 RSS |
/{user} |
时间线 HTML,?cursor= 翻页 |
/i/lists/{id} |
列表 HTML |
/search?f=tweets&q=... |
搜索(f=top 为热度序) |
/hashtag/{tag} |
标签兜底 |
/{user}/status/{id} |
单条推文页 |
/pic/media/{file} |
媒体代理(转发 pbs.twimg.com,支持视频封面子路径) |
/css/style.css |
页面样式 |
同一个用户 + 同一种输出默认缓存 60 秒,命中时响应 1~2 ms。
git clone https://github.com/EvilNeko/xnitter.git
cd xnitter
sudo bash deploy/install.sh # 默认装到 /opt/xnitter,端口 8090,仅监听 127.0.0.1装完先自测(会真连 X,能验证取数链路和输出契约):
sudo -u xnitter python3 /opt/xnitter/xn_test.py然后用 nginx 把域名反代到 127.0.0.1:8090:
server {
listen 443 ssl;
server_name nitter.example.com;
location / {
proxy_pass http://127.0.0.1:8090;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
}全部走环境变量,systemd 读 /etc/xnitter.env(见 deploy/xnitter.env.example)。
| 变量 | 默认 | 说明 |
|---|---|---|
XNITTER_HOST |
127.0.0.1 |
监听地址 |
XNITTER_PORT |
8090 |
监听端口 |
XNITTER_BASE_URL |
http://127.0.0.1:8090 |
对外域名,RSS 里的绝对链接靠它 |
XNITTER_NAME |
xnitter |
实例名,出现在 RSS title / 首页 |
XNITTER_TIMEOUT |
15 |
单次请求 X 的超时(秒) |
XNITTER_RETRIES |
2 |
失败重试次数 |
XNITTER_CACHE_TTL |
60 |
响应缓存秒数 |
XNITTER_PAGE_SIZE |
20 |
单页条数 |
XNITTER_RSS_MAX_ITEMS |
25 |
RSS 单次输出上限 |
XNITTER_BEARER / XNITTER_COOKIE / XNITTER_CSRF |
内置假凭据 | 一般不用改;X 更换 Bearer 时再覆盖 |
XNITTER_LOG_LEVEL |
INFO |
日志级别 |
插件 astrbot_plugin_nitter_tweets 的 WebUI 里把实例指向本服务,并把抓取后端切成 nitter(mix 会优先走 FxTwitter,日志里会一直显示 生效实例: FxTwitter):
{
"basic": {
"instances": ["https://nitter.example.com"],
"fetch_backend": "nitter"
}
}配置文件的编码是 UTF-8 BOM,读写记得用 utf-8-sig。
screen_name → 数字 uid:先用api.fxtwitter.com/{name},失败再走UserByScreenName。- 时间线:
POST SearchTimeline,变量{"rawQuery": "from:<user>", "product": "Latest"},从TimelineTimelineCursor拿翻页游标。 - 媒体时间线:
POST UserMedia(这个接口的返回是严格时间序,且带媒体)。 - 所有 GraphQL 请求都必须用 POST——GET 一律返回
{"errors":[{"message":"Could not authenticate you","code":32}]}。 queryId会随 X 发版漂移。权威表在 fa0311/twitter-openapi 的src/config/placeholder.json,失效时替换xn_api.py顶部的OPERATIONS即可。
- 时间线接口不返回 media,普通 RSS 里的推文没有图;要图/视频封面只能用
/{user}/media/rss。 - 媒体代理只转发封面,不代理视频本体。
- 翻页游标由 X 决定,偶尔缺失,那就只有第一页。
- 上游
queryId或风控变化时会失效,需要更新OPERATIONS或降低频率。 - Cloudflare 会缓存 RSS(边缘命中时不回源),最长滞后取决于你的缓存策略。
本项目仅用于个人自托管、低频读取公开内容。请自行确认你的使用方式符合 X 的服务条款与当地法律;作者不对任何账号风控、封禁或数据问题负责。