AnyBot/docs/protocol.md

118 lines
6.0 KiB
Markdown
Raw Normal View History

# 介面與長連線格式
給要改帳號伺服器或寫其他用戶端的人。所有回覆都是 JSON;錯誤是 `{"error": "白話的說明"}` 加上對應的 HTTP 狀態碼。
## 身分
| 誰 | 怎麼證明 | 從哪來 |
|---|---|---|
| 人(手機) | `Authorization: Bearer <session>` | `POST /relay/login` |
| 一台 AnyBot(主機) | `Authorization: Bearer <host token>` | `POST /relay/hosts`(登入後)或 `POST /relay/pair/claim`(配對碼) |
伺服器只存密碼和金鑰的雜湊值。
## 帳號伺服器(relay)
### 帳號
| 方法 | 路徑 | 說明 |
|---|---|---|
| POST | `/relay/signup` | `{user, password, invite}`。第一個帳號不用邀請碼,之後要 `ANYBOT_RELAY_INVITE` |
| POST | `/relay/login` | `{user, password, device}` → `{token}`。錯 8 次停 5 分鐘 |
| POST | `/relay/logout` | 登出這個 session |
| GET | `/relay/me` | 帳號、可用的 AnyBot(含 `online`、`uses`、`hands`)、登入過的裝置、有沒有備份 |
### 主機
| 方法 | 路徑 | 說明 |
|---|---|---|
| POST | `/relay/hosts` | (人)`{name}` → `{id, token}`。同名的主機會換新金鑰 |
| POST | `/relay/hosts/<id>/remove` | (人)移除主機 |
| GET | `/relay/host/ws` | (主機)長連線,見下面 |
| GET | `/relay/host/next` | (主機)舊式長輪詢:有要求就回 job,25 秒沒有回 204 |
| POST | `/relay/host/reply/<job id>` | (主機)回答舊式 job |
| GET | `/relay/host/peers` | (主機)帳號裡的其他主機 |
| GET/POST | `/relay/host/members` | (主機)團隊成員和角色(admin、member) |
| POST | `/relay/host/notify` | (主機)推播通知到帳號的手機 |
| POST | `/relay/host/sites/<name>` | (主機)發布 Bot 做的網頁,公開在 `/s/<name>/` |
| PUT/GET | `/relay/backup` | 加密備份(伺服器打不開) |
### 配對碼
| 方法 | 路徑 | 說明 |
|---|---|---|
| POST | `/relay/pair` | (人或主機)`{for: 大腦的主機 id}` → `{code, expires_in: 600}` |
| POST | `/relay/pair/claim` | (不用登入)`{code, name}` → `{id, token, user, for, for_name}`。只能用一次;同一個位址錯 8 次停 5 分鐘 |
### 轉送
| 路徑 | 誰 | 轉成 |
|---|---|---|
| `/h/<host>/api/...` | 手機 | 那台 AnyBot 的 `/api/...`,角色是 owner/admin/member |
| `/relay/hosts/<host>/api/sync[/...]` | 同帳號的主機 | 大腦的 `/api/sync...`,角色 `mirror` |
| `/relay/hosts/<host>/api/device/run` | 同帳號的主機 | 手腳的 `/api/device/run`,角色 `device` |
轉送時伺服器附上 `user`、`role` 和 `from`(問的那台主機 id)。對方不在線時立刻回 503「目前離線」;收到了但 35 秒內沒回答,回 504。
## AnyBot 提供的介面(`remote::handle`)
只有經過帳號伺服器(或同一個網路、用手機密碼登入)才到得了。
| 方法 | 路徑 | 角色 | 說明 |
|---|---|---|---|
| GET | `/api/state?since=` | 手機 | Bot 清單、誰在工作、等核准的事(有變化才回,最多等 20 秒) |
| GET | `/api/bots/<id>?since=` | 手機 | 一個 Bot 的訊息 |
| POST | `/api/bots/<id>/<動作>` | 手機 | `send`、`stop`、`answer`、`read`、`new`、`upload`… |
| GET/POST | `/api/screen/<box\|mac>` | 手機 | 看畫面、點擊打字 |
| POST | `/api/device/run` | device | `{tool, args, who}` → `{text, ok, image}`:在這台跑一個工具 |
| GET | `/api/sync?since=&client=` | mirror | 大腦的變化(見 architecture.md) |
| POST | `/api/sync/edit` | mirror | `{ops, client, seq}` → `{ok, applied}` |
| POST | `/api/sync/act` | mirror | `{bot, action, …}`:send、stop、answer、read、new、regenerate、reply、restore |
| POST | `/api/sync/upload/<bot>?name=` | mirror | 原始檔案內容 → `{path}` |
| GET | `/api/sync/file?path=` | mirror | 大腦上的附件(只限 `attachments/` 裡的檔案) |
| POST | `/api/sync/models` | mirror | `{provider}` → `{models}`:用大腦的金鑰取得模型清單 |
## 長連線的格式(`src/wire.rs`)
WebSocket 的二進位訊息,每則是:
```
[4 位元組,表頭長度,big-endian] [表頭 JSON] [內容的原始位元組]
```
內容不做 base64。`z: true` 表示內容用 deflate 壓過;`z_ok: true` 表示問的人看得懂壓過的回答。
| t | 方向 | 表頭欄位 | 意思 |
|---|---|---|---|
| `hello` | AnyBot → 伺服器 | `brain`、`hands` | 這台在看哪個大腦、是不是手腳(手機和設定畫面用) |
| `job` | 伺服器 → AnyBot | `id, method, path, query, user, role, from, z, z_ok` | 給這台的要求 |
| `reply` | AnyBot → 伺服器 | `id, status, ctype, headers, z` | 回答 job |
| `call` | AnyBot → 伺服器 | `id, to, method, path, query, z` | 要問帳號裡的另一台(只能是 `/api/sync…` 或 `/api/device/run`) |
| `ret` | 伺服器 → AnyBot | `id, status, ctype, z` | call 的回答 |
心跳用 WebSocket 的 ping/pong。連線斷掉時,伺服器把還在等的 job 都回 503「斷線了」,AnyBot 把還在等的 call 都回「和帳號伺服器的連線斷了」。
## 同步的改動(`/api/sync/edit` 的 ops)
| op | 欄位 | 說明 |
|---|---|---|
| `set` | `key, value` | 換掉一個設定(`next_*` 計數只會變大) |
| `patch` | `key, patch` | 清單或對照表逐項修改 |
| `merge` | `key, value` | 搬移時補上大腦沒有的(providers、skills、mcp_servers) |
| `bot_add` | `bot` | 新增 Bot(id 撞到會換) |
| `bot_del` | `id` | 刪除(主要 Bot 不能刪) |
| `bot_set` | `id, key, value` | 改 Bot 的一個欄位 |
| `bot_patch` | `id, key, patch` | 逐項修改 Bot 的清單欄位(例行任務保留大腦記錄的執行時間) |
| `bot_read` | `id` | 標為已讀 |
| `bot_order` | `ids` | Bot 的順序 |
`patch` 的格式:
```json
{"value": …} 換掉整個值
{"map": {"put": {"鍵": 值}, "del": ["鍵"]}} 對照表
{"list": {"put": [項目], "del": [鍵], "order": [鍵]}} 清單;鍵是項目的 id,沒有就用 name,純值就是值本身
```
清單裡的項目沒有 id 或名字、或鍵重複時,送整個值。