xhs-controller 整体架构方案
xhs-controller 是一个运行在 root Android 设备上的控制器 App。它把小红书 App、Frida 注入脚本、本机 HTTP 服务和 ngrok 暴露能力打包到一个 APK 中,让后续采集任务不再依赖电脑端 Termux/Frida 启动链路。
核心目标不是做一个通用爬虫,而是复用已经登录的小红书 App 网络环境,提供低并发、可恢复、可被 AI/脚本调用的数据查询服务。
总体分层
当前实现位于 reverse/xhs-android/android-controller,主要组件如下:
用户/脚本/AI
|
| HTTP 8765 / ngrok / Tailscale
v
NativeRuntimeServer
|
| JSON task over localhost:18766
v
BridgeClient
|
| frida-inject
v
BridgeScript in XHS process
|
| reuse okhttp3.OkHttpClient
v
XHS internal APIs这套结构可以类比为一个 NestJS/Hono 服务,只是 HTTP handler 不直接访问外部 API,而是把任务投递给已经注入到小红书进程里的 bridge。bridge 相当于一个运行在目标 App 内部的轻量 RPC worker。
组件职责
MainActivity
MainActivity 是手机端操作面板,目标是让日常使用足够简单。
它只保留四个主要动作:
一键启动:安装内置二进制、启动小红书、启动本机 HTTP server、启动 ngrok、确保 Frida bridge 可用。测试搜索:调用本机/search?q=gptimage2做 canary。状态/日志:查看/health、进程状态、bridge/ngrok 日志。停止:停止 ngrok 和本机 server,不主动强杀小红书。
UI 层不直接理解小红书业务接口,主要负责串联生命周期和展示状态。
XhsRuntimeManager
XhsRuntimeManager 负责所有需要 root 权限和设备状态管理的部分:
- 从 APK assets 释放
frida-inject和ngrok到/data/local/tmp/xhs-controller-bin/。 - 通过
su设置可执行权限。 - 启动或拉起小红书前台 Activity。
- 等待小红书 PID 稳定,避免冷启动时 PID 切换导致注入失败。
- 写入 Frida bridge 脚本到 App 私有目录。
- 使用
frida-inject -p <pid> -s <script>注入 bridge。 - 启动 ngrok 固定域名,把
8765暴露出去。 - 提供日志、进程状态和 stop/reset 能力。
这里的设计重点是“PID 稳定性”。小红书刚启动时可能重启自身进程,所以不能简单 pidof -> inject。当前策略是等待一段稳定窗口,然后 bridge 启动后再做 PID guard。如果 PID 变化,就返回明确错误并触发 bounded recovery。
NativeRuntimeServer
NativeRuntimeServer 是 Android App 内置的 HTTP server,监听 0.0.0.0:8765。
它提供三类接口:
- 文档接口:
/openapi.json、/docs/openapi.json、/llm.txt、/docs/llm.txt,不需要 token。 - 运行时接口:
/health、/xhs/runtime/ensure、/xhs/bridge/status。 - 数据接口:
/search、用户信息、关注、粉丝、收藏、发布、专辑、专辑笔记、笔记详情、评论等。
鉴权逻辑很轻:如果 App 配置了 token,除文档端点外都需要 X-API-Key header 或 ?token= query。这个定位是个人设备控制面板,不是多租户 API 网关。
BridgeClient
BridgeClient 是 Android 侧到 Frida bridge 的本地 RPC 客户端。
它连接 127.0.0.1:18766,发送一行 JSON task,读取一行 JSON response。这个协议刻意保持简单,类似一个极简 JSON-RPC,但没有 method envelope,task 本身就是执行描述。
示例 task:
{
"mode": "list",
"kind": "user.followings",
"item_type": "user",
"pagination": "cursor",
"items_path": "data.users",
"url_template": "https://edith.xiaohongshu.com/api/sns/v1/user/followings?user_id=xxx&cursor=__CURSOR__&order=0",
"max_pages": 1,
"cursor": ""
}BridgeScript
BridgeScript 是注入到小红书进程里的 Frida JavaScript。
它的核心能力是:
- 在小红书进程里
Java.choose("okhttp3.OkHttpClient")找一个真实 OkHttp client。 - 用这个 client 直接请求小红书内部 API。
- 对 search、single、list 三种任务模式做统一处理。
- 对用户、笔记、专辑、评论做轻量 normalize。
- 在
127.0.0.1:18766启动一个 JavaServerSocket,接收 Android 侧 task。
关键点是复用 App 内已有的 OkHttp client,而不是在外部重新实现签名、cookie、设备指纹和登录态。这和后端里“复用上游 SDK 的授权 client”类似,只是这里 client 存在于 Android App 进程中。
启动链路
一键启动 的顺序如下:
loadToken
-> ensureBinaries
-> ensureXhsReady
-> startServer
-> startNgrok
-> ensureBridge
-> health关键约束:
ensureBinaries必须先执行,否则frida-inject和ngrok不存在。ensureXhsReady必须等 PID 稳定,否则 bridge 容易注入到即将退出的进程。NativeRuntimeServer可以先启动,但数据接口实际依赖 bridge。ensureBridge会检查已有 bridge 是否匹配当前 XHS PID,不匹配会重新注入。- ngrok 只负责外网入口,本机和 Tailscale 调用不依赖它。
API 设计
当前公开的主要数据接口:
GET /search?q=<keyword>&pages=1&page_size=20&sort=general&timeout=90
GET /xhs/user/info?user_id=<id>&timeout=90
GET /xhs/user/followings?user_id=<id>&pages=1&cursor=&timeout=90
GET /xhs/user/followers?user_id=<id>&pages=1&cursor=&timeout=90
GET /xhs/user/faved?user_id=<id>&pages=1&cursor=&num=20&timeout=90
GET /xhs/user/posted?user_id=<id>&pages=1&cursor=&num=20&timeout=90
GET /xhs/user/boards?user_id=<id>&pages=1&num=20&timeout=90
GET /xhs/board/notes?board_id=<id>&pages=1&cursor=&num=20&timeout=90
GET /xhs/note/detail?note_id=<id>&page=1&num=20&timeout=90
GET /xhs/note/comments?note_id=<id>&pages=1&cursor=&timeout=90推荐调用流程:
BASE="https://stridulatory-greyson-dhooly.ngrok-free.dev"
TOKEN="<token>"
curl "$BASE/llm.txt"
curl "$BASE/openapi.json"
curl -H "X-API-Key: $TOKEN" "$BASE/health"
curl -H "X-API-Key: $TOKEN" "$BASE/xhs/runtime/ensure?auto_restart=1"
curl -H "X-API-Key: $TOKEN" "$BASE/search?q=gptimage2&pages=1&timeout=90"/llm.txt 是给 AI agent 使用的轻量上下文,/openapi.json 是给 Apifox、Postman、Swagger 或代码生成工具使用的结构化描述。
错误与限流策略
HTTP 状态映射:
200:请求成功,响应体通常有ok=true。400:参数缺失,例如没有传q或user_id。401:token 校验失败。404:未知 path。429:命中访问频繁、status=429、status=461或code=300013。502:bridge 或小红书内部请求失败。500:server 内部异常。
限流命中时,server 会补充:
{
"ok": false,
"rate_limited": true,
"retry_after_seconds": 600
}调用方应该把并发保持在 1,遇到 429 或 rate_limited=true 至少等待 600 秒。不要在手机端做高并发压测,因为请求本质上复用真实 App 账号、设备和网络环境。
稳定性设计
当前方案针对几个常见故障做了处理:
- 小红书未启动:
ensureXhsReady会主动拉起。 - 冷启动 PID 变化:等待 PID 稳定窗口后再注入。
- bridge PID 不匹配:
ensureBridge校验当前 XHS PID 和 bridge 上报 PID。 - bridge 启动失败:最多做有限次数 recovery,避免无限重启。
- OkHttp 未找到:bridge 返回
no_okhttp_client,调用方可以重新 ensure 一次。 - 小红书安全限制:识别访问频繁类响应后返回
429,交给调用方冷却。
这套恢复策略类似后端 worker 的 health check + bounded retry。区别是这里的 worker 是一个真实手机 App 进程,不能像 Kubernetes pod 一样随意重启,否则容易触发风控。
部署与访问方式
支持三种访问路径:
- 手机本机:
http://127.0.0.1:8765 - 内网/VPN:
http://100.113.57.4:8765 - 公网:
https://stridulatory-greyson-dhooly.ngrok-free.dev
ngrok 由 App 内置二进制启动,配置写入 App 私有目录里的 ngrok.yml。当前固定域名适合个人使用;如果后续需要多设备并行,应该把 domain/token 做成设备级配置,避免多个设备抢同一个域名。
为什么不依赖 Termux
早期方案可以通过 Termux 安装 frida/ngrok/server,但维护成本高:
- 用户需要手动安装 Termux 和包。
- 二进制版本、权限、PATH 容易不一致。
- 手机独立运行时,多进程生命周期不容易统一管理。
现在 APK 自带二进制,App 通过 root 释放到固定目录并启动。这样更接近一个“设备上的边缘采集节点”,用户只需要安装 APK、登录小红书、点击一键启动。
当前限制
- 只打包了
arm64-v8a,非 arm64 设备不支持。 - 依赖 root 和
su,普通手机不能独立完成注入。 - 依赖小红书 App 已登录,不能替代登录流程。
- 推荐并发为
1,不适合批量高并发采集。 liked相关接口当前标记为 unsupported。- bridge 依赖运行时能找到
okhttp3.OkHttpClient,小红书版本升级后可能需要调整。
后续扩展建议
- 把接口 endpoint registry 从
NativeRuntimeServer中拆出来,减少 handler 里的 if/else。 - 为 task 协议定义一个稳定 schema,便于后续生成 OpenAPI 和 LLM 文档。
- 增加设备端任务队列,显式串行化所有 XHS 数据请求,避免外部并发直接打到 bridge。
- 增加
/xhs/runtime/check,只做轻量健康检查,不触发重启。 - 对用户关注、收藏、专辑导入任务增加完整 SOP:采集、合并、上传、验证、字段对比。
- 把 ngrok token/domain 从常量迁移到安全存储或用户配置,避免打包进 APK。