Skip to content

xhs-controller 整体架构方案

xhs-controller 是一个运行在 root Android 设备上的控制器 App。它把小红书 App、Frida 注入脚本、本机 HTTP 服务和 ngrok 暴露能力打包到一个 APK 中,让后续采集任务不再依赖电脑端 Termux/Frida 启动链路。

核心目标不是做一个通用爬虫,而是复用已经登录的小红书 App 网络环境,提供低并发、可恢复、可被 AI/脚本调用的数据查询服务。

总体分层

当前实现位于 reverse/xhs-android/android-controller,主要组件如下:

text
用户/脚本/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-injectngrok/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:

json
{
  "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 启动一个 Java ServerSocket,接收 Android 侧 task。

关键点是复用 App 内已有的 OkHttp client,而不是在外部重新实现签名、cookie、设备指纹和登录态。这和后端里“复用上游 SDK 的授权 client”类似,只是这里 client 存在于 Android App 进程中。

启动链路

一键启动 的顺序如下:

text
loadToken
  -> ensureBinaries
  -> ensureXhsReady
  -> startServer
  -> startNgrok
  -> ensureBridge
  -> health

关键约束:

  • ensureBinaries 必须先执行,否则 frida-injectngrok 不存在。
  • ensureXhsReady 必须等 PID 稳定,否则 bridge 容易注入到即将退出的进程。
  • NativeRuntimeServer 可以先启动,但数据接口实际依赖 bridge。
  • ensureBridge 会检查已有 bridge 是否匹配当前 XHS PID,不匹配会重新注入。
  • ngrok 只负责外网入口,本机和 Tailscale 调用不依赖它。

API 设计

当前公开的主要数据接口:

text
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

推荐调用流程:

bash
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:参数缺失,例如没有传 quser_id
  • 401:token 校验失败。
  • 404:未知 path。
  • 429:命中访问频繁、status=429status=461code=300013
  • 502:bridge 或小红书内部请求失败。
  • 500:server 内部异常。

限流命中时,server 会补充:

json
{
  "ok": false,
  "rate_limited": true,
  "retry_after_seconds": 600
}

调用方应该把并发保持在 1,遇到 429rate_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。

基于 MIT 许可证发布。