1.77.6: SERP sorry-重定向按验证码语义(不换号+15min冷却);浏览池保温释放跳过+60min持有上限;速率-门壳实验与监控工具

This commit is contained in:
BigCode
2026-10-29 15:54:37 +08:00
commit 05d9462414
25 changed files with 3973 additions and 0 deletions
+929
View File
@@ -0,0 +1,929 @@
# SerpMax API 文档
> 版本:2026-09-24(依据源码 `admin/src/server.ts` 与 `node/src/api/` 整理)
> 架构:**admin**(后台,默认端口 `5310`)与 **node**(Profile Manager 节点,默认端口 `3100`,容器宿主映射常见为 `3106`)。
> admin 的浏览器操作类接口大多是**中继**:admin 收到请求后按 `profile.node → nodes.base_url` 转发到对应节点,并自动附带 admin API Token 与节点密钥。
---
## 目录
- [1. 通用约定](#1-通用约定)
- [1.1 统一响应信封](#11-统一响应信封)
- [1.2 认证](#12-认证)
- [1.3 公共请求头](#13-公共请求头)
- [1.4 错误码表](#14-错误码表)
- [2. Admin API(端口 5310)](#2-admin-api-端口-5310)
- [2.1 认证 /api/auth/*](#21-认证-apiauth)
- [2.2 仪表盘 /api/dashboard](#22-仪表盘-apidashboard)
- [2.3 Profile 管理 /api/profiles](#23-profile-管理-apiprofiles)
- [2.4 浏览器操作中继(浏览器/VNC/打码)](#24-浏览器操作中继)
- [2.5 Cookie 管理(admin 本地 Jar)](#25-cookie-管理admin-本地-jar)
- [2.6 代理管理 /api/proxies](#26-代理管理-apiproxies)
- [2.7 节点管理 /api/nodes](#27-节点管理-apinodes)
- [2.8 节点代码包与自更新](#28-节点代码包与自更新)
- [2.9 日志 /api/logs](#29-日志-apilogs)
- [2.10 系统设置 /api/settings](#210-系统设置-apisettings)
- [2.11 事件上报 /api/events](#211-事件上报-apievents)
- [3. Node API(端口 3100)](#3-node-api-端口-3100)
- [3.1 Profile](#31-profile)
- [3.2 Lease(租约)](#32-lease租约)
- [3.3 浏览器生命周期](#33-浏览器生命周期)
- [3.4 浏览器操作(Action + SSE)](#34-浏览器操作action--sse)
- [3.5 HTTP 请求执行(fetch / browse / browser-fetch)](#35-http-请求执行)
- [3.6 Cookie(node 运行时)](#36-cookienode-运行时)
- [3.7 缓存管理](#37-缓存管理)
- [3.8 Schema 与 Execution](#38-schema-与-execution)
- [3.9 reCAPTCHA 打码](#39-recaptcha-打码)
- [3.10 VNC 与直连](#310-vnc-与直连)
- [3.11 系统 /health /ready /metrics 等](#311-系统)
- [4. 附录](#4-附录)
- [4.1 Browser Action 类型一览](#41-browser-action-类型一览)
- [4.2 SSE 事件格式](#42-sse-事件格式)
- [4.3 典型调用流程](#43-典型调用流程)
---
# 1. 通用约定
## 1.1 统一响应信封
所有 JSON 接口(除 `/metrics`、图片、SSE、脚本下载外)使用统一信封:
成功:
```json
{ "success": true, "data": { ... } }
```
失败:
```json
{
"success": false,
"error": {
"code": "PROFILE_NOT_FOUND",
"message": "Profile 不存在",
"requestId": "req_xxxxxxxxxxxx",
"details": {}
}
}
```
- 每个响应都带 `x-request-id` 响应头;请求可自带 `x-request-id` 头,否则自动生成。
- admin 的部分错误信封为 `{ success:false, error:{ code, message } }`(无 requestId 字段),语义一致。
## 1.2 认证
### Admin API
| 方式 | 说明 |
|---|---|
| 会话 Cookie | `POST /api/auth/login` 后下发 `smax_session`(HttpOnly,默认 7 天) |
| API Token | `Authorization: Bearer smax_xxx...`(在 设置页 或 `GET /api/settings` 获取,可轮换) |
免会话的例外端点:
| 端点 | 凭证 |
|---|---|
| `POST /api/nodes/register` | 无需鉴权(`key` 即身份,须与 provision 下发的密钥一致) |
| `POST /api/nodes/heartbeat` | 会话/API Token,**或**该节点自身的通信密钥(`x-node-key` / `Bearer <NODE_KEY>`) |
| `GET /node/install`、`GET /node/package` | query 中的节点密钥 `?key=` |
| `GET /node/version` | 公开 |
### Node API
- 配置了环境变量 `NODE_KEY` 后,所有路由要求携带:
- `x-node-key: <NODE_KEY>`,或
- `Authorization: Bearer <NODE_KEY>`
- 未配置 `NODE_KEY` 时全部放行(本机/内网模式)。
- 例外(在节点密钥之外**额外**支持一次性直连票据,供前端跨源直连):
- `POST /profiles/:id/browser/actions`、`GET /profiles/:id/browser/events`(SSE)、`GET /profiles/:id/vnc/ws`(WebSocket)
- 票据来源:`POST /profiles/:id/direct-ticket`(仅持节点密钥者可调)→ `{ ticket, expiresInMs }`,绑定 Profile、5 分钟有效;actions 用后即焚,events/VNC 在 TTL 内可复用。携带方式:`?ticket=` 或 `x-direct-ticket` 头。
## 1.3 公共请求头
| 头 | 说明 |
|---|---|
| `x-request-id` | 可选。请求追踪 ID,缺省自动生成,原样回传于响应头与错误信封 |
| `idempotency-key` | 可选。`POST /profiles`(node)创建 Profile 幂等键 |
| `x-lease-id` | 可选。node 浏览器操作 / schema run 时声明 Lease 持有者(也可在 body 传 `leaseId`) |
跨源直连:node 对所有接口返回 CORS 头(`access-control-allow-origin: *`),`OPTIONS` 预检统一应答 204。
## 1.4 错误码表
node 侧规范错误码(`node/src/types/common.ts`,括号内为默认 HTTP 状态):
| 类别 | 错误码 |
|---|---|
| Profile | `PROFILE_NOT_FOUND`(404)、`PROFILE_BUSY`(409)、`PROFILE_INVALID`(400)、`PROFILE_CREATING`(409)、`PROFILE_DELETING`(409)、`PROFILE_DISK_LIMIT`(413) |
| Lease | `LEASE_NOT_FOUND`(404)、`LEASE_EXPIRED`(410)、`LEASE_CONFLICT`(409) |
| Browser | `BROWSER_NOT_RUNNING`(409)、`BROWSER_START_FAILED`(500)、`BROWSER_CRASHED`(500)、`BROWSER_TIMEOUT`(504)、`PROFILE_NOT_RUNNING`(409)、`BROWSER_SESSION_NOT_FOUND`(409) |
| Action | `INVALID_ACTION`(400)、`INVALID_ACTION_PARAMETER`(400)、`INVALID_SELECTOR`(400)、`ELEMENT_NOT_FOUND`(404)、`ELEMENT_NOT_VISIBLE`(400)、`ELEMENT_NOT_INTERACTABLE`(400)、`NAVIGATION_FAILED`(502)、`ACTION_TIMEOUT`(504)、`SCREENSHOT_FAILED`(500) |
| Schema | `SCHEMA_NOT_FOUND`(404)、`SCHEMA_INVALID`(400)、`SCHEMA_VERSION_NOT_FOUND`(404)、`SCHEMA_EXECUTION_FAILED`(500)、`SCHEMA_TIMEOUT`(504) |
| Fetch | `FETCH_FAILED`(502)、`FETCH_TIMEOUT`(504)、`PROXY_FAILED`(502) |
| Cache | `CACHE_CLEAR_FAILED`(500) |
| 通用 | `VALIDATION_FAILED`(400)、`UNAUTHORIZED`(401)、`NOT_FOUND`(404)、`CONFLICT`(409)、`INTERNAL_ERROR`(500) |
admin 侧常用错误码:`UNAUTHORIZED`(401)、`INVALID_CREDENTIALS`(401)、`VALIDATION_FAILED`(400)、`PROFILE_NOT_FOUND`(404)、`FINGERPRINT_CONFLICT`(409)、`PROXY_NOT_FOUND`(404)、`COOKIE_NOT_FOUND`(404)、`NODE_NOT_FOUND`(404)、`NODE_NOT_AVAILABLE`(503)、`NODE_RELAY_FAILED`(502)、`DIRECT_UNAVAILABLE`(502/503)、`PACKAGE_FAILED`(500)。
---
# 2. Admin API(端口 5310)
## 2.1 认证 `/api/auth`
### POST /api/auth/login — 登录
```json
// 请求
{ "username": "admin", "password": "******" } // username 缺省为 admin
// 200 → data
{ "username": "admin" }
// 失败 401 INVALID_CREDENTIALS
```
成功时 `Set-Cookie: smax_session=...; HttpOnly; SameSite=Lax`。
### POST /api/auth/logout — 登出 → `data: null`,清除 Cookie
### GET /api/auth/me — 当前登录状态
```json
{ "authenticated": true, "username": "admin" }
// 未登录也返回 200:{ "authenticated": false, "username": null }
```
## 2.2 仪表盘 `/api/dashboard`
### GET /api/dashboard — 概览数据一次返回
```json
// data
{
"statistics": {
"totalProfiles": 10, "runningProfiles": 3, "errorProfiles": 0,
"totalProxies": 8, "captcha": 12, "recaptcha": 5, "requests": 1200
},
"activity": [ { "id": "12345678", "name": "...", "status": "running",
"browserType": "browser", "proxyName": "...", "totalRequests": 100,
"lastUsedAt": "...", "updatedAt": "..." } ],
"captchaSeries": [ /* 近 7 天验证码统计 */ ],
"requestSeries": [ /* 近 7 天请求统计 */ ],
"recentLogs": [ /* 最近 12 条日志 */ ]
}
```
## 2.3 Profile 管理 `/api/profiles`
> `:id` 统一接受 **Profile UUID 或指纹数字**(6~8 位);节点侧运行 ID 一律为指纹数字。
### GET /api/profiles — 列表(联表统计)
Query:`q`(搜索)、`status`、`proxyId`、`node`、`sort`、`order`、`page`、`limit`
```json
// data
{
"total": 10, "page": 1, "limit": 20,
"items": [ {
"id": "12345678", "name": "...", "fingerprint": "12345678",
"node": "node-1", "status": "running", "browserType": "browser",
"browser": { /* 指纹快照 */ }, "tlsPreset": "...",
"proxyId": "uuid", "proxyName": "...", "proxyStatus": "connected", "proxyType": "socks5",
"statistics": {
"totalRequests": 0, "successfulRequests": 0, "failedRequests": 0,
"captchaCount": 0, "recaptchaCount": 0, "recaptchaSolved": 0, "lastCaptchaAt": null
},
"lastUsedAt": null, "createdAt": "2026-09-01T00:00:00.000Z"
} ]
}
```
### POST /api/profiles — 创建 Profile
```json
// 请求(全部可选)
{
"name": "my-profile",
"fingerprint": "12345678", // 6~8 位数字(100000~99999999),缺省自动生成;冲突 → 409 FINGERPRINT_CONFLICT
"browserType": "browser", // 仅支持 "browser"
"proxyId": "uuid", // 绑定代理
"node": "node-1", // 指派运行节点(随分配自动注册节点)
"notes": "备注"
}
// 201 → data:Profile 详情(同 GET /api/profiles/:id 的 profile 部分)
```
### GET /api/profiles/nodes — 已有 Profile 的运行节点列表(去重,用于筛选)→ `data: ["node-1", ...]`
### GET /api/profiles/:id — 详情
```json
// data
{
/* Profile 详情(指纹、代理、网络、统计等) */
"cookieCount": 12,
"captchaEvents": [ { "id": "1", "type": "recaptcha", "website": "...", "url": "...", "createdAt": "...", "metadata": {} } ],
"logs": [ { "id": "1", "time": "...", "level": "info", "type": "...", "message": "..." } ]
}
```
### PUT /api/profiles/:id — 编辑基础信息
请求体(字段均可选):`{ name?, browserType?, proxyId?(null 解绑), node?(空串回退 "local"), notes? }` → `data: Profile 详情`。
变更节点/代理会自动写系统日志,节点随分配自动注册。
### DELETE /api/profiles/:id — 删除 → `data: null`
### POST /api/profiles/:id/duplicate — 复制(克隆指纹)
请求体 `{ name? }`(缺省 `<原名>-copy`)→ `201`,data 为新 Profile 详情。
### POST /api/profiles/:id/start · /stop — 启动/停止(状态标记 running/stopped)
→ `data: Profile 详情`。(真实浏览器启停走 [2.4 浏览器操作中继](#24-浏览器操作中继)。)
### POST /api/profiles/:id/fetch — 通过 Profile 运行时执行请求(转发到节点)
```json
// 请求
{
"runtime": "fetch", // fetch | browser | page;缺省取 Profile 的 browserType
"url": "https://example.com",
"method": "GET", // GET/POST/PUT/PATCH/DELETE/HEAD/OPTIONS
"headers": {}, // 透传
"body": "...", // 透传(字符串)
"timeoutMs": 30000 // 1000~120000
}
```
转发规则:`runtime=fetch` → 节点 `POST /profiles/:id/fetch`(指纹 UA/头序 + Cookie Jar + impersonate 代理链);`browser` / `page` → 节点 `POST /profiles/:id/browser/fetch`(真实浏览器渲染 / 页面上下文 fetch)。
```json
// data(RunResult,归一化后)
{
"ok": true, "status": 200, "headers": {}, "bodyText": "<html>...",
"truncated": false,
"timingMs": 812, "runtime": "browser",
"captcha": { "type": "recaptcha", "kind": "cloudflare", "website": "...", "url": "..." } // 检出时才有
}
```
`bodyText` 完整返回;仅当响应体超过节点 `FETCH_BODY_LIMIT`(默认 5MB,环境变量可调)时被截断,此时 `truncated: true`(非文本 content-type 时 bodyText 为 base64,`bodyBase64` 见节点侧对应接口)。响应命中 CF 挑战时节点自动回退浏览器过盾(见 3.5),此时 `runtime` 可能为 `browser-fallback` 且 `browserSolved: true`。
结果自动落库:请求统计、验证码事件。
## 2.4 浏览器操作中继
以下端点均需 admin 会话/API Token;admin 负责:解析 Profile → 定位节点(`nodes.base_url`)→ 自动**幂等导入** Profile 到节点(按指纹,节点侧缺失时以 admin 声明的指纹/代理创建)→ 附 `x-node-key` 转发。请求/响应与节点侧同名接口一致,细节见第 3 章对应小节。
| Admin 端点 | 方法 | 转发到节点 | 说明 |
|---|---|---|---|
| `/api/profiles/:id/browser-status` | GET | `GET /browser/status` | 浏览器/VNC 状态 |
| `/api/profiles/:id/browser-actions` | POST | `POST /browser/actions` | 执行动作批次(4MB 上限) |
| `/api/profiles/:id/browser-events?quality=` | GET | `GET /browser/events` | SSE 中继(state/screenshot/action/error) |
| `/api/profiles/:id/browser-restart` | POST | `POST /browser/restart` | 重启浏览器 |
| `/api/profiles/:id/browser-stop` | POST | `POST /browser/stop` | 关闭浏览器 |
| `/api/profiles/:id/browser-window` | POST | `POST /browser/window` | 新建窗口 |
| `/api/profiles/:id/cf-solver` | POST | `POST /cf-solver` | 自动 CF 挑战开关,`{ enabled: bool }` |
| `/api/profiles/:id/vnc/restart` | POST | `POST /vnc/restart` | 重启/恢复 x11vnc |
| `/api/profiles/:id/vnc/paste` | POST | `POST /vnc/paste` | VNC 粘贴,`{ text }`(1MB 上限) |
| `/api/profiles/:id/vnc/ws` | GET(WS) | `WS /vnc/ws` | noVNC WebSocket 中继(鉴权后 upgrade) |
| `/api/profiles/:id/browser-cookies` | GET / PUT | `GET·PUT /cookies` | 读取/覆盖导入节点浏览器 Cookie(8MB 上限) |
| `/api/profiles/:id/cookie-filter` | GET / PUT | `GET·PUT /cookie-filter` | Cookie 同步黑白名单 |
| `/api/profiles/:id/captcha/state` | GET | `GET /captcha/state` | 打码状态 |
| `/api/profiles/:id/captcha/events` | GET | `GET /captcha/events` | 打码状态 SSE |
| `/api/profiles/:id/captcha/image` | GET | `GET /captcha/image` | 主图(jpeg 原样透传) |
| `/api/profiles/:id/captcha/tile/:idx` | GET | `GET /captcha/tile/:idx` | 格子图 |
| `/api/profiles/:id/captcha/:action` | GET / POST | `GET·POST /captcha/:action` | `action ∈ start·adopt·click·verify·reload·action·autosolve·status·detach`,未知 action → 404 |
| `/api/profiles/:id/direct-session` | GET | `POST /direct-ticket` | 签发直连会话,见下 |
### GET /api/profiles/:id/direct-session — 直连会话
前端 VNC/浏览器操作直连节点的前置:admin 以节点密钥向节点索取一次性票据,密钥与 API Token 均不出服务端。
```json
// data
{ "nodeUrl": "https://node.example.com", "profileId": "12345678", "ticket": "dtk_...", "expiresInMs": 300000 }
```
- 节点未配置公开地址(`nodes.public_url`)→ `503 DIRECT_UNAVAILABLE`,前端回退 admin 中继。
- `profileId` 须为指纹数字或 UUID,否则 `400 VALIDATION_FAILED`。
## 2.5 Cookie 管理(admin 本地 Jar)
admin 数据库中按 Profile 存储的 Cookie(与节点浏览器同步的"源头")。Cookie 形状:
```json
{ "id": "uuid", "profileId": "uuid", "name": "k", "value": "v", "domain": "example.com",
"path": "/", "expiresAt": "2026-12-31T00:00:00.000Z", "secure": false, "httpOnly": false,
"sameSite": "lax", "createdAt": "...", "updatedAt": "..." }
```
| 端点 | 说明 |
|---|---|
| `GET /api/profiles/:id/cookies` | 列出 Cookie → `data: Cookie[]` |
| `POST /api/profiles/:id/cookies` | 新增单条:`{ name, value, domain, path?, expiresAt?, secure?, httpOnly?, sameSite?(strict\|lax\|none) }` → `201` |
| `PUT /api/profiles/:id/cookies` | 批量导入:数组或 `{ cookies:[...] }`(按 name+domain+path upsert)→ `{ imported: n }` |
| `DELETE /api/profiles/:id/cookies` | 清空该 Profile → `{ cleared: n }` |
| `PUT /api/cookies/:id` | 编辑单条(部分更新) |
| `DELETE /api/cookies/:id` | 删除单条 → `data: null` |
## 2.6 代理管理 `/api/proxies`
代理形状(`serializeProxy`):`{ id, name, host, port, type, username, enabled, tags[], status, checkMessage, exitIp, exitCountry, exitCity, timingMs, createdAt, updatedAt }`。
### GET /api/proxies — 列表
Query:`q`、`tag`、`country`、`sort`、`order` → `data: Proxy[]`
### POST /api/proxies — 创建
```json
{ "name": "p1", "host": "10.0.0.10", "port": 1080, "type": "socks5", "username": "", "password": "" }
// type 仅支持 socks5 | http | https,缺省 socks5 → 201
```
### POST /api/proxies/import — 批量导入
```json
{ "text": "socks5://user:pass@1.2.3.4:1080\n5.6.7.8:1080" }
// → data { created, skipped, failed: [ { line, reason } ] }
```
一行一个,支持 `protocol://user:pass@host:port` 与 `host:port`。
### GET /api/proxies/:id — 详情
### PUT /api/proxies/:id — 更新(部分字段;`password: ""` 表示清除;`tags` 为字符串数组)
### DELETE /api/proxies/:id — 删除 → `data: null`
### POST /api/proxies/:id/test — 连通性测试(真实走代理出口,geoip 归属)
```json
// data
{ "ok": true, "message": "...", "exitIp": "1.2.3.4", "exitCountry": "US", "exitCity": "...",
"timingMs": 320, "proxy": { /* 更新后的代理 */ } }
```
### POST /api/proxies/:id/toggle — 启用/禁用切换 → `data: Proxy`
## 2.7 节点管理 `/api/nodes`
节点形状(`serializeNode`):
```json
{ "name": "node-1", "ip": "1.2.3.4", "country": "US", "status": "online", "cpu": "12%", "memory": "40%",
"baseUrl": "http://1.2.3.4:3100", "publicUrl": "https://node.example.com", "version": "20260924.1010",
"maxProfiles": null, "maxMemoryPct": null, "maxCpuPct": null, "hasKey": true,
"totalProfiles": 5, "runningProfiles": 2, "createdAt": "...", "updatedAt": "..." }
```
节点不提供手动添加 —— 随 Profile 分配(`POST /api/profiles` 的 `node` 字段)或安装自注册自动创建。
### GET /api/nodes — 列表 → `data: Node[]`
### POST /api/nodes/provision — 预置节点(生成/轮换通信密钥)
```json
// 请求 { "name": "node-2" }
// data
{ "name": "node-2", "key": "pmk_...", "installCmd": "wget -qO- https://<admin>/node/install?key=..&name=node-2 | bash" }
```
### POST /api/nodes/register — 节点安装完成后自注册(免会话)
```json
// 请求
{ "name": "node-2", "key": "pmk_...", "baseUrl": "http://1.2.3.4:3100", "ip": "1.2.3.4" }
// name 与 key 必填;key 须与 provision 下发的一致 → data: Node
```
### POST /api/nodes/heartbeat — 节点心跳上报(注册 + 指标更新)
```json
// 请求
{ "name": "node-1", "ip": "1.2.3.4", "country": "US", "status": "online",
"cpu": "12%", "memory": "40%", "baseUrl": "http://1.2.3.4:3100", "version": "20260924.1010" }
```
- 认证:admin 会话/API Token,**或**该节点自己的密钥(`x-node-key` / `Bearer`,且须与 `name` 匹配)。
- `status` 仅支持 `unknown|online|offline`;`version` 为节点代码版本(v0.4 起落库)。
### PUT /api/nodes/:name — 编辑节点
```json
{ "ip": "...", "country": "US", "publicUrl": "node.example.com", // 无协议自动补 https://;空串清除(回退中继)
"status": "online", "maxProfiles": 20, "maxMemoryPct": 80, "maxCpuPct": 90 } // 配置项传 null 清除
// → data: Node
```
### DELETE /api/nodes/:name — 删除(节点上仍有 Profile 时拒绝)→ `data: null`
### POST /api/nodes/update-batch — 批量触发节点自更新
```json
// 请求 { "names": ["node-1", "node-2"] }
// data
{ "results": [ { "name": "node-1", "ok": true, "detail": "已触发" },
{ "name": "node-2", "ok": false, "detail": "节点未注册地址或无密钥" } ] }
```
逐个串行中继 `POST {节点}/system/update`(15s 超时);节点后台拉包替换后自行退出,由容器/pm2 重启生效。
## 2.8 节点代码包与自更新
### GET /node/install?key=&name= — 下发一键安装脚本
返回 bash 脚本(`text/plain`):生成 docker-compose.yml → 拉镜像 → 启动容器 → 健康检查 → 自注册。支持环境变量覆盖:`NODE_KEY`、`NODE_NAME`、`IMAGE`、`INSTALL_DIR`,参数 `-n/--name -k/--key -i/--image -b/--base-url -d/--dir`。
### GET /node/package?key= — 下载节点代码包
- 凭节点密钥;`tar.gz`(src/ + package.json + bun.lock + tsconfig.json + version)。
- 响应头:`x-package-version: <版本>`、`content-disposition: attachment; filename="serpmax-node-<版本>.tgz"`。
- 无效 key → `401 UNAUTHORIZED`。
### GET /node/version — admin 侧节点代码版本(公开)
```json
{ "success": true, "data": { "version": "20260924.1010" } }
```
> 版本号 = 源码内容指纹,`YYYYMMDD.HHMM` 日期格式,缓存 30s。节点页用 `GET /node/version` 与各节点 `GET /system/version` 比对判断是否需要更新。
## 2.9 日志 `/api/logs`
### GET /api/logs — 日志列表
Query:`q`、`profileId`、`proxyId`、`level`、`type`、`from`、`to`、`sort`、`order`、`page`、`limit`
```json
// data
{ "total": 100, "page": 1, "limit": 20, "items": [ { "id": "1", "time": "...", "level": "info",
"type": "profile.started", "message": "...", "profileId": "uuid", "proxyId": null, "metadata": {} } ] }
```
### GET /api/logs/filters — 现存的级别与类型(筛选下拉)→ `data: { levels: [...], types: [...] }`
## 2.10 系统设置 `/api/settings`
### GET /api/settings — 当前设置
```json
// data
{ "username": "admin", "apiToken": "smax_...",
"settings": { "theme": "system", "defaultRuntime": "fetch", "autoTestProxy": true,
"defaultTimeout": 30000, "systemName": "SerpMax Admin" } }
```
### PUT /api/settings — 更新(白名单键)
```json
{ "theme": "dark", "defaultRuntime": "browser", "autoTestProxy": false, "defaultTimeout": 60000, "systemName": "..." }
// → data { changed: n };无可更新项 → 400
```
### POST /api/settings/password — 修改管理员密码
```json
{ "currentPassword": "...", "newPassword": "..." } // 新密码 ≥6 位;当前密码错 → 401 INVALID_CREDENTIALS
```
### POST /api/settings/api-token/rotate — 重置 API Token → `data: { apiToken: "smax_..." }`(旧 Token 立即失效)
## 2.11 事件上报 `/api/events`
节点(或任意持 Token 方)向 admin 上报事件;`profileId` 可为 UUID 或指纹数字(自动解析)。
```json
// POST /api/events
{
"event": "captcha.detected", // 见下表
"profileId": "12345678", // 按事件类型必填
"proxyId": "uuid",
"type": "recaptcha", "kind": "cloudflare", "website": "...", "url": "...",
"level": "info", "message": "...", "status": 200, "ok": true,
"metadata": {}
}
// data 按类别:{ "recorded": "captcha" | "captcha-solved" | "browser" | "proxy" | "request" | "log" }
```
| event | 副作用 |
|---|---|
| `captcha.detected` / `recaptcha.detected` / `challenge.detected` | 记验证码事件 + 统计(`type=recaptcha` 判定:`type` 字段或事件名) |
| `captcha.solved` / `recaptcha.solved` | recaptcha 解题成功计数 |
| `browser.started` / `browser.stopped` / `browser.crashed` | Profile 状态 → running / stopped / error |
| `proxy.connected` / `proxy.failed` | 代理检测状态(必须带 `proxyId`) |
| `request.completed` | 请求统计(`ok` 优先,缺省按 `status` 2xx/3xx 判定) |
| 其他 | 仅按 `level`(debug/info/success/warn/error)记日志 |
---
# 3. Node API(端口 3100)
> Profile Manager 节点:负责指纹浏览器(Chromium/CloakBrowser)生命周期、Lease、fetch/render、打码、VNC。
> 鉴权见 [1.2](#12-认证)。`data` 字段以下均指成功信封中的内容。
## 3.1 Profile
### POST /profiles — 创建(支持 Idempotency-Key 幂等)
```json
// 请求(全部可选;对象字段只校验"必须是对象",结构由指纹导入约定决定)
{
"id": "12345678", // 外部 ID(字母数字与 -_,≤64 位);admin 导入时用指纹数字
"name": "my-profile",
"fingerprint": { "import": { /* admin BrowserFingerprint:UA/画布/字体等,导入身份以声明值为准 */ } },
"network": { "tls": { "preset": "chrome-146" }, "proxy": { /* ... */ } },
"proxy": { "id": "...", "protocol": "socks5", "host": "1.2.3.4", "port": 1080, "username": "", "password": "" },
"browser": { /* 指纹快照(同 fingerprint.import) */ },
"metadata": {}
}
// 201 → data:Profile 对象
```
Profile 状态机:`READY → IN_USE → READY`,`→ DELETING`,`ERROR →(隐式恢复)READY`。
### GET /profiles — 列表 → `data: { profiles: [...], total: n }`
### GET /profiles/:id — 详情(含当前 Lease 概要)
```json
// data
{ "id": "...", "name": "...", "state": "READY", "updatedAt": "...", /* ... */
"lease": { "leaseId": "...", "consumerId": "...", "expiresAt": "..." } } // 有 Lease 时
```
### DELETE /profiles/:id?force=true|false — 删除
流程:停浏览器 → 清 Runtime → 删数据 → 删 Profile。`force=1/true` 强制(如忽略 Lease);→ `data: { deleted: true, profileId }`。
## 3.2 Lease(租约)
同一 Profile 同时只允许一个 Consumer;浏览器操作要求持有 Lease(部分接口无 Lease 时会短期自动接管)。
### POST /profiles/:id/lease — 获取
```json
// 请求
{ "consumerId": "my-worker-1", "ttlMs": 60000 } // ttlMs ≥1000
// 201 → data
{ "leaseId": "ls_...", "profileId": "12345678", "expiresAt": "2026-09-24T10:00:00.000Z" }
```
- ⚠️ 此端点特例:参数校验失败与「正在删除」时 **HTTP 状态码为 400/409,但信封仍是 `success:true`**,错误在 `data` 内 —— `data: { error: "VALIDATION_FAILED", message: "ttlMs 必须 ≥1000" }` 或 `data: { error: "PROFILE_DELETING" }`。
- ERROR 状态自动隐式恢复为 READY 后发放。
### POST /profiles/:id/lease/:leaseId/heartbeat — 续租
```json
// 请求 { "ttlMs": 60000 } // 可选,重新计时
// data { "leaseId": "...", "profileId": "...", "expiresAt": "...", "lastHeartbeat": "..." }
```
### DELETE /profiles/:id/lease/:leaseId — 释放 → `data: { released: true, profileId, leaseId }`(Profile 回 READY,浏览器可能仍在运行)
## 3.3 浏览器生命周期
### POST /profiles/:id/browser/start — 启动浏览器(需 Lease)
请求头 `x-lease-id`(或 body `leaseId`)须匹配当前 Lease,否则 `409 LEASE_CONFLICT`。
```json
// data
{ "profileId": "12345678", "browserId": "...", "reused": false,
"cdp": { "endpoint": "http://127.0.0.1:9222", "wsEndpoint": "ws://...", "port": 9222 },
"processId": 1234 }
```
### POST /profiles/:id/browser/stop — 停止(不要求 Lease;验收流程 = 先释放 Lease 再 stop)→ `data: { profileId, status: "STOPPED" }`
### POST /profiles/:id/browser/restart — 重启
有 Lease 时校验持有者;无 Lease 时以 `admin:restart:<requestId>` 短期接管(120s),完成即释放。响应同 `start`。
### GET /profiles/:id/browser/status — 状态
```json
// data
{ "profileId": "...", "status": "RUNNING", // RUNNING | STOPPED
"browserId": "...", "cdpEndpoint": "http://...", "processId": 1234, "startedAt": "...",
"vncPort": 6100, "vncDisplay": ":100", // VNC 启用时
"vncAvailable": true,
"idleCloseInMs": 118000 } // 空闲自动关闭剩余毫秒;未启用为 null
```
### POST /profiles/:id/browser/window — 新建空白窗口(浏览器须运行中)→ `data: { url: "about:blank", windows: 3 }`
### GET /profiles/:id/browser/cdp — CDP 端点 → `data: { profileId, cdp: { endpoint, wsEndpoint, port } }`;未运行 → `409 BROWSER_NOT_RUNNING`
### GET·POST /profiles/:id/cf-solver — 自动 CF 挑战开关
- `GET` → `data: { enabled: true }`(缺省启用,随节点进程保留)
- `POST` `{ "enabled": false }` → `data: { enabled: false }`
## 3.4 浏览器操作(Action + SSE)
> 设计:HTTP 负责控制、SSE 负责状态、截图负责视觉反馈;协议细节见 `node/docs/Browser-Control.md`。
### POST /profiles/:id/browser/actions — 执行动作批次(按序,任一失败即停)
```json
// 请求
{
"actions": [
{ "type": "goto", "url": "https://www.google.com/recaptcha/api2/demo?hl=en" },
{ "type": "fill", "selector": "#email", "text": "a@b.c" },
{ "type": "click", "selector": "button.submit" }, // 或 { "x": 100, "y": 200 } 坐标点击
{ "type": "press", "key": "Control+c" },
{ "type": "wait", "ms": 1000 } // 或 { "selector": "...", "ms": 5000 }
],
"screenshotQuality": 30, // 1-100,默认 30
"timeout": 30000 // 单动作超时,默认 30000,上限 120000
}
```
支持 16 种 action:`goto · reload · back · forward · click · dblclick · hover · fill · type · press · select · check · uncheck · scroll · scrollIntoView · wait`(单次最多 30 个,详见 [附录 4.1](#41-browser-action-类型一览))。
```json
// data
{ "actionId": "act_...", "results": [ /* 每个动作的产出,按序 */ ], "clipboardText": "..." }
```
- 浏览器未运行(如空闲自动关闭后)自动重启 —— "操作即连接"。
- Lease:已持有则复用;无 Lease 时短期接管(`admin:action:<requestId>`,批次期间持有,完成即释放)。
- 每个动作的进度经 SSE `action` 事件推送(`{ actionId, status: running|completed|failed }`)。
### GET /profiles/:id/browser/events?quality=30 — SSE 事件流
- 鉴权:节点密钥或直连票据(EventSource 无法自定义头,票据在 TTL 内可复用)。
- 事件类型:`state`(url/title/loading/viewport)、`screenshot`(jpeg base64,动态 FPS 调度)、`action`(动作进度)、`error`(错误)、`browser`(`{ status:"STOPPED", reason:"browser-closed" }`,浏览器关闭时终态)。
- 15s `: ping` 保活;连接期间自动持有 Lease 并心跳,断开释放。
- 浏览器随需启动;`quality` 控制截图质量。
## 3.5 HTTP 请求执行
### POST /profiles/:id/fetch — Profile 上下文 HTTP 请求(不走浏览器)
指纹 UA/头序 + Cookie Jar + impersonate-proxy(→socks5)链路。适配器两级:优先 **go-fetch**(Go 原生客户端,`GO_FETCH_BIN` 或节点根 `bin/go-fetch`;原生 CONNECT 隧道与 MITM 完全兼容),回落系统 curl。MITM 出站连接按 (上游, 域名) 池化复用(空闲 120s/寿命 10min,借出前 PING 探活),一次性 fetch 可复用浏览器焐热的同一条出站连接,连接层指纹与浏览器一致。
```json
// 请求
{ "url": "https://httpbin.org/get", "method": "GET", "headers": { "x-demo": "1" },
"body": "...", "timeoutMs": 30000, "browserFallback": true, "rawEncoding": false } // timeoutMs 100~300000
// data:RunResult 形状(ok/status/headers/bodyText/timingMs/runtime:"fetch" 等)
// 响应体超过 FETCH_BODY_LIMIT(默认 5MB)时截断并置 truncated:true
```
**rawEncoding 压缩直通**(默认 false;v0.42.0 新增,serp-api 使用):请求强制 `accept-encoding: gzip`,
go-fetch 以 `--keep-encoding` 透传上游原始 gzip 字节——响应 `body` 为 base64、`bodyBase64:true`、
`bodyEncoding:"gzip"`,全链(Google→go-fetch→node→调用方)不解压不重压缩;上游未压缩(identity)时仍按文本返回。
仅 impersonate(go-fetch/curl)链路支持,其余链路自动降级为解压文本;此模式下不做 CAPTCHA 检测与 CF 浏览器回退。
**CF 挑战自动回退**(默认开启,仅 GET;`browserFallback:false` 关闭):响应被 Cloudflare/Turnstile 挑战拦截时,节点自动借同一 Profile 的浏览器过盾(CF 自动解题钩子生效)→ 吸收过盾 Cookie 后重试一次纯 HTTP;若站点免挑战放行浏览器、从不发 clearance(wethrift 类),则直接返回浏览器渲染内容,此时 `runtime:"browser-fallback"`、`browserSolved:true`、`status:200`。
### POST /profiles/:id/browser/fetch — 浏览器运行时请求
已运行直接 goto;未运行按需启动(复用真实指纹/代理)。两种模式:
```json
// 请求
{
"url": "https://example.com/api/data",
"runtime": "page", // 缺省 = 整页导航(browser 模式)
"method": "GET", "headers": {}, "body": "...", // 仅 page 模式使用
"timeoutMs": 45000 // 钳制 5000~120000
}
```
- `runtime:"page"`:导航到目标源(等 CF 解题放行)后在**页面 JS 里执行 fetch**,复用浏览器原生 TLS/连接池/Cookie/cf_clearance —— 适合 API 类请求。
- 默认(整页导航):返回渲染后 HTML + `title`。
- 响应含 `restarted: true|false`(本次是否触发了浏览器启动)、`finalUrl`、`timingMs`。
### POST /profiles/:id/browse — 浏览器渲染一次目标页面(同步返回)
```json
// 请求 { "url": "https://example.com", "timeoutMs": 30000 } // timeoutMs 1000~120000
// data
{ "ok": true, "status": 200, "headers": {}, "bodyText": "<html>...", "truncated": false,
"timingMs": 900, "runtime": "chromium",
"captcha": { "type": "recaptcha", "kind": "...", "website": "...", "url": "..." } }
```
- Lease:已持有则复用;否则临时获取(`admin:browse:<requestId>`),结束释放,并新开页渲染后关闭。
- 检出验证码时发布 `captcha.detected` / `challenge.detected` / `blocked.detected` 事件。
- 超时 → `504 FETCH_TIMEOUT`;证书/代理错误 → `502 PROXY_FAILED`。
## 3.6 Cookie(node 运行时)
操作**浏览器中的 Cookie**(运行中经 Playwright Context),并与 fetch 链路的 Cookie Jar 双向同步;浏览器未运行时仅操作 Jar。形状:`{ id, profileId, name, value, domain, path, expiresAt, secure, httpOnly, sameSite }`。
| 端点 | 说明 |
|---|---|
| `GET /profiles/:id/cookies` | 浏览器运行中 → `context.cookies()`(过滤同步黑名单);未运行 → Jar |
| `PUT /profiles/:id/cookies` | 覆盖导入:数组或 `{cookies:[...]}`;运行中 addCookies + Jar,未运行仅 Jar → `{ imported, browser, deferred }`(`deferred:true` 表示浏览器侧留待下次启动) |
| `DELETE /profiles/:id/cookies` | 浏览器 clearCookies + 清 Jar → `{ cleared: true }` |
`expiresAt` 兼容 ISO 字符串与秒/毫秒时间戳;`sameSite ∈ strict|lax|none`。
### GET·PUT /profiles/:id/cookie-filter — Cookie 同步黑白名单
```json
// GET → data { "whitelist": [], "blacklist": ["ads.example.com"] } // 空 = 不限
// PUT 请求同形状 → data { whitelist, blacklist, pruned: 3 } // 立即剔除 Jar 中不允许同步的存量(浏览器 USER_DATA 不动)
```
只影响"浏览器 ↔ Jar / admin 视图"的同步方向,浏览器本地 Cookie 不变。
## 3.7 缓存管理
### GET /profiles/:id/cache — 磁盘用量
```json
// data
{ "profileId": "...", "totalBytes": 10485760, "cacheBytes": 8388608, "storageBytes": 2097152, "downloadsBytes": 0 }
```
### POST /profiles/:id/cache/clear — 清理
```json
// 请求 { "mode": "soft" } // soft=清缓存目录(cache);hard=整目录重置(含 storage,浏览器须停止)
// soft → data { profileId, mode: "soft", bytesFreed: 8388608 }
// hard → data { profileId, mode: "hard", bytesFreed: null }
// 其他值 → 400 VALIDATION_FAILED
```
## 3.8 Schema 与 Execution
Schema = 声明式自动化流程(版本化,禁止覆盖只能递增)。
### POST /schemas — 创建新版本 → `201`,data:Schema 对象
### GET /schemas/:id — 读取
- `?version=N` 指定版本(缺省最新);`?versions=1` → `data: { id, versions: [...] }`
### DELETE /schemas/:id — 删除(全部版本;运行中的版本阻止删除)→ `data: { deleted: true, schemaId }`
### POST /profiles/:id/schema/:schemaId/run — 执行 Schema
```json
// 请求
{ "version": 3, "variables": { "keyword": "test" }, "leaseId": "ls_..." } // 均可选;leaseId 也可用 x-lease-id 头
// 202 → data
{ "executionId": "exec_...", "profileId": "...", "schemaId": "...", "version": 3, "status": "RUNNING" }
```
Schema 内 `captcha.autoSolve` 声明会自动触发 reCAPTCHA 看门狗注入。
### GET /executions/:id — 查询执行记录(状态/步骤结果)
### POST /executions/:id/cancel — 取消 → `data: { executionId, cancelRequested: true|false }`
## 3.9 reCAPTCHA 打码
打码会话由显式端点(答题面板 / autosolve)创建;`state` 是只读端点,不会触发注入。
| 端点 | 方法 | 说明 |
|---|---|---|
| `/profiles/:id/captcha/state` | GET | 组合状态:手动 mapper 状态 + 自动求解器摘要;未连接会话 → `{ status:"absent", message, solver }` |
| `/profiles/:id/captcha/events` | GET | SSE:状态推送(连接即创建会话,15s ping) |
| `/profiles/:id/captcha/image` | GET | 主图 jpeg(无图 → 404 `no image`) |
| `/profiles/:id/captcha/tile/:idx` | GET | 第 idx 格子图 jpeg |
| `/profiles/:id/captcha/start` | POST | 点击 checkbox 拉起挑战 |
| `/profiles/:id/captcha/adopt` | POST | 采纳页面上已打开的挑战 |
| `/profiles/:id/captcha/click` | POST | `{ "index": 3 }` 点格子,或 `{ "nx": 0.5, "ny": 0.5 }` 归一化坐标 |
| `/profiles/:id/captcha/verify` | POST | 提交验证 |
| `/profiles/:id/captcha/reload` | POST | 换一题 |
| `/profiles/:id/captcha/action` | POST | `{ "kind": "next" \| "skip" \| "verify" }`(缺省 verify) |
| `/profiles/:id/captcha/autosolve` | POST | 触发自动求解(立即返回,结果轮询 status);页面无挑战 → `{ started:false, reason:"no-challenge" }`;求解中幂等忽略 |
| `/profiles/:id/captcha/status` | GET | 自动求解器状态:`{ solving, result, solvedToken }` |
| `/profiles/:id/captcha/detach` | POST | 丢弃会话 → `{ disposed: true }` |
## 3.10 VNC 与直连
### WS /profiles/:id/vnc/ws — noVNC WebSocket 桥(→ 本机 x11vnc TCP)
- 握手即升级;鉴权:节点密钥或一次性直连票据(`?ticket=`)。
- Query `cfAuto=1|0`:连接时设置「自动 CF 挑战」开关(缺省不动)。
- 浏览器未运行或 VNC 未启用 → 404 信封(升级前)。
- 客户端真实操作(RFB KeyEvent / 带按键 PointerEvent / ClientCutText)会重置浏览器空闲计时。
### POST /profiles/:id/direct-ticket — 签发直连票据(仅节点密钥持有者)
→ `data: { ticket: "dtk_...", expiresInMs: 300000 }`。票据绑定 Profile、一次性(actions)短时可复用(events/VNC)、5 分钟过期。
### POST /profiles/:id/vnc/paste — VNC 粘贴
```json
// 请求 { "text": "hello" }
// 纯 X 路径:xsel 写 CLIPBOARD + x11vnc 注入真实 Ctrl+V,落点为 VNC 当前焦点
// data { "pasted": 5, "mode": "x11" }
```
VNC 未启用 → `409 BROWSER_NOT_RUNNING`;缺 xsel → `500 INTERNAL_ERROR`。
### POST /profiles/:id/vnc/restart — VNC 重启/恢复
x11vnc 重启(色深切换清残留);实例丢失时自动整组重建(浏览器不动)。
→ `data: { restarted: true, recovered: false, port: 6100 }`;无法恢复 → `409 BROWSER_NOT_RUNNING`。
## 3.11 系统
| 端点 | 方法 | 说明 / data |
|---|---|---|
| `/health` | GET | `{ status:"ok", uptimeMs }` |
| `/ready` | GET | 就绪检查:`{ ready, checks: { profileCache, runtimeManager, chromiumBinary, recoveryDone } }`;未就绪整体 503 |
| `/metrics` | GET | Prometheus 文本(`text/plain; version=0.0.4`),非 JSON 信封 |
| `/events?limit=&profileId=` | GET | 最近事件环形缓冲:`{ events: [...], total }`(调试) |
| `/browsers` | GET | `{ running: n }` 当前运行浏览器数(调试) |
| `/node/info` | GET | 节点自述(与心跳同构):`{ name, ip, country, status:"online", version, admin:{enabled,url,eventForward,heartbeatInterval}, profiles:{total,running}, browsersRunning }` |
| `/system/version` | GET | `{ version: "20260924.1010" }`(30s 缓存) |
| `/system/update` | POST | 自更新:立即 `{ started:true, from:"<旧版本>" }`;后台拉包(admin `/node/package`)→ 替换 /app → `bun install` → `process.exit(0)` → 容器 `restart: unless-stopped` 载入新代码。未配置 `ADMIN_URL` → `400 { started:false, reason:"ADMIN_URL 未配置" }` |
---
# 4. 附录
## 4.1 Browser Action 类型一览
单次请求最多 **30** 个 action;按数组顺序执行,任一失败即停止后续。前置校验快速失败:
| type | 必填/常用参数 | 说明 |
|---|---|---|
| `goto` | `url`(http/https) | 导航 |
| `reload` | — | 刷新 |
| `back` / `forward` | — | 历史前进/后退 |
| `click` | `selector` 或 `x`+`y`(视口 CSS 像素) | 点击 |
| `dblclick` | `selector` | 双击 |
| `hover` | `selector` | 悬停 |
| `fill` | `selector`、`text` | 清空并填入 |
| `type` | `selector`、`text` | 逐键输入 |
| `press` | `key`(如 `Enter`、`Control+c`) | 按键;`Control+c` 的剪贴板结果经 `clipboardText` 返回 |
| `select` | `selector`、`value(s)` | 下拉选择 |
| `check` / `uncheck` | `selector` | 勾选/取消 |
| `scroll` | `x`/`y` 或 `selector` | 滚动 |
| `scrollIntoView` | `selector` | 滚动到元素 |
| `wait` | `ms` 或 `selector` | 等待时长/元素出现 |
请求级参数:`screenshotQuality`(1-100,默认 30)、`timeout`(单动作超时 ms,默认 30000,上限 120000)。
错误码:`INVALID_ACTION`(类型非法/超数)、`INVALID_ACTION_PARAMETER`(参数缺失)、`ELEMENT_NOT_FOUND`、`ELEMENT_NOT_VISIBLE`、`ACTION_TIMEOUT` 等,见 [1.4](#14-错误码表)。
## 4.2 SSE 事件格式
`GET /profiles/:id/browser/events` 每条事件:
```
event: state
data: {"url":"https://...","title":"...","loading":false,"viewport":{"w":1440,"h":900,"dpr":1}}
event: screenshot
data: {"jpeg":"<base64>","w":1440,"h":900}
event: action
data: {"actionId":"...","status":"completed"}
event: browser
data: {"status":"STOPPED","reason":"browser-closed"}
```
- 注释行 `: ping`(15s)保活;空闲超时 120s(服务端)。
- `GET /profiles/:id/captcha/events` 同为 SSE,`data` 为打码状态对象(带 `solver` 摘要)。
## 4.3 典型调用流程
**自动化消费方(节点直连):**
```bash
BASE=http://127.0.0.1:3100
K="x-node-key: $NODE_KEY"
# 1. 创建 Profile(幂等)
curl -s -X POST $BASE/profiles -H "$K" -H "idempotency-key: job-42" \
-H "content-type: application/json" -d '{"id":"12345678","name":"demo"}'
# 2. 取 Lease
curl -s -X POST $BASE/profiles/12345678/lease -H "$K" -H "content-type: application/json" \
-d '{"consumerId":"worker-1","ttlMs":60000}'
# → data.leaseId
# 3. 启动浏览器(拿 CDP)
curl -s -X POST $BASE/profiles/12345678/browser/start -H "$K" -H "x-lease-id: <leaseId>" -d '{}'
# 4. 执行动作
curl -s -X POST $BASE/profiles/12345678/browser/actions -H "$K" \
-H "content-type: application/json" \
-d '{"actions":[{"type":"goto","url":"https://example.com"}],"screenshotQuality":60}'
# 5. 释放 Lease、停浏览器
curl -s -X DELETE $BASE/profiles/12345678/lease/<leaseId> -H "$K"
curl -s -X POST $BASE/profiles/12345678/browser/stop -H "$K"
```
**admin 中继(前端):** 登录(`POST /api/auth/login`)→ `GET /api/profiles/:id/direct-session` 拿 `{ nodeUrl, ticket }` → 前端直连 `wss://<nodeUrl>/profiles/<指纹>/vnc/ws?ticket=<ticket>`(或全程走 admin 中继 `/api/profiles/:id/vnc/ws`、`/browser-actions`、`/browser-events`)。
**节点部署:** admin 节点页 `POST /api/nodes/provision` → 复制 `installCmd` 到目标机执行 → 脚本自动 docker compose 启动 + `POST /api/nodes/register` → 之后节点定时 `POST /api/nodes/heartbeat`;升级时 admin 调 `POST /api/nodes/update-batch`。