AnyBot/docs/architecture.md

131 lines
12 KiB
Markdown
Raw Normal View History

# 技術架構
AnyBot 是一個 Rust 程式(`anybot`),用 egui/eframe 畫介面。同一個執行檔有三種跑法:
| 跑法 | 指令 | 用途 |
|---|---|---|
| 視窗 | `anybot` | 桌面 App |
| 無視窗 | `anybot --serve` | 伺服器、一直開著的電腦、手腳:介面邏輯照跑,只是不畫畫面 |
| 設定 | `anybot setup …` | 終端機裡把這台接到帳號(`src/setup.rs`) |
另一個小程式 `anybot-relay`(`relay/`)是帳號伺服器。
## 程式分層
```
┌─────────────── 介面(src/app.rs、src/app/*)───────────────┐
│ 聊天、側邊欄、設定、電腦、檔案預覽、大腦同步(mirror_ui) │
│ 每個畫面一個 App 方法;狀態全部在 App.state(model::State) │
└───────┬───────────────────────────────┬────────────────────┘
│ 送出訊息 │ 手機/其他 AnyBot 的要求
┌───────▼──────────┐ ┌────────▼───────────────────┐
│ 回合(agent.rs) │ │ 遠端(remote.rs) │
│ 呼叫模型、跑工具、 │ │ 手機 API、裝置連結、大腦同步 │
│ 等你核准、整理摘要 │ │ 長連線(tunnel.rs、wire.rs) │
└───┬─────────┬────┘ └──────────────────────────────┘
│ │
┌───▼───┐ ┌───▼──────────────────────────────────────────────┐
│ 模型 │ │ 工具(tools.rs 為主) │
│ provi- │ │ 檔案與指令、Bot 的電腦(cloudbox.rs)、瀏覽器 │
│ ders.rs│ │ (botbrowser.rs、browser.rs、web.rs)、螢幕操作 │
└────────┘ │ (computer.rs、desktop.rs、axread.rs;Linux:x11.rs、 │
│ wl.rs、portal.rs、desk.rs)、Mac App(macapps.rs)、 │
│ 保險庫(vault*.rs)、MCP(mcp.rs)、記憶(memory.rs) │
└──────────────────────────────────────────────────────┘
```
| 檔案 | 做什麼 |
|---|---|
| `model.rs` | 資料:State、Bot、Message、設定;讀寫 `state.json` |
Bots work like someone who knows the computer: they know each machine, reach what they see, plan, and hand the planned steps to a quick model Measured on the bot's own box (8 tasks x 2, grok-4.7): 14/16 done, 120 steps, 1114 s before; 16/16, 102 steps, 574 s after (model time 1015 -> 471 s). A replayed skill: 49 s -> 15 s. Knowing the machine - facts.rs: 電腦現況 per computer (system, desktop, systemd, sudo, network, package manager, tools, apps, a fixed TZ), read once by a small read-only probe, kept 30 min, in the instructions at the start of a turn. - The box's sudo is the bot's: only what follows sudo is checked against the always-denied list (it was refused outright, while the instructions said it could sudo), and commands with sudo run as the box's root (setuid does nothing under gVisor). - Box image v8 drops ENV TZ=UTC (it overrode /etc/localtime). ANYBOT_BOX_IMAGE / ANYBOT_BOX_VOLUME give a throwaway box its own image and volume, never adopting the real one. - Mac shell sandbox: App Store app CLIs (they carry their own app sandbox) and ps/top run outside it; plain `osascript -e` too, asked the first time even in direct mode, refusing do shell script, terminals, keystrokes, file reads, ObjC and the secret folders. A command the sandbox broke says so, instead of looking like a broken program. Reaching what it sees - The Mac menu bar's status icons are numbered (each app's AXExtrasMenuBar, named from inside for the system's own), computer_use_zoom looks closer at full sharpness. - Numbers stay with their controls from look to look (they were reassigned every look, so a step's second click landed elsewhere); the box's front window is listed first; a control can be named instead of numbered (name "4"). - Web page numbering walks open shadow DOM (Chromium settings, web components). - A box on a Docker host elsewhere retries the next port when the usual one is taken (a retry without it came up with no network). Working like a person - Instructions: pick the route, know what done looks like, one route, check once, say what's stuck instead of faking it with a substitute, ask before cutting the connection. - StepPlan: the bot's model takes the first step, the step after trouble (failed, refused, repeated, nothing changed on screen, the quick model switching routes), one in four, and the final check; a quick model (Bot.step_model, auto: grok-4.20-0309-non-reasoning on xAI) does the planned steps in between, falling back to the bot's model if it can't be used. - Skills keep the steps that worked (with what each acted on, not numbers); replay_skill does them again with no model in between, finding each control again, handing back on a mismatch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uirha5F5gJt93t3yEx3XcS
2026-11-09 01:01:03 +00:00
| `agent.rs` | 一個回合:組系統指示、送模型、收串流、跑工具、核准關卡、長對話摘要;每一步用哪個模型(`StepPlan`:Bot 的模型規劃、出問題時和收尾,快速模型照計畫執行) |
| `facts.rs` | 電腦現況:每台電腦一段唯讀小腳本讀出系統、桌面、systemd、sudo、網路、套件管理、常用指令和 App,30 分鐘快取,回合開始時放進系統指示 |
| `providers.rs` | 各家模型的 API(OpenAI 相容、Anthropic、OpenCode Go…)、串流、重試、看不到圖時的看圖小幫手 |
| `tools.rs` | 檔案、指令、背景指令;指令在 macOS 沙盒裡跑(見「安全」) |
| `policy.rs` | 哪些動作直接做、先問、拒絕 |
| `cloudbox.rs`、`box/` | Bot 自己的電腦:一個 Docker 容器(有桌面、瀏覽器) |
| `computer.rs`、`desktop.rs`、`axread.rs` | 看畫面、滑鼠鍵盤;讀 App 的按鈕清單(agent-desktop,只讀) |
| `vnc.rs`、`mobile/screen.html` | 人看 Bot 的電腦、需要時接手:VNC 代理(經帳號伺服器或同一個網路),noVNC 網頁 |
| `botbrowser.rs`、`browser.rs`、`web.rs` | AnyBot 自己的瀏覽器(Chrome DevTools 協定)、讀你的瀏覽器、抓網頁 |
| `routines.rs`、`triggers.rs` | 例行任務和觸發 |
| `channels.rs` | 訊息管道(Slack、Discord、Telegram) |
| `remote.rs` | 手機 API、帳號登入、裝置連結、大腦同步的請求端 |
| `tunnel.rs`、`wire.rs` | 和帳號伺服器之間的長連線(WebSocket) |
| `app/mirror_ui.rs` | 大腦和螢幕之間的同步 |
| `setup.rs` | `anybot setup` |
| `httpd.rs` | 很小的 HTTP 伺服器(AnyBot 和帳號伺服器共用) |
| `mobile/index.html` | 手機網頁(單一檔案,編進程式裡) |
## 資料存放
都在資料資料夾裡(macOS `~/Library/Application Support/AnyBot/`、Linux `~/.local/share/AnyBot/`、Windows `%APPDATA%\AnyBot\`;`ANYBOT_DATA_DIR` 可以改):
| 檔案 | 內容 |
|---|---|
| `state.json` | 全部的 Bot、聊天、設定和金鑰 |
| `attachments/bot-<id>/` | 聊天附件 |
| `browser/` | AnyBot 瀏覽器的設定檔(登入狀態) |
| `brain-copy.json` | 螢幕電腦保留的大腦副本(離線時還能看) |
| `brain-files/` | 螢幕電腦從大腦下載的附件 |
| `image-notes.json` | 看圖小幫手對每張圖的描述(不用重看) |
A turn cut off by a crash leaves a 「繼續」 card, and nothing it may have done runs twice Each bot keeps a work log (oplog/bot-<id>.jsonl, after pi's durable-harness design, cut down): before something happens a line says it is about to — the turn, each model call, each step once its checks passed (with the arguments it runs with and whether it may run again), each message typed mid-turn — synced to disk first. When the turn ends the file shrinks to the messages still waiting, usually nothing. On the next start after AnyBot was killed mid-turn, the log puts the chat right before anything runs: the message the turn answered comes back if the chat lost it; a step that was running and reaches outside (a text, a click, a file, a published site) shows as 「可能已經執行了」, both to the person and in the result the bot reads — no longer as "not allowed"; a step that only looked shows as cut off; messages typed mid-turn that never reached the bot go back into the chat. The chat is saved, then the log closes the turn. A turn the person didn't stop ends with a 「繼續」 card (what it was doing, what may have run, the messages put back); it goes on only when they press it — in the window, on a screen of the brain, or on the phone — from where its steps stand, never running a step that may have run again. Pressing stop is on disk at once, and a stopped turn stays stopped. Also in the turn loop: - 自動審核 that doesn't answer (or answers something that isn't a verdict) asks the person; in 直接操作 mode it used to let the step through unchecked. - "Too long for this model" folds older messages into a summary and asks the same step again, once per thing the person said, instead of ending the turn. - Folding keeps the person's chat whole: the summary goes in at the fold and the model reads from the newest summary on (it used to delete what it folded). - A step moved to the chosen computer runs there, but the chat keeps the call as the model wrote it; the result says where it ran. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uirha5F5gJt93t3yEx3XcS
2026-11-08 16:49:12 +00:00
| `oplog/bot-<id>.jsonl` | 每個 Bot 的工作紀錄:回合開始、每次問模型、每個步驟開始和結束、做事時傳的訊息。每行先寫進磁碟才做那件事;AnyBot 在做事時被關掉,下次打開靠它把聊天修好,並留一張「繼續」卡片(`src/oplog.rs`)。回合結束就清空 |
| `anybot.lock` | 同一個資料夾一次只給一個 AnyBot 用(視窗、`--serve`、`--ask`、`anybot setup` 寫設定時)。是系統的檔案鎖,程式結束(包括當掉)就放開(`src/lock.rs`) |
寫入時先寫暫存檔再改名,不會寫一半。
## 安全
- **核准關卡**:工具動作照規則直接做、先問或拒絕(`policy.rs`,FEATURES.md 第 16、19 節)。「直接操作模式」可以跳過詢問。
Bots work like someone who knows the computer: they know each machine, reach what they see, plan, and hand the planned steps to a quick model Measured on the bot's own box (8 tasks x 2, grok-4.7): 14/16 done, 120 steps, 1114 s before; 16/16, 102 steps, 574 s after (model time 1015 -> 471 s). A replayed skill: 49 s -> 15 s. Knowing the machine - facts.rs: 電腦現況 per computer (system, desktop, systemd, sudo, network, package manager, tools, apps, a fixed TZ), read once by a small read-only probe, kept 30 min, in the instructions at the start of a turn. - The box's sudo is the bot's: only what follows sudo is checked against the always-denied list (it was refused outright, while the instructions said it could sudo), and commands with sudo run as the box's root (setuid does nothing under gVisor). - Box image v8 drops ENV TZ=UTC (it overrode /etc/localtime). ANYBOT_BOX_IMAGE / ANYBOT_BOX_VOLUME give a throwaway box its own image and volume, never adopting the real one. - Mac shell sandbox: App Store app CLIs (they carry their own app sandbox) and ps/top run outside it; plain `osascript -e` too, asked the first time even in direct mode, refusing do shell script, terminals, keystrokes, file reads, ObjC and the secret folders. A command the sandbox broke says so, instead of looking like a broken program. Reaching what it sees - The Mac menu bar's status icons are numbered (each app's AXExtrasMenuBar, named from inside for the system's own), computer_use_zoom looks closer at full sharpness. - Numbers stay with their controls from look to look (they were reassigned every look, so a step's second click landed elsewhere); the box's front window is listed first; a control can be named instead of numbered (name "4"). - Web page numbering walks open shadow DOM (Chromium settings, web components). - A box on a Docker host elsewhere retries the next port when the usual one is taken (a retry without it came up with no network). Working like a person - Instructions: pick the route, know what done looks like, one route, check once, say what's stuck instead of faking it with a substitute, ask before cutting the connection. - StepPlan: the bot's model takes the first step, the step after trouble (failed, refused, repeated, nothing changed on screen, the quick model switching routes), one in four, and the final check; a quick model (Bot.step_model, auto: grok-4.20-0309-non-reasoning on xAI) does the planned steps in between, falling back to the bot's model if it can't be used. - Skills keep the steps that worked (with what each acted on, not numbers); replay_skill does them again with no model in between, finding each control again, handing back on a mismatch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uirha5F5gJt93t3yEx3XcS
2026-11-09 01:01:03 +00:00
- **指令的沙盒(macOS)**:Bot 執行的指令在 `sandbox-exec` 裡跑,讀不到也寫不到 AnyBot 的資料資料夾、`.gnupg`、鑰匙圈、`.aws` 等;只有 `attachments/` 和 `.ssh` 可以讀(都不能寫,`.ssh` 可讀讓 git 和 ssh 連得動)。只有沙盒管得到的才是祕密防線,核准層不再逐字掃路徑。例外(`tools::runs_outside_sandbox`,只在你的電腦上):App Store 版 App 的指令列(自帶 App 沙盒,在這裡會兩層衝突而當掉)、`ps`/`top`(setuid,沙盒裡不能跑)、單獨一條且不含危險寫法的 `osascript -e`(沙盒裡指揮別的 App 一律 -10004;第一次先問,直接操作模式也問,`osascript:outside` 授權後不再問)。AppleScript 在沙盒外沒有東西擋它讀檔,所以靠寫法過濾加第一次詢問,不是密不透風。環境變數裡的模型金鑰會先拿掉,PATH 會補齊 Homebrew 等位置(從 Finder 或 launchd 啟動也要找得到指令)。
- **Bot 的電腦**:放在容器裡,和你的電腦分開。標準(runc)容器和主機共用 Linux 核心,Chromium 在裡面關掉自己的沙盒,容器就是唯一那道牆。Docker 主機裝了 gVisor 時,設定 › 電腦 › 容器隔離可改「強隔離(gVisor)」:容器有自己的虛擬核心,Chromium 的沙盒重新開啟,牆變成兩道(虛擬核心+瀏覽器沙盒)。改完要按電腦的「更新」重建才生效。gVisor 裡 setuid 不起作用,bot 使用者用 sudo 會失敗(「effective uid is not 0」),所以含 sudo 的指令改用容器裡的 root 執行(`cloudbox::exec_wrapper`),跑完把它在 bot 家目錄留下的檔案還給 bot。在 macOS 上沒有能裝 gVisor 的 Docker,用 `deploy/mac/gvisor-vm.sh` 建一台自帶 Docker+gVisor 的 Lima 虛擬機,再把 Docker 主機指過去;那台虛擬機沒開時(例如 Mac 重開機後)AnyBot 會自己開。
- **保險庫**:密碼、卡號、CVC 不會交給模型,由 AnyBot 直接填進網頁。
- **多台電腦**:金鑰只留在大腦(下面說明);手腳只接受帳號裡的 AnyBot,用配對碼加入的只接受那一個大腦。
## 多台電腦怎麼運作
### 連線
每台 AnyBot(大腦、螢幕、手腳)都保持一條 WebSocket 連到帳號伺服器(`/relay/host/ws`),雙方每 15–20 秒互相確認,45–60 秒沒聲音就重連。帳號伺服器沒有 WebSocket 時(舊版),改用 HTTP 長輪詢(`/relay/host/next`)。
- 手機的要求:手機 → 帳號伺服器(HTTPS)→ 這條連線 → AnyBot → 回覆原路回去。
- AnyBot 找另一台 AnyBot(同步、手腳):從自己的連線送 `call` 上去,伺服器轉給對方的連線,答案以 `ret` 回來。
- 連線斷掉時,等待中的請求立刻收到「斷線了」,不用等逾時;伺服器也立刻知道這台離線。
- 超過 1 KB、而且壓得小的內容用 deflate 壓縮(圖片、PDF 這類已壓縮的照原樣送)。
格式見 [protocol.md](protocol.md)。
### 大腦和螢幕的同步(`app/mirror_ui.rs`)
大腦回答「從版本 N 之後改了什麼」:
- **設定**:State 去掉聊天、拿掉金鑰後算指紋,變了才送整份。
- **聊天**:每則訊息有指紋,只送第一個不同的那則之後的部分。
- **討論串**、**誰在工作**、**等你核准的事**。
螢幕的請求會等最多 20 秒,有變化就馬上回(長輪詢,跑在長連線上)。大腦上的改動通常 1 秒內出現在螢幕上。
螢幕上的改動變成小操作送回大腦:
- 單一值(`set`、`bot_set`);
- 清單和對照表逐項修改(`patch`、`bot_patch`:新增或修改某幾項、刪掉某幾項、改順序),所以兩台同時改同一份清單的不同項目,兩邊都會保留;
- 新增、刪除、排序 Bot(`bot_add`、`bot_del`、`bot_order`)。
每個改動有編號,大腦回報收到第幾號;還沒確認的改動,會疊在大腦傳回來的新設定上,打字不會被蓋回去。所有改動和聊天動作由一個工作排隊依序送出。送失敗時,螢幕重新要一份完整資料。
聊天動作(傳訊息、停止、核准、重新產生、開新對話…)送 `act`,由大腦執行。附件先上傳到大腦。
### 金鑰只在大腦
送給螢幕的設定裡,模型金鑰、訊息管道的 Token、連接器的環境變數和標頭都換成「••••(已設定,在大腦上)」。螢幕改設定送回來時,大腦看到這個替代字,就保留原本的值(清單裡依 id 或名字對上)。螢幕要取得模型清單時,由大腦用自己的金鑰去抓。
### 手腳
大腦的 Bot 呼叫工具時可以指定 `computer`。如果是帳號裡的另一台,就透過長連線送 `/api/device/run` 給它,由它在自己身上執行(螢幕、檔案、指令、它的 AnyBot 瀏覽器),再把文字和截圖傳回來。那台要開「裝置連結」才接受;若設了只給某個大腦用(`hands_for`),帳號伺服器會附上是哪一台在問,不是那台就拒絕。指令在那台的工作資料夾(Bot 的 `workdir`)跑;連線經過帳號伺服器只有 35 秒,所以手腳上的指令超過 25 秒就移到背景,用同一台的 `read_terminal` 追蹤(computer 填那台)。
### 配對碼
已登入的手機或 AnyBot 向帳號伺服器要一組 6 位數字(10 分鐘、一次)。新電腦用這組數字換到自己的身分(主機 id 和金鑰),不經過密碼。配對碼記得它是給哪個大腦的,新電腦就只讓那個大腦使用。
### 看圖小幫手
模型拒收圖片時,AnyBot 記下這個模型看不到圖。之後送給它的請求裡,你傳的每張圖會先交給你帳號裡看得到圖的模型(依序試 xAI、Anthropic、OpenAI、Gemini、OpenRouter…,先用你設的預設模型),把描述(包括圖裡的文字)放進訊息。描述依圖片記住,同一張圖不會再看一次;失敗的模型 10 分鐘內跳過。