From 472a004f87ebea0e741fe4662f118a63683f67ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E6=80=A7=E9=A9=8A?= Date: Thu, 5 Nov 2026 00:52:10 +0800 Subject: [PATCH] One-step install: install.sh, fixed Linux and server builds, clearer README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - install.sh (curl | sh): app, hands, server and deps modes; installs system packages (right names per release, e.g. libasound2t64 on Ubuntu 24.04), Rust via rustup (1.95+), Docker for the server; limits parallel compiles on small machines - Server Docker image: add the ALSA build/runtime packages (the build failed), cache the cargo registry and target between rebuilds, JOBS build arg - 加一台電腦 now gives a line that installs AnyBot on a blank computer before joining it - Makefile: make deps installs Rust too, make doctor, clear message when Rust is missing/old - README rewritten: one line per use, getting started, update/remove, troubleshooting, deployment options; docs/install.md and multi-device.md follow it - Tested on fresh ubuntu:24.04 (app) and debian:12 (curl | sh hands) containers Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa --- Cargo.toml | 2 + Makefile | 46 +++-- README.md | 181 +++++++++++++----- deploy/linux/install.sh | 22 +-- deploy/server/Dockerfile | 16 +- deploy/server/docker-compose.yml | 2 + deploy/server/setup.sh | 19 +- docs/install.md | 68 +++++-- docs/multi-device.md | 25 ++- install.sh | 315 +++++++++++++++++++++++++++++++ mobile/index.html | 6 +- relay/Dockerfile | 7 +- src/app/remote_ui.rs | 4 +- src/remote.rs | 9 + src/setup.rs | 21 ++- 15 files changed, 613 insertions(+), 130 deletions(-) create mode 100755 install.sh diff --git a/Cargo.toml b/Cargo.toml index 315b011..10dba24 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,8 @@ name = "anybot" version = "0.1.0" edition = "2021" +# Older Rust stops with a clear message instead of odd errors deep in a dependency. +rust-version = "1.95" [[bin]] name = "anybot" diff --git a/Makefile b/Makefile index 94bee1e..e40ee34 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,14 @@ # AnyBot — build and install from source. make help lists everything. # -# git clone git@git.30cm.net:daniel.w/AnyBot.git && cd AnyBot -# make build (release) -# make install install for you: ~/Applications/AnyBot.app on a Mac, ~/.local/bin on Linux +# git clone https://code.30cm.net/daniel.w/AnyBot.git && cd AnyBot +# make deps everything the build needs (system packages, Rust) — once +# make install build and install: ~/Applications/AnyBot.app on a Mac, ~/.local/bin on Linux # make run open it without installing +# +# The one-step version of all of this is ./install.sh (README.md). +# Rust installed by rustup lives in ~/.cargo/bin, which a fresh shell may not have on PATH yet. +export PATH := $(HOME)/.cargo/bin:$(PATH) CARGO ?= cargo UNAME := $(shell uname -s) VERSION ?= $(shell git describe --tags --always 2>/dev/null | sed 's/^v//' || echo dev) @@ -13,7 +17,7 @@ APPDIR ?= $(HOME)/Applications BIN := $(TARGET)/release/anybot RELAY := $(if $(CARGO_TARGET_DIR),$(CARGO_TARGET_DIR),relay/target)/release/anybot-relay -.PHONY: all build app relay run serve setup relay-run test deps install uninstall hands server dist clean help +.PHONY: all build app relay run serve setup relay-run test deps doctor install uninstall hands server dist clean help check-rust all: build @@ -24,7 +28,8 @@ help: @echo " make run 直接打開 AnyBot(不安裝)" @echo " make install 安裝:Mac → $(APPDIR)/AnyBot.app;Linux → ~/.local/bin/anybot 和選單" @echo " make uninstall 移除安裝的程式(你的 Bot 和聊天不會刪)" - @echo " make deps Linux:安裝系統套件(會問 sudo 密碼)" + @echo " make deps 裝好建置需要的東西:系統套件和 Rust(Linux 會問 sudo 密碼)" + @echo " make doctor 檢查這台缺什麼" @echo " make test 跑測試" @echo "" @echo " make serve 不開視窗執行(伺服器、一直開著的電腦)" @@ -38,10 +43,14 @@ help: build: app relay -app: +check-rust: + @command -v $(CARGO) >/dev/null 2>&1 || { echo; echo " ✗ 找不到 Rust(cargo)。先執行:make deps"; echo; exit 1; } + @v=$$(rustc --version | sed -n 's/^rustc 1\.\([0-9]*\).*/\1/p'); [ "$${v:-0}" -ge 95 ] || { echo; echo " ✗ Rust 太舊($$(rustc --version)),要 1.95 以上。執行:rustup update stable(或 make deps)"; echo; exit 1; } + +app: check-rust $(CARGO) build --release --bin anybot -relay: +relay: check-rust cd relay && $(CARGO) build --release run: app @@ -56,19 +65,26 @@ setup: app relay-run: relay $(RELAY) --port 8080 --data relay-data -test: +test: check-rust $(CARGO) test --bin anybot cd relay && $(CARGO) test deps: + sh ./install.sh deps + +doctor: + @echo "AnyBot $(VERSION) · $(UNAME) $$(uname -m)" + @command -v git >/dev/null && echo " ✓ git" || echo " ✗ git:make deps" + @command -v cc >/dev/null && echo " ✓ C 編譯器" || echo " ✗ C 編譯器:make deps" + @command -v $(CARGO) >/dev/null && echo " ✓ $$(rustc --version)" || echo " ✗ Rust:make deps" ifeq ($(UNAME),Linux) - sh deploy/linux/install.sh --deps-only -else ifeq ($(UNAME),Darwin) - @xcode-select -p >/dev/null 2>&1 || xcode-select --install - @command -v cargo >/dev/null || echo "需要 Rust:curl --proto '=https' -sSf https://sh.rustup.rs | sh" -else - @echo "Windows:請照 docs/install.md 安裝 Rust 和 Visual Studio Build Tools" + @pkg-config --exists alsa 2>/dev/null && echo " ✓ ALSA(libasound2-dev)" || echo " ✗ ALSA 開發套件:make deps" + @command -v docker >/dev/null && echo " ✓ $$(docker --version)" || echo " · 沒有 Docker(只有伺服器和 Bot 自己的電腦需要;make server 會自動裝)" endif +ifeq ($(UNAME),Darwin) + @xcode-select -p >/dev/null 2>&1 && echo " ✓ Xcode Command Line Tools" || echo " ✗ Xcode Command Line Tools:make deps" +endif + @test -x $(BIN) && echo " ✓ 已建置 $(BIN)" || echo " · 還沒建置:make" install: app ifeq ($(UNAME),Darwin) @@ -94,7 +110,7 @@ hands: app sh deploy/linux/hands.sh --deps server: - cd deploy/server && ./setup.sh + sh ./install.sh server dist: build ifeq ($(UNAME),Darwin) diff --git a/README.md b/README.md index 13b81e0..e309902 100644 --- a/README.md +++ b/README.md @@ -1,74 +1,167 @@ # AnyBot -你自己的 AI Bot:可以聊天,也能在它自己的電腦和你的電腦上幫你做事(檔案、指令、瀏覽器、螢幕操作、例行任務)。每個 Bot 可以用不同的 AI 服務:xAI Grok、OpenAI、Claude、Gemini、OpenRouter、OpenCode Go、Ollama(本機),或任何 OpenAI 相容的服務。 +你自己的 AI Bot。可以聊天,也能幫你動手做事:處理檔案、執行指令、操作瀏覽器和螢幕、跑例行任務。 -- 桌面 App:macOS、Linux(Windows 可以編譯,還沒有安裝包) -- 手機:網頁版,可加到主畫面、收推播通知 -- 多台電腦:一台當**大腦**(保存 Bot、聊天、記憶和排程),其他電腦當**手腳**或**螢幕** -- 用 Rust 和 egui 寫成,一個執行檔就是全部(視窗、無視窗伺服器模式、設定工具) +- 每個 Bot 可以用不同的 AI:xAI Grok、OpenAI、Claude、Gemini、OpenRouter、OpenCode Go、Ollama(本機),或任何 OpenAI 相容的服務 +- 桌面 App(macOS、Linux)+手機網頁版(可加到主畫面、收通知) +- 多台電腦一起用:一台當**大腦**,其他電腦當它的**手腳** -## 從原始碼安裝 +--- -```sh -git clone git@git.30cm.net:daniel.w/AnyBot.git -cd AnyBot -make deps # 只有 Linux 需要:安裝系統套件(會問 sudo 密碼) -make # 建置 -make install # Mac → ~/Applications/AnyBot.app;Linux → ~/.local/bin/anybot 和選單 +## 安裝 + +先決定你要裝哪一種,複製那一行到終端機執行就好。需要的東西(系統套件、Rust、Docker)腳本會自己裝。 + +| 我要… | 在哪台執行 | 指令 | +|---|---|---| +| **桌面 App** | Mac、有螢幕的 Linux | `curl -fsSL https://code.30cm.net/daniel.w/AnyBot/raw/branch/main/install.sh \| sh` | +| **架伺服器**(帳號伺服器+大腦+HTTPS) | 有網域的 Linux 主機 | `curl -fsSL https://code.30cm.net/daniel.w/AnyBot/raw/branch/main/install.sh \| sh -s -- server` | +| **加一台電腦當手腳** | 任何 Linux(有沒有螢幕都可以) | 在 AnyBot 按「加一台電腦」,貼上它給你的那一行 | + +> 想先看腳本做什麼:[install.sh](install.sh)。它只會裝 AnyBot 需要的套件,程式裝在你的使用者資料夾(不用 root),原始碼放在 `~/AnyBot`。 + +**第一次要多久?** 腳本會在你的電腦上編譯,第一次大約 5–15 分鐘(看電腦快慢),之後更新只要 1–2 分鐘。 + +--- + +## 開始使用 + +### 只用一台電腦 + +1. 裝好桌面 App,打開。 +2. 設定 › 模型,貼上你的 API 金鑰(例如 xAI、OpenAI)。 +3. 開始聊天。 + +### 手機和多台電腦一起用 + +``` + 手機(網頁) 筆電(螢幕) 沒螢幕的 Linux(手腳) + \ | / + └──────── 帳號伺服器(每台都連過來)─────────┘ + | + 大腦:Bot、聊天、記憶、例行任務都在這裡 ``` -需要 Rust(`curl --proto '=https' -sSf https://sh.rustup.rs | sh`)。Mac 另外需要 Xcode Command Line Tools(`make deps` 會提示)。詳細步驟、各平台需求和常見問題:[docs/install.md](docs/install.md)。 +**步驟 1:架伺服器**(只做一次) -常用指令(`make help` 列出全部): +需要:一台 Linux 主機(2 GB 以上記憶體)、一個網域,網域的 A 紀錄要指到這台主機,80 和 443 埠要打開。 + +```sh +curl -fsSL https://code.30cm.net/daniel.w/AnyBot/raw/branch/main/install.sh | sh -s -- server +``` + +只會問三件事:**網域、帳號、密碼**。它會自動裝 Docker、申請 HTTPS 憑證、建立帳號、啟動大腦。完成畫面會告訴你網址。 + +> 沒有伺服器?也可以先拿一台 Mac 當大腦,見 [docs/multi-device.md](docs/multi-device.md)(做法 B)。 + +**步驟 2:手機**:用瀏覽器打開你的網址並登入,再「加入主畫面」。 + +**步驟 3:其他電腦** + +- **有螢幕的電腦**:裝好桌面 App,到 設定 › 一般 › 手機與帳號 登入,在「大腦」那一欄選伺服器。 +- **沒螢幕的 Linux**:在手機 AnyBot 右上角按「⋯ › 加一台電腦」(或在電腦的 設定 › 一般 › 手機與帳號),會出現一行像這樣的指令: + + ```sh + curl -fsSL https://code.30cm.net/daniel.w/AnyBot/raw/branch/main/install.sh | sh -s -- hands https://你的網域 482913 + ``` + + 貼到那台電腦執行,就會自動裝好、接上大腦,之後開機也會自動執行。不用帳號密碼,這組碼 10 分鐘內有效、只能用一次。 + +--- + +## 更新和移除 + +| 要做什麼 | 指令 | +|---|---| +| 更新桌面 App 或手腳 | 再執行一次安裝的那一行 | +| 更新伺服器 | 在伺服器上 `cd ~/AnyBot && make server`,選「用現有設定,更新並重新啟動」 | +| 移除程式 | `cd ~/AnyBot && make uninstall`(你的 Bot 和聊天會留著) | +| 停掉手腳 | `systemctl --user disable --now anybot` | + +--- + +## 出問題時 + +先執行這個,它會告訴你缺什麼: + +```sh +cd ~/AnyBot && make doctor +``` + +| 看到 | 怎麼辦 | +|---|---| +| 編譯到一半被中斷、`Killed` 或 `signal: 9` | 記憶體不夠。加 2 GB swap 再試:`sudo fallocate -l 2G /swapfile && sudo chmod 600 /swapfile && sudo mkswap /swapfile && sudo swapon /swapfile` | +| `Rust 太舊` 或 `requires rustc 1.95` | `rustup update stable`。用 apt 裝的 rustc 太舊,請改用 rustup | +| `alsa` 或 `pkg-config` 找不到 | 少了系統套件,執行 `make deps` | +| 伺服器的網址連不到 | 網域是不是指到這台?80、443 埠有沒有被防火牆擋?`cd ~/AnyBot/deploy/server && docker compose logs caddy` | +| 大腦登不進帳號 | `cd ~/AnyBot/deploy/server && docker compose logs anybot` | +| Mac 說「無法驗證開發者」 | 對 AnyBot 按右鍵 › 打開,只有第一次需要 | +| 手機上看到「大腦目前離線」 | 大腦那台關機、睡眠,或 AnyBot 沒開。詳見 [docs/multi-device.md](docs/multi-device.md) 的「連不上時」 | + +還是不行:把 `make doctor` 的結果和錯誤訊息的最後幾行貼給維護的人。 + +--- + +## 自己從原始碼建置 + +適合想改程式的人。一般使用請用上面的一行安裝。 + +```sh +git clone https://code.30cm.net/daniel.w/AnyBot.git +cd AnyBot +make deps # 裝好系統套件和 Rust(只要一次;Linux 會問 sudo 密碼) +make # 建置 +make install # 安裝:Mac → ~/Applications/AnyBot.app;Linux → ~/.local/bin/anybot 和應用程式選單 +``` | 指令 | 做什麼 | |---|---| | `make run` | 不安裝,直接打開 | | `make test` | 跑測試 | -| `make serve` | 不開視窗執行(伺服器、一直開著的電腦) | -| `make setup` | 把這台接到你的 AnyBot(終端機裡的引導畫面) | -| `make server` | 在伺服器上架好帳號伺服器、大腦和 HTTPS(Docker) | +| `make doctor` | 檢查這台缺什麼 | +| `make serve` | 不開視窗執行(一直開著的電腦) | +| `make setup` | 把這台接到你的大腦(終端機裡的引導畫面) | +| `make server` | 在這台架伺服器(同上面的 `install.sh server`) | | `make dist` | 打包發行檔到 `dist/` | -| `make uninstall` | 移除程式(Bot 和聊天不會刪) | +| `make help` | 列出全部 | -## 多台電腦一起用 +需要:Rust 1.95 以上、C 編譯器、git;Linux 另外需要 `pkg-config` 和 ALSA 開發套件(`libasound2-dev`)。`make deps` 會全部裝好。各平台細節:[docs/install.md](docs/install.md)。 -``` - 手機(網頁) 筆電(螢幕) 沒螢幕的 Linux(手腳) - \ | / - └──── 帳號伺服器(一條一直開著的連線)────┘ - | - 大腦:Bot、聊天、記憶、排程 -``` +--- -1. **架帳號伺服器和大腦**:在伺服器上執行 `make server`(只問網域、帳號、密碼);或先用一台 Mac 當大腦([docs/multi-device.md](docs/multi-device.md))。 -2. **其他電腦當螢幕**:打開 AnyBot › 設定 › 手機與帳號,登入同一個帳號,在「大腦」選它。 -3. **加一台手腳**:手機 AnyBot 右上「⋯ › 加一台電腦」,把出現的那一行貼到那台的終端機: - ```sh - anybot setup https://anybot.example.com 469615 - ``` - 不用帳號密碼,會設成開機自動執行;沒有螢幕就開一個虛擬螢幕。 +## 部署方式 + +| 方式 | 用在 | 說明 | +|---|---|---| +| **Docker Compose**(預設) | 伺服器 | `install.sh server` 用的就是它:帳號伺服器、大腦、Caddy(自動 HTTPS)三個容器,資料放在 Docker 磁碟區,重開機自動啟動。設定檔:[deploy/server/docker-compose.yml](deploy/server/docker-compose.yml) | +| Portainer、Coolify、Dokploy… | 已經在用這些管理工具的伺服器 | 它們都吃標準的 `docker-compose.yml`:把 repo 加進去、指定 `deploy/server/docker-compose.yml`、在環境變數填 `ANYBOT_USER`、`ANYBOT_PASSWORD`、`ANYBOT_RELAY_PUBLIC_URL`。若工具自己處理 HTTPS,就拿掉 `caddy` 那一段,把網域導到 `relay` 的 8080 埠 | +| **systemd**(使用者服務) | 手腳、沒螢幕的 Linux 大腦 | `anybot setup` 會自動建立 `anybot.service`;登出後也要繼續跑:`sudo loginctl enable-linger $USER` | +| **launchd** | Mac 當大腦或帳號伺服器 | 見 [docs/multi-device.md](docs/multi-device.md) | +| Gitea Actions | 推 tag 自動打包發行檔 | 見 [docs/release.md](docs/release.md) | + +不需要另外架資料庫:資料是幾個 JSON 檔,寫入時不會寫一半。要備份伺服器,備份 Docker 磁碟區 `server_relay-data`、`server_anybot-data` 就好。 + +--- ## 文件 | 文件 | 內容 | |---|---| -| [docs/install.md](docs/install.md) | 從原始碼建置和安裝(macOS、Linux、Windows)、更新、移除、常見問題 | +| [docs/install.md](docs/install.md) | 安裝細節:各平台需求、手動步驟、常見問題 | | [docs/multi-device.md](docs/multi-device.md) | 大腦、手腳、螢幕:怎麼架、怎麼接、連不上時看什麼 | -| [docs/architecture.md](docs/architecture.md) | 技術架構:程式分層、資料存放、安全設計、同步怎麼運作 | -| [docs/protocol.md](docs/protocol.md) | 帳號伺服器和 AnyBot 之間的介面與長連線格式 | -| [docs/release.md](docs/release.md) | 發行:打包、Gitea Actions 自動建置、runner 設定 | -| [FEATURES.md](FEATURES.md) | 所有功能的完整說明 | -| [relay/README.md](relay/README.md) | 帳號伺服器的架設細節 | +| [docs/architecture.md](docs/architecture.md) | 技術架構:程式分層、資料存放、安全設計 | +| [docs/protocol.md](docs/protocol.md) | 帳號伺服器的介面與長連線格式 | +| [docs/release.md](docs/release.md) | 打包和自動建置 | +| [FEATURES.md](FEATURES.md) | 所有功能 | ## 專案結構 ``` -src/ AnyBot(桌面 App、無視窗模式、anybot setup) - app/ 視窗介面(聊天、設定、電腦、大腦同步的畫面) -relay/ 帳號伺服器(獨立的小程式) -mobile/ 手機網頁(由 AnyBot 和帳號伺服器提供) +install.sh 一鍵安裝(桌面 App、手腳、伺服器) +src/ AnyBot:桌面 App、無視窗模式、anybot setup +relay/ 帳號伺服器 +mobile/ 手機網頁 box/ Bot 自己的電腦(Docker 容器) -deploy/ 安裝與部署:mac/、linux/、server/、ci/ +deploy/ 各平台的安裝和部署檔:mac/、linux/、server/、ci/ docs/ 技術文件 ``` diff --git a/deploy/linux/install.sh b/deploy/linux/install.sh index 7690147..6716a47 100755 --- a/deploy/linux/install.sh +++ b/deploy/linux/install.sh @@ -22,26 +22,16 @@ DEPS_FEDORA="alsa-lib libxkbcommon-x11 mesa-libGL xdg-utils libnotify libsecret DEPS_ARCH="alsa-lib libxkbcommon-x11 mesa xdg-utils libnotify libsecret espeak-ng zenity xclip wl-clipboard ffmpeg docker noto-fonts-cjk" BUILD_DEBIAN="build-essential pkg-config libasound2-dev" -if [ "$1" = "--deps" ] || [ "$1" = "--deps-only" ]; then - if command -v apt-get >/dev/null; then - sudo apt-get update && sudo apt-get install -y $DEPS_DEBIAN $BUILD_DEBIAN - elif command -v dnf >/dev/null; then - sudo dnf install -y $DEPS_FEDORA gcc pkgconf alsa-lib-devel - elif command -v pacman >/dev/null; then - sudo pacman -S --needed $DEPS_ARCH base-devel - else - echo "Install these with your package manager: $DEPS_DEBIAN" - fi - if command -v docker >/dev/null && ! id -nG | grep -qw docker; then - echo "Adding you to the docker group (the bot's computer needs it); log out and back in afterwards." - sudo usermod -aG docker "$USER" - fi +# The package lists above are for reference; ../../install.sh installs them (it picks the right +# names per release, e.g. libasound2t64 on Ubuntu 24.04, and installs Rust when it's missing). +if [ "${1:-}" = "--deps" ] || [ "${1:-}" = "--deps-only" ]; then + sh ./install.sh deps fi -[ "$1" = "--deps-only" ] && exit 0 +[ "${1:-}" = "--deps-only" ] && exit 0 OUT="${CARGO_TARGET_DIR:-target}/release/anybot" -if [ -x "$OUT" ] && [ "$1" != "--rebuild" ]; then +if [ -x "$OUT" ] && [ "${1:-}" != "--rebuild" ]; then echo "Using the existing build ($OUT)." else command -v cargo >/dev/null || { echo "Rust is needed to build: https://rustup.rs"; exit 1; } diff --git a/deploy/server/Dockerfile b/deploy/server/Dockerfile index 8d1ec1a..005b71d 100644 --- a/deploy/server/Dockerfile +++ b/deploy/server/Dockerfile @@ -1,16 +1,24 @@ # AnyBot without a window (`anybot --serve`) on a Linux server: the bots, routines, triggers and # channels run around the clock; the person uses the phone. The bot's own computer is a sibling # container started through the host's Docker (the socket is mounted in docker-compose.yml). -# Build from the app/ folder: docker build -f deploy/server/Dockerfile -t anybot-serve . +# Build from the repository root: docker build -f deploy/server/Dockerfile -t anybot-serve . +# JOBS limits parallel compiles on small servers (setup.sh sets it from the memory). FROM rust:1-bookworm AS build +RUN apt-get update && apt-get install -y --no-install-recommends pkg-config libasound2-dev \ + && rm -rf /var/lib/apt/lists/* +ARG JOBS= WORKDIR /src COPY . . -RUN cargo build --release --bin anybot +# The registry and target caches survive rebuilds, so an update only recompiles what changed. +RUN --mount=type=cache,target=/usr/local/cargo/registry \ + --mount=type=cache,target=/src/target \ + cargo build --release --bin anybot ${JOBS:+-j $JOBS} \ + && cp target/release/anybot /anybot FROM debian:bookworm-slim -RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl git ripgrep poppler-utils docker.io openssh-client \ +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl git ripgrep poppler-utils docker.io openssh-client libasound2 \ && rm -rf /var/lib/apt/lists/* -COPY --from=build /src/target/release/anybot /usr/local/bin/anybot +COPY --from=build /anybot /usr/local/bin/anybot ENV ANYBOT_DATA_DIR=/data VOLUME /data CMD ["anybot", "--serve"] diff --git a/deploy/server/docker-compose.yml b/deploy/server/docker-compose.yml index 865d4bb..f825b76 100644 --- a/deploy/server/docker-compose.yml +++ b/deploy/server/docker-compose.yml @@ -18,6 +18,7 @@ services: build: context: ../.. dockerfile: relay/Dockerfile + args: { JOBS: "${ANYBOT_BUILD_JOBS:-}" } restart: unless-stopped environment: # Your public address (for the links of sites the bots publish), e.g. https://anybot.example.com @@ -38,6 +39,7 @@ services: build: context: ../.. dockerfile: deploy/server/Dockerfile + args: { JOBS: "${ANYBOT_BUILD_JOBS:-}" } restart: unless-stopped depends_on: [relay] environment: diff --git a/deploy/server/setup.sh b/deploy/server/setup.sh index 5231fa2..3f065ca 100755 --- a/deploy/server/setup.sh +++ b/deploy/server/setup.sh @@ -71,7 +71,7 @@ choose() { card_open "$title" for i in "${!opts[@]}"; do local name=${opts[$i]%%|*} desc=${opts[$i]#*|} - if [ "$i" -eq "$sel" ]; then line "$A❯ ● $B$name$N"; else line " $D○$N $name"; fi + if [ "$i" -eq "$sel" ]; then line "${A}❯ ● $B$name$N"; else line " ${D}○$N $name"; fi [ "$desc" != "${opts[$i]}" ] && [ -n "$desc" ] && note " $desc" line "" done @@ -153,12 +153,12 @@ if [ "${SKIP:-0}" != 1 ]; then ips=$(getent ahosts "$DOMAIN" 2>/dev/null | awk '{print $1}' | sort -u | tr '\n' ' ') if [ -z "$ips" ]; then card_close - choose GO "「$DOMAIN」還沒有指到任何位址" "重新輸入|" "仍然繼續|DNS 剛設好、還沒生效時可以先繼續;生效後憑證會自動申請" + choose GO "「${DOMAIN}」還沒有指到任何位址" "重新輸入|" "仍然繼續|DNS 剛設好、還沒生效時可以先繼續;生效後憑證會自動申請" [ "$GO" = 0 ] && { MSG=""; continue; } else mine=$(hostname -I 2>/dev/null || true) hit=0; for ip in $ips; do case " $mine " in *" $ip "*) hit=1 ;; esac; done - if [ $hit = 1 ]; then MSG=$(ok "$DOMAIN → $ips(就是這台)"); else MSG=$(warn "$DOMAIN → $ips(這台的位址:${mine:-不知道};在 NAT 或雲端主機後面時這是正常的)"); fi + if [ $hit = 1 ]; then MSG=$(ok "$DOMAIN → ${ips}(就是這台)"); else MSG=$(warn "$DOMAIN → ${ips}(這台的位址:${mine:-不知道};在 NAT 或雲端主機後面時這是正常的)"); fi header; card_open "手機和其他電腦用哪個網址連到這台?"; printf '%s\n' "$MSG"; card_close; sleep 1 fi break @@ -212,8 +212,15 @@ if [ $DRY = 1 ]; then warn "試跑(--dry-run):沒有啟動。要啟動:docker compose up -d --build" card_close; exit 0 fi -if [ "${SKIP:-0}" = 1 ] && [ -d ../../../.git ] && command -v git >/dev/null; then - spin "拉下最新版" git -C ../../.. pull --ff-only || true +if [ "${SKIP:-0}" = 1 ] && [ -d ../../.git ] && command -v git >/dev/null; then + spin "拉下最新版" git -C ../.. pull --ff-only || true +fi +# Small servers run out of memory compiling many crates at once: one compile per 2 GB. +if [ -z "${ANYBOT_BUILD_JOBS:-}" ] && [ -r /proc/meminfo ]; then + mem=$(awk '/MemTotal/ {m=$2} /SwapTotal/ {s=$2} END {print int((m + s / 2) / 1024)}' /proc/meminfo) + j=$((mem / 2000)); [ "$j" -lt 1 ] && j=1 + export ANYBOT_BUILD_JOBS=$j + [ "$mem" -lt 3000 ] && warn "記憶體只有 ${mem} MB:編譯會比較久。被中斷的話先加 2 GB swap(見 docs/install.md)" fi spin "建置並啟動(第一次約 5–15 分鐘)" docker compose up -d --build || die @@ -245,7 +252,7 @@ card_close printf '\n' card_open "$([ -n "$joined" ] && echo "完成了" || echo "差一點")" line "${B}手機${N} 打開 ${A}https://$DOMAIN${N},用 ${B}$USERNAME${N} 登入,可以「加入主畫面」" -line "${B}電腦${N} AnyBot › 設定 › 手機與帳號 登入,在「大腦」選「$BRAIN」" +line "${B}電腦${N} AnyBot › 設定 › 一般 › 手機與帳號 登入,在「大腦」選「${BRAIN}」" line "${B}AI 金鑰${N} 手機或電腦的 AnyBot › 設定 › 模型(只存在大腦上)" line "${B}加一台電腦${N} 手機 AnyBot 右上 ⋯ › 加一台電腦,把那一行貼到那台的終端機" line "" diff --git a/docs/install.md b/docs/install.md index 097fd85..68aed47 100644 --- a/docs/install.md +++ b/docs/install.md @@ -1,26 +1,42 @@ -# 從原始碼建置和安裝 +# 安裝細節 -## 需要準備 +一般使用請用 README 的一行安裝(`install.sh`),它會做完下面所有步驟。這份文件說明它做了什麼,以及想自己一步一步來時怎麼做。 + +## install.sh 做了什麼 + +| 步驟 | 桌面 App(`app`) | 手腳(`hands`) | 伺服器(`server`) | +|---|---|---|---| +| 1. 系統套件 | 編譯工具、ALSA、視窗和聲音、通知、剪貼簿、ffmpeg、中文字型 | 編譯工具、ALSA、虛擬螢幕(Xvfb)、視窗管理員、輔助使用服務 | curl、git | +| 2. 工具 | Rust(rustup,裝在 `~/.cargo`) | Rust | Docker(官方 get.docker.com)和 docker compose | +| 3. 原始碼 | 下載到 `~/AnyBot`(已存在就更新) | 同左 | 同左 | +| 4. 安裝 | 編譯,放到 `~/Applications`(Mac)或 `~/.local/bin`(Linux) | 編譯、放到 `~/.local/bin`,帶網址和配對碼時接著執行 `anybot setup` | 執行 `deploy/server/setup.sh`:問網域、帳號、密碼,用 Docker 建置並啟動 | + +- 系統套件會依發行版挑對的名字(例如 Ubuntu 24.04 的 `libasound2` 叫 `libasound2t64`)。支援 apt(Ubuntu、Debian)、dnf(Fedora)、pacman(Arch)、zypper(openSUSE)。 +- 已經有 Rust 但版本低於 1.95:有 rustup 就更新;是系統套件裝的就另外用 rustup 裝新版。 +- 記憶體小的機器會自動減少同時編譯的數量(每 2 GB 一個)。 +- 不帶參數時自己判斷:Mac 和有桌面的 Linux 裝桌面 App,沒有桌面的 Linux 裝手腳。 + +環境變數:`ANYBOT_DIR`(原始碼放哪,預設 `~/AnyBot`)、`ANYBOT_BRANCH`(預設 `main`)。 + +## 自己一步一步來 + +### 需要準備 | 平台 | 需要 | |---|---| -| 全部 | Rust(stable,用 rustup 安裝)、git、make | +| 全部 | Rust **1.95 以上**(用 rustup 安裝;apt 裝的通常太舊)、git、make | | macOS 12 以後 | Xcode Command Line Tools(`xcode-select --install`) | -| Linux | 編譯用:`build-essential pkg-config libasound2-dev`;執行用的系統套件見下面,`make deps` 會一次裝好 | +| Linux | `build-essential pkg-config libasound2-dev`;執行用的套件見下面 | | Windows | Visual Studio Build Tools(C++)。可以 `cargo build --release`,安裝包還沒做 | -安裝 Rust: +`make deps` 會把這些都裝好(等於 `./install.sh deps`)。`make doctor` 檢查還缺什麼。 + +### 步驟 ```sh -curl --proto '=https' -sSf https://sh.rustup.rs | sh -``` - -## 步驟 - -```sh -git clone git@git.30cm.net:daniel.w/AnyBot.git +git clone https://code.30cm.net/daniel.w/AnyBot.git cd AnyBot -make deps # Linux:安裝系統套件(會問 sudo 密碼);Mac:檢查 Xcode 工具 +make deps # 系統套件和 Rust(Linux 會問 sudo 密碼);Mac:Xcode 工具和 Rust make # 建置 AnyBot 和帳號伺服器(第一次約 5–15 分鐘) make install ``` @@ -34,11 +50,11 @@ make install ## Linux 的系統套件 -`make deps` 依發行版(apt、dnf、pacman)安裝這些,全部都是選用的,缺了 App 會說明少了什麼: +`make deps` 依發行版(apt、dnf、pacman、zypper)安裝這些。除了第一行,全部都是選用的,缺了 App 會說明少了什麼: | 套件 | 用途 | |---|---| -| libasound2、libxkbcommon-x11-0、libgl1、xdg-utils | 視窗和聲音(必要) | +| libasound2(Ubuntu 24.04 起叫 libasound2t64)、libxkbcommon-x11-0、libgl1、xdg-utils | 視窗和聲音(必要) | | libnotify-bin | 通知 | | libsecret-tools | 保險庫存在系統鑰匙圈 | | espeak-ng | 唸出回覆 | @@ -48,10 +64,12 @@ make install | docker.io | Bot 自己的電腦 | | fonts-noto-cjk | 更多中文字形(內建的粉圓體已經夠用) | -沒有螢幕的 Linux(只當手腳或大腦):只需要 `libasound2`。要讓 Bot 看得到畫面,再執行 `make hands`,會裝好虛擬螢幕(Xvfb)、視窗管理員和輔助使用服務。 +沒有螢幕的 Linux(只當手腳或大腦):只需要 `libasound2`;要讓 Bot 看得到畫面,還要虛擬螢幕(Xvfb)、視窗管理員和輔助使用服務。`./install.sh hands` 會一起裝好(自己來:`make hands`)。 ## 更新 +再執行一次安裝的那一行就好。自己建置的話: + ```sh git pull make install @@ -94,10 +112,24 @@ make uninstall 系統設定 › 隱私權與安全性:「螢幕錄製」和「輔助使用」要打開 AnyBot。App 的 設定 › 電腦 有按鈕直接開到那一頁。 **Linux 啟動時說找不到 libasound.so.2** -`sudo apt-get install libasound2`(Ubuntu 24.04 以後叫 `libasound2t64`)。 +`sudo apt-get install libasound2t64`(Ubuntu 24.04 以後)或 `libasound2`(更早的版本)。 **編譯時找不到 alsa 或 pkg-config** -Linux 少了編譯用的套件:`sudo apt-get install build-essential pkg-config libasound2-dev`。 +Linux 少了編譯用的套件:`make deps`,或 `sudo apt-get install build-essential pkg-config libasound2-dev`。 + +**`make: cargo: No such file or directory` 或「找不到 Rust」** +還沒裝 Rust:`make deps`。剛裝好但終端機找不到:`. "$HOME/.cargo/env"`,或開一個新的終端機。 + +**`requires rustc 1.95` 或「Rust 太舊」** +`rustup update stable`。如果 rustc 是 apt 裝的,先 `sudo apt-get remove rustc cargo`,再 `make deps`。 + +**編譯到一半 `Killed`、`signal: 9`、或整台卡住** +記憶體不夠(常見於 1–2 GB 的 VPS)。加 2 GB swap 再試: + +```sh +sudo fallocate -l 2G /swapfile && sudo chmod 600 /swapfile && sudo mkswap /swapfile && sudo swapon /swapfile +echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab # 重開機後也保留 +``` **第一次 `make` 很久** 要下載並編譯所有套件,第一次需要 5–15 分鐘,之後只會重編有改的部分。 diff --git a/docs/multi-device.md b/docs/multi-device.md index bd13967..c6e126b 100644 --- a/docs/multi-device.md +++ b/docs/multi-device.md @@ -5,7 +5,7 @@ | 角色 | 是什麼 | 怎麼設 | |---|---|---| | 大腦 | 保存所有 Bot、聊天、記憶和例行任務,由它思考和排程。要一直開著 | 登入帳號就是(預設) | -| 螢幕 | 有視窗的電腦:看到的、改的都是大腦的 Bot;同時也是大腦的手腳 | 設定 › 手機與帳號 › 大腦 › 「改用它當大腦」 | +| 螢幕 | 有視窗的電腦:看到的、改的都是大腦的 Bot;同時也是大腦的手腳 | 設定 › 一般 › 手機與帳號 › 大腦 › 「改用它當大腦」 | | 手腳 | 讓大腦的 Bot 操作這台:畫面、點擊打字、檔案和指令、這台的瀏覽器 | 「加一台電腦」的那一行,或 `anybot setup` | | 手機 | 網頁版,打開帳號伺服器的網址 | 登入同一個帳號 | @@ -15,16 +15,15 @@ ### 做法 A:放在伺服器上(推薦,Mac 關機時大腦也還在) -需要:一台有 Docker 的 Linux 主機,以及一個指到它的網域。 +需要:一台 Linux 主機(2 GB 以上記憶體;更小的請先加 swap,見 [install.md](install.md) 的常見問題)、一個 A 紀錄指到它的網域、打開 80 和 443 埠。Docker 沒裝會自動裝。 ```sh -git clone git@git.30cm.net:daniel.w/AnyBot.git && cd AnyBot -make server +curl -fsSL https://code.30cm.net/daniel.w/AnyBot/raw/branch/main/install.sh | sh -s -- server ``` -只問三件事:網域、帳號、密碼。會自動申請 HTTPS 憑證、建立帳號、啟動大腦。完成畫面會告訴你手機和電腦怎麼連。之後再執行一次 `make server`,可以選「用現有設定,更新並重新啟動」。 +只問三件事:網域、帳號、密碼。會自動申請 HTTPS 憑證、建立帳號、啟動大腦。完成畫面會告訴你手機和電腦怎麼連。之後在伺服器上執行 `cd ~/AnyBot && make server`,可以選「用現有設定,更新並重新啟動」。 -不想在伺服器上編譯:下載發行檔 `AnyBot-<版本>-server-x86_64.tar.gz`,解開後 `cd deploy/server && ./setup.sh`,用的是已經編好的程式。 +有發行檔時(推 `v*` tag 後由 Gitea Actions 產生,見 [release.md](release.md)),也可以不在伺服器上編譯:下載 `AnyBot-<版本>-server-<架構>.tar.gz`,解開後 `cd deploy/server && ./setup.sh`。 ### 做法 B:先用一台 Mac 當大腦(家裡用 Tailscale) @@ -37,7 +36,7 @@ make server launchctl load ~/Library/LaunchAgents/com.anybot.relay.plist ``` 2. 用 Tailscale 給它 HTTPS 網址:`tailscale serve --bg 8780`(網址像 `https://mac-mini.xxxx.ts.net`)。 -3. 打開 AnyBot › 設定 › 手機與帳號,填這個網址,建立帳號並登入。這台就是大腦。 +3. 打開 AnyBot › 設定 › 一般 › 手機與帳號,填這個網址,建立帳號並登入。這台就是大腦。 只有同一個 Tailscale 網路裡的手機和電腦連得到。 @@ -45,7 +44,7 @@ make server ### 有視窗的電腦(螢幕) -打開 AnyBot › 設定 › 手機與帳號,登入同一個帳號,在「大腦」那一欄按某一台旁邊的「改用它當大腦」。 +打開 AnyBot › 設定 › 一般 › 手機與帳號,登入同一個帳號,在「大腦」那一欄按某一台旁邊的「改用它當大腦」。 - 這台原本的 Bot 會留著不刪,可以按「把這台的 Bot 搬過去」複製到大腦(連同聊天、金鑰、技能和連接器;已經在大腦上的不會重複搬)。 - 隨時可以按「改回用這台自己的 Bot」。 @@ -53,16 +52,16 @@ make server ### 沒有螢幕的 Linux(手腳) -1. 在手機 AnyBot 右上「⋯ › 加一台電腦」,或電腦 AnyBot 的 設定 › 手機與帳號 › 大腦 › 加一台電腦 › 產生配對碼。 +1. 在手機 AnyBot 右上「⋯ › 加一台電腦」,或電腦 AnyBot 的 設定 › 一般 › 手機與帳號 › 大腦 › 加一台電腦 › 產生配對碼。 2. 會出現一行(10 分鐘內有效,只能用一次): ```sh - anybot setup https://anybot.example.com 469615 + curl -fsSL https://code.30cm.net/daniel.w/AnyBot/raw/branch/main/install.sh | sh -s -- hands https://anybot.example.com 469615 ``` -3. 在那台電腦裝好 AnyBot(`make install`,或發行檔 `AnyBot-<版本>-headless-x86_64.tar.gz` 裡的 `./install.sh`),再貼上這一行執行。 +3. 在那台電腦的終端機貼上這一行執行。 -它會:加入帳號(不用密碼)、只讓產生配對碼的那個大腦使用、設成開機自動執行(systemd 使用者服務 `anybot`)、沒有螢幕時開一個 1440×900 的虛擬螢幕,最後確認大腦看得到它。大腦約一分鐘內就會把它列進 Bot 可以用的電腦。 +它會:還沒裝 AnyBot 就先裝好(系統套件、Rust、編譯,第一次約 5–15 分鐘;已經裝好就直接下一步)、加入帳號(不用密碼)、只讓產生配對碼的那個大腦使用、設成開機自動執行(systemd 使用者服務 `anybot`)、沒有螢幕時開一個 1440×900 的虛擬螢幕,最後確認大腦看得到它。大腦約一分鐘內就會把它列進 Bot 可以用的電腦。 -想讓 Bot 看得到這台的畫面,先執行 `make hands`(裝 Xvfb、視窗管理員、輔助使用服務)。 +虛擬螢幕(Xvfb)、視窗管理員和輔助使用服務會一起裝好,Bot 看得到這台的畫面。 只打 `anybot setup` 會出現一個畫面:填網址和配對碼(或改用帳號密碼)。給程式用: diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..98826f6 --- /dev/null +++ b/install.sh @@ -0,0 +1,315 @@ +#!/bin/sh +# AnyBot 一鍵安裝。什麼都沒裝的電腦也可以:會自己裝好需要的東西(系統套件、Rust、Docker)。 +# +# curl -fsSL https://code.30cm.net/daniel.w/AnyBot/raw/branch/main/install.sh | sh +# 桌面 App(Mac、有螢幕的 Linux)。沒有螢幕的 Linux 會自動改成「手腳」 +# … | sh -s -- hands [網址 配對碼] +# 沒有螢幕的 Linux:裝好後接成大腦的手腳(網址和配對碼在 AnyBot 的「加一台電腦」) +# … | sh -s -- server +# 伺服器:裝 Docker,架好帳號伺服器 + 大腦 + HTTPS(只問網域、帳號、密碼) +# … | sh -s -- deps +# 只裝建置需要的東西(之後自己 make) +# +# 已經 clone 下來的話,在資料夾裡執行 ./install.sh [app|hands|server|deps],效果一樣。 +# 環境變數:ANYBOT_DIR(放原始碼的地方,預設 ~/AnyBot)、ANYBOT_BRANCH(預設 main)。 +set -eu + +REPO=${ANYBOT_REPO:-https://code.30cm.net/daniel.w/AnyBot.git} +BRANCH=${ANYBOT_BRANCH:-main} +DIR=${ANYBOT_DIR:-$HOME/AnyBot} +RUST_MIN=95 # Rust 1.95 以上 + +MODE=${1:-auto} +[ $# -gt 0 ] && shift +case $MODE in + app|hands|headless|server|deps|auto) ;; + -h|--help|help) sed -n '2,14p' "$0" 2>/dev/null | sed 's/^# \{0,1\}//'; exit 0 ;; + *) echo "不認得「${MODE}」。可以用:app、hands、server、deps"; exit 1 ;; +esac +[ "$MODE" = headless ] && MODE=hands + +# ---------- 外觀 ---------- +if [ -t 1 ]; then B=$(printf '\033[1m'); D=$(printf '\033[90m'); G=$(printf '\033[32m'); Y=$(printf '\033[33m'); R=$(printf '\033[31m'); C=$(printf '\033[36m'); N=$(printf '\033[0m') +else B=; D=; G=; Y=; R=; C=; N=; fi +STEP=0; TOTAL=4 +step() { STEP=$((STEP + 1)); printf '\n%s[%d/%d]%s %s%s%s\n' "$C" "$STEP" "$TOTAL" "$N" "$B" "$1" "$N"; } +ok() { printf ' %s✓%s %s\n' "$G" "$N" "$1"; } +info() { printf ' %s%s%s\n' "$D" "$1" "$N"; } +warn() { printf ' %s!%s %s\n' "$Y" "$N" "$1"; } +die() { printf '\n %s✗ %s%s\n' "$R" "$1" "$N"; [ $# -gt 1 ] && printf ' %s\n' "$2"; exit 1; } +have() { command -v "$1" >/dev/null 2>&1; } + +# 互動輸入:用 curl | sh 時標準輸入是腳本本身,改從終端機讀。 +TTY=/dev/tty +[ -r "$TTY" ] && [ -w "$TTY" ] && (exec <"$TTY") 2>/dev/null || TTY= + +# ---------- 這台是什麼 ---------- +OS=$(uname -s); ARCH=$(uname -m) +case $OS in + Darwin) PLAT=mac ;; + Linux) PLAT=linux ;; + *) die "這個腳本只支援 macOS 和 Linux。" "Windows:照 docs/install.md 用 cargo build --release。" ;; +esac +if [ "$MODE" = auto ]; then + if [ $PLAT = linux ] && [ -z "${DISPLAY:-}${WAYLAND_DISPLAY:-}" ] && ! have gnome-shell && ! have plasmashell && ! have startxfce4; then + MODE=hands + else + MODE=app + fi +fi +[ $PLAT = mac ] && [ "$MODE" = hands ] && MODE=app + +SUDO= +if [ $PLAT = linux ] && [ "$(id -u)" != 0 ]; then + have sudo || die "需要 sudo 才能安裝系統套件。" "請用 root 執行,或先裝 sudo。" + SUDO=sudo +fi +PM= +if [ $PLAT = linux ]; then + for p in apt-get dnf pacman zypper; do have $p && { PM=$p; break; }; done +fi + +case $MODE in + app) TITLE="桌面 App" ;; + hands) TITLE="沒有螢幕的電腦(手腳)" ;; + server) TITLE="伺服器(帳號伺服器 + 大腦 + HTTPS)"; TOTAL=3 ;; + deps) TITLE="只裝建置需要的東西"; TOTAL=2 ;; +esac +printf '\n %s◆ AnyBot 安裝%s %s%s · %s %s%s\n' "$C$B" "$N" "$D" "$TITLE" "$OS" "$ARCH" "$N" + +# ---------- 系統套件 ---------- +# apt:只裝這個發行版有的名字(例如 Ubuntu 24.04 的 libasound2 叫 libasound2t64)。 +apt_pick() { + for p in "$@"; do + case $p in + *\|*) # 幾個名字選第一個有的 + for alt in $(echo "$p" | tr '|' ' '); do + if apt-cache show "$alt" >/dev/null 2>&1; then echo "$alt"; break; fi + done ;; + *) apt-cache show "$p" >/dev/null 2>&1 && echo "$p" ;; + esac + done +} + +linux_packages() { + # $1: build | app | hands | server + case $PM in + apt-get) + $SUDO apt-get update -qq || warn "apt-get update 有錯誤,繼續試試看" + case $1 in + server) set -- curl ca-certificates git ;; + build) set -- build-essential pkg-config git curl ca-certificates libasound2-dev ;; + # 手腳:虛擬螢幕、視窗管理員、輔助使用服務(讀 App 的按鈕)、session bus,Bot 才看得到這台的畫面。 + hands) set -- build-essential pkg-config git curl ca-certificates libasound2-dev "libasound2t64|libasound2" \ + xvfb openbox at-spi2-core libatk-adaptor dbus-x11 dbus-user-session xauth fonts-noto-cjk ;; + app) set -- build-essential pkg-config git curl ca-certificates libasound2-dev "libasound2t64|libasound2" \ + libxkbcommon-x11-0 "libgl1|libgl1-mesa-glx" xdg-utils libnotify-bin libsecret-tools espeak-ng zenity \ + xclip wl-clipboard ffmpeg fonts-noto-cjk ;; + esac + pkgs=$(apt_pick "$@" | tr '\n' ' ') + info "apt-get install $pkgs" + $SUDO env DEBIAN_FRONTEND=noninteractive apt-get install -y -qq $pkgs >/dev/null || die "安裝系統套件失敗。" "請看上面的錯誤,或手動執行:sudo apt-get install $pkgs" + ;; + dnf) + base="gcc gcc-c++ make pkgconf-pkg-config git curl ca-certificates alsa-lib-devel" + extra="" + [ "$1" = app ] && extra="libxkbcommon-x11 mesa-libGL xdg-utils libnotify libsecret espeak-ng zenity xclip wl-clipboard ffmpeg-free google-noto-sans-cjk-fonts" + [ "$1" = hands ] && extra="xorg-x11-server-Xvfb openbox at-spi2-core at-spi2-atk dbus-x11 xorg-x11-xauth google-noto-sans-cjk-fonts" + [ "$1" = server ] && base="curl git ca-certificates" + $SUDO dnf install -y -q $base $extra || die "安裝系統套件失敗。" "sudo dnf install $base $extra" + ;; + pacman) + base="base-devel git curl alsa-lib" + [ "$1" = app ] && base="$base libxkbcommon-x11 mesa xdg-utils libnotify libsecret espeak-ng zenity xclip wl-clipboard ffmpeg noto-fonts-cjk" + [ "$1" = hands ] && base="$base xorg-server-xvfb openbox at-spi2-core dbus xorg-xauth noto-fonts-cjk" + [ "$1" = server ] && base="curl git" + $SUDO pacman -S --needed --noconfirm $base || die "安裝系統套件失敗。" "sudo pacman -S --needed $base" + ;; + zypper) + base="gcc gcc-c++ make pkg-config git curl alsa-devel" + [ "$1" = server ] && base="curl git" + $SUDO zypper -n install $base || die "安裝系統套件失敗。" "sudo zypper install $base" + ;; + *) + die "認不出這台的套件管理程式(apt、dnf、pacman、zypper 都沒有)。" \ + "請自己裝:C 編譯器、pkg-config、git、curl、ALSA 開發套件,再執行一次。" + ;; + esac +} + +mac_tools() { + if xcode-select -p >/dev/null 2>&1; then ok "Xcode Command Line Tools"; return; fi + warn "需要 Xcode Command Line Tools,正在打開安裝視窗…" + xcode-select --install 2>/dev/null || true + die "裝好之後(約 5 分鐘),再執行一次同一行指令。" +} + +# ---------- Rust ---------- +rust_ok() { + have cargo && have rustc || return 1 + minor=$(rustc --version 2>/dev/null | sed -n 's/^rustc 1\.\([0-9]*\).*/\1/p') + [ -n "$minor" ] && [ "$minor" -ge $RUST_MIN ] +} +ensure_rust() { + [ -f "$HOME/.cargo/env" ] && . "$HOME/.cargo/env" + if rust_ok; then ok "Rust $(rustc --version | cut -d' ' -f2)"; return; fi + if have rustup; then + info "Rust 太舊,更新中(rustup update stable)…" + rustup update stable >/dev/null 2>&1 && rustup default stable >/dev/null 2>&1 || true + else + if have rustc; then warn "系統的 Rust $(rustc --version | cut -d' ' -f2) 太舊(要 1.$RUST_MIN 以上),另外用 rustup 裝新版"; fi + info "安裝 Rust(rustup,裝在 ~/.cargo,不用 root)…" + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y -q --profile minimal --no-modify-path >/dev/null 2>&1 \ + || die "安裝 Rust 失敗。" "手動:curl --proto '=https' -sSf https://sh.rustup.rs | sh" + . "$HOME/.cargo/env" + fi + rust_ok || die "Rust 還是太舊:$(rustc --version 2>/dev/null)" "執行 rustup update stable,或移除系統的 rustc/cargo 套件。" + ok "Rust $(rustc --version | cut -d' ' -f2)" + case ":$PATH:" in *":$HOME/.cargo/bin:"*) ;; *) + for rc in "$HOME/.profile" "$HOME/.bashrc" "$HOME/.zshrc"; do + [ -f "$rc" ] && ! grep -q '.cargo/env' "$rc" && echo '. "$HOME/.cargo/env"' >>"$rc" + done ;; + esac +} + +# ---------- 原始碼 ---------- +SRC= +find_source() { + here=$(cd "$(dirname "$0")" 2>/dev/null && pwd || true) + if [ -n "$here" ] && [ -f "$here/Cargo.toml" ] && grep -q '^name = "anybot"' "$here/Cargo.toml"; then + SRC=$here; ok "用這份原始碼:$SRC"; return + fi + have git || die "需要 git。" + if [ -d "$DIR/.git" ]; then + info "更新 $DIR …" + git -C "$DIR" pull --ff-only -q || warn "沒辦法更新(你改過裡面的檔案?),用現有的版本" + else + [ -e "$DIR" ] && die "$DIR 已經存在,但不是 AnyBot 的原始碼。" "換個地方:ANYBOT_DIR=~/別的資料夾 再執行一次。" + info "下載原始碼到 $DIR …" + git clone -q --depth 1 -b "$BRANCH" "$REPO" "$DIR" || die "下載原始碼失敗。" "網路連得到 $REPO 嗎?" + fi + SRC=$DIR; ok "原始碼:${SRC}($(git -C "$SRC" log -1 --format=%h))" +} + +# 記憶體小的機器同時編太多會被系統砍掉:每 2 GB 一個工作。 +build_jobs() { + mem_mb=0 + if [ -r /proc/meminfo ]; then + mem_mb=$(awk '/MemTotal/ {m=$2} /SwapTotal/ {s=$2} END {print int((m + s / 2) / 1024)}' /proc/meminfo) + fi + [ "$mem_mb" -gt 0 ] || { echo ""; return; } + j=$((mem_mb / 2000)); [ $j -lt 1 ] && j=1 + cpus=$(nproc 2>/dev/null || echo $j); [ $j -gt "$cpus" ] && j=$cpus + echo $j + if [ "$mem_mb" -lt 3000 ]; then + warn "這台記憶體只有 ${mem_mb} MB,編譯會比較久(一次編一個)。若被中斷,加 2 GB swap 再試:" >&2 + info " sudo fallocate -l 2G /swapfile && sudo chmod 600 /swapfile && sudo mkswap /swapfile && sudo swapon /swapfile" >&2 + fi +} + +build() { + jobs=$(build_jobs) + info "編譯中,第一次約 5–15 分鐘(之後更新只重編改過的部分)…" + log="$SRC/target/install-build.log"; mkdir -p "$SRC/target" + if (cd "$SRC" && cargo build --release --bin anybot ${jobs:+-j $jobs}) >"$log" 2>&1; then + ok "編譯完成" + else + tail -n 25 "$log" | sed 's/^/ /' + die "編譯失敗(完整紀錄:${log})" "把紀錄的最後幾行貼給維護的人;常見原因見 docs/install.md 的「常見問題」。" + fi +} + +# ---------- Docker(伺服器)---------- +ensure_docker() { + if ! have docker; then + info "安裝 Docker(官方腳本 get.docker.com)…" + curl -fsSL https://get.docker.com | $SUDO sh >/dev/null || die "安裝 Docker 失敗。" "手動:https://docs.docker.com/engine/install/" + fi + $SUDO systemctl enable --now docker >/dev/null 2>&1 || true + docker compose version >/dev/null 2>&1 || $SUDO docker compose version >/dev/null 2>&1 || { + case $PM in + apt-get) $SUDO apt-get install -y -qq docker-compose-plugin >/dev/null 2>&1 || $SUDO apt-get install -y -qq docker-compose-v2 >/dev/null 2>&1 || true ;; + dnf) $SUDO dnf install -y -q docker-compose-plugin >/dev/null 2>&1 || true ;; + esac + } + docker compose version >/dev/null 2>&1 || $SUDO docker compose version >/dev/null 2>&1 || die "沒有 docker compose。" "請更新 Docker:https://docs.docker.com/compose/install/" + ok "Docker $(docker --version 2>/dev/null | sed 's/Docker version \([^,]*\).*/\1/')" + if ! docker info >/dev/null 2>&1; then + if [ -n "$SUDO" ]; then + $SUDO usermod -aG docker "$(id -un)" 2>/dev/null || true + DOCKER_SUDO=1 + info "已把你加進 docker 群組(下次登入後不用 sudo);這次先用 sudo。" + fi + fi +} + +# ===================================================================== +case $MODE in + deps) + step "系統套件" + if [ $PLAT = mac ]; then mac_tools; else linux_packages app; ok "系統套件"; fi + step "Rust" + ensure_rust + printf '\n %s好了。%s在原始碼資料夾裡:%smake%s 建置、%smake install%s 安裝。\n' "$G$B" "$N" "$B" "$N" "$B" "$N" + [ -n "${PS1:-}" ] || info "這個終端機還找不到 cargo 的話:. \"\$HOME/.cargo/env\"" + ;; + + server) + [ $PLAT = linux ] || die "伺服器模式要在 Linux 上跑。" "Mac 當大腦的做法見 docs/multi-device.md。" + step "系統套件和 Docker" + linux_packages server + ensure_docker + step "原始碼" + find_source + step "設定和啟動" + info "接下來會問網域、帳號、密碼。" + [ -n "$TTY" ] || die "需要一個終端機回答問題。" "在伺服器上直接執行:cd $SRC && ./install.sh server" + cd "$SRC/deploy/server" + if [ "${DOCKER_SUDO:-0}" = 1 ]; then exec $SUDO -E ./setup.sh "$@" <"$TTY"; else exec ./setup.sh "$@" <"$TTY"; fi + ;; + + app|hands) + step "系統套件" + if [ $PLAT = mac ]; then mac_tools + elif [ "$MODE" = hands ] && have anybot && [ $# -gt 0 ]; then ok "AnyBot 已經裝好,跳過" + else linux_packages $MODE; ok "系統套件"; fi + + if [ "$MODE" = hands ] && have anybot && [ $# -gt 0 ]; then + STEP=3; info "已經有 $(command -v anybot),直接接上。(要更新:不帶網址再執行一次)" + else + step "Rust" + ensure_rust + step "下載和編譯" + find_source + build + step "安裝" + if [ $PLAT = mac ]; then + (cd "$SRC" && make install >/dev/null) || die "安裝失敗。" "在 $SRC 執行 make install 看錯誤。" + ok "AnyBot.app 放在 ~/Applications" + else + sh "$SRC/deploy/linux/install.sh" >/dev/null || die "安裝失敗。" "執行 sh $SRC/deploy/linux/install.sh 看錯誤。" + ok "程式放在 ~/.local/bin/anybot" + [ "$MODE" = app ] && ok "應用程式選單裡有 AnyBot" + fi + fi + PATH="$HOME/.local/bin:$PATH" + + if [ "$MODE" = hands ]; then + if [ $# -gt 0 ]; then + printf '\n' + [ -n "$TTY" ] && exec anybot setup "$@" <"$TTY" + exec anybot setup "$@" + fi + printf '\n %s裝好了。%s接著把這台接到你的大腦:\n' "$G$B" "$N" + printf ' 1. 在手機或電腦的 AnyBot 按「加一台電腦」,取得配對碼\n' + printf ' 2. 在這裡執行:%sanybot setup 網址 配對碼%s\n' "$B" "$N" + else + printf '\n %s裝好了。%s' "$G$B" "$N" + if [ $PLAT = mac ]; then printf '打開 ~/Applications/AnyBot(第一次:按右鍵 › 打開)。\n' + else printf '從應用程式選單打開 AnyBot,或在終端機輸入 %sanybot%s。\n' "$B" "$N"; fi + if ! have docker; then info "Bot 自己的電腦(一個隔離的桌面)需要 Docker,要用時 AnyBot 會告訴你怎麼裝。"; fi + info "要更新:再執行一次同一行指令。" + fi + case ":$PATH:" in *":$HOME/.local/bin:"*) ;; *) [ $PLAT = linux ] && info "(終端機找不到 anybot 的話,重新登入,或把 ~/.local/bin 加進 PATH)" ;; esac + ;; +esac diff --git a/mobile/index.html b/mobile/index.html index 07f1df3..4357b76 100644 --- a/mobile/index.html +++ b/mobile/index.html @@ -230,7 +230,7 @@ header .sub{font-size:12px;color:var(--text3);font-weight:400}
加一台電腦
-
在那台電腦的終端機貼上這一行執行,它就會變成「」的手腳,開機自動執行。不用帳號密碼。
+
在那台電腦(Linux 或 Mac)的終端機貼上這一行執行:還沒裝 AnyBot 會先自動裝好,接著變成「」的手腳,開機自動執行。不用帳號密碼。
······
@@ -555,7 +555,7 @@ $("m-pair").onclick = async () => { $("pair").classList.add("on"); try { const v = await api("relay/pair", {json: {for: S.host}}); - const line = `anybot setup ${location.origin} ${v.code}`; + const line = `curl -fsSL https://code.30cm.net/daniel.w/AnyBot/raw/branch/main/install.sh | sh -s -- hands ${location.origin} ${v.code}`; $("pair-code").textContent = v.code.slice(0, 3) + " " + v.code.slice(3); $("pair-line").textContent = line; const end = Date.now() + (v.expires_in || 600) * 1000; @@ -569,7 +569,7 @@ $("m-pair").onclick = async () => { }; $("pair-copy").onclick = async () => { const line = $("pair-line").textContent; - if (!line.startsWith("anybot")) return; + if (!line.startsWith("curl")) return; try { await navigator.clipboard.writeText(line); toast("已複製"); } catch (_) { toast("請長按上面那一行複製"); } }; $("pair-close").onclick = () => { clearInterval(S.pairTimer); $("pair").classList.remove("on"); }; diff --git a/relay/Dockerfile b/relay/Dockerfile index cb48673..1a48308 100644 --- a/relay/Dockerfile +++ b/relay/Dockerfile @@ -6,11 +6,14 @@ COPY src/httpd.rs src/httpd.rs COPY src/wire.rs src/wire.rs COPY mobile mobile COPY relay relay -RUN cd relay && cargo build --release +ARG JOBS= +RUN --mount=type=cache,target=/usr/local/cargo/registry \ + --mount=type=cache,target=/src/relay/target \ + cd relay && cargo build --release ${JOBS:+-j $JOBS} && cp target/release/anybot-relay /anybot-relay FROM debian:bookworm-slim RUN useradd -m relay && mkdir /data && chown relay /data -COPY --from=build /src/relay/target/release/anybot-relay /usr/local/bin/anybot-relay +COPY --from=build /anybot-relay /usr/local/bin/anybot-relay USER relay VOLUME /data EXPOSE 8080 diff --git a/src/app/remote_ui.rs b/src/app/remote_ui.rs index 1ebcb8b..3c60e0c 100644 --- a/src/app/remote_ui.rs +++ b/src/app/remote_ui.rs @@ -153,7 +153,7 @@ impl App { ui.separator(); ui.horizontal(|ui| { ui.label(RichText::new(t("加一台電腦")).size(13.5).strong()); - help_mark(ui, "讓另一台電腦(例如沒有螢幕的 Linux)也當大腦的手腳:在那台的終端機貼上這一行就好,不用帳號密碼、不用回答問題。它會設成開機自動執行,只讓這個大腦使用。", p); + help_mark(ui, "讓另一台電腦(例如沒有螢幕的 Linux)也當大腦的手腳:在那台的終端機貼上這一行就好:還沒裝 AnyBot 會先自動裝好,不用帳號密碼、不用回答問題。它會設成開機自動執行,只讓這個大腦使用。", p); ui.with_layout(Layout::right_to_left(Align::Center), |ui| { if ui.add(secondary_button("產生配對碼")).clicked() { let (s, slot) = (self.state.remote.clone(), self.mir.pair.clone()); @@ -161,7 +161,7 @@ impl App { *slot.lock().unwrap() = (t("正在產生…"), false, None); std::thread::spawn(move || { *slot.lock().unwrap() = match crate::remote::pair_code(&s, &for_host) { - Ok(code) => (format!("anybot setup {} {code}", s.relay_url), true, Some(Instant::now())), + Ok(code) => (crate::remote::pair_line(&s.relay_url, &code), true, Some(Instant::now())), Err(e) => (e, false, None), }; }); diff --git a/src/remote.rs b/src/remote.rs index 15fc0a4..d830d1e 100644 --- a/src/remote.rs +++ b/src/remote.rs @@ -1250,3 +1250,12 @@ mod tests { assert_eq!(req.query["since"], "4"); } } + +/// Where the one-step installer lives (it also installs AnyBot on a computer that has nothing yet). +pub const INSTALL_URL: &str = "https://code.30cm.net/daniel.w/AnyBot/raw/branch/main/install.sh"; + +/// The line 「加一台電腦」 shows: pasted into any Linux or Mac terminal, it installs AnyBot when it +/// isn't there yet and joins this computer as hands (`anybot setup 網址 配對碼`). +pub fn pair_line(relay_url: &str, code: &str) -> String { + format!("curl -fsSL {INSTALL_URL} | sh -s -- hands {} {code}", relay_url.trim_end_matches('/')) +} diff --git a/src/setup.rs b/src/setup.rs index 2236a19..17f96a2 100644 --- a/src/setup.rs +++ b/src/setup.rs @@ -496,11 +496,13 @@ impl Wiz { fn type_text(&mut self, s: &str) { let s: String = s.chars().filter(|c| !c.is_control()).collect(); - // A pasted 「anybot setup 網址 配對碼」 line fills both. - let words: Vec<&str> = s.split_whitespace().filter(|w| *w != "anybot" && *w != "setup").collect(); - if words.len() == 2 && words[1].chars().all(|c| c.is_ascii_digit()) && !self.password { - self.url = words[0].to_string(); - self.code = words[1].to_string(); + // A pasted 「anybot setup 網址 配對碼」 line (or the 「curl … | sh -s -- hands 網址 配對碼」 one) + // fills both. + let words: Vec<&str> = s.split_whitespace().collect(); + let n = words.len(); + if n >= 2 && words[n - 1].chars().all(|c| c.is_ascii_digit()) && words[n - 2].contains("://") && !self.password { + self.url = words[n - 2].to_string(); + self.code = words[n - 1].to_string(); self.focus = 1; return; } @@ -687,7 +689,7 @@ impl Plan { // 2. Start with the computer. say(1, Mark::Run, ""); let started = if !self.env.linux { - say(1, Mark::Warn, "Mac 和 Windows:打開 AnyBot App 就會用這些設定連上(設定 › 手機與帳號)。"); + say(1, Mark::Warn, "Mac 和 Windows:打開 AnyBot App 就會用這些設定連上(設定 › 一般 › 手機與帳號)。"); false } else if !self.service { say(1, Mark::Warn, "這台沒有 systemd:請用「anybot --serve」啟動,或把它加到開機執行。"); @@ -742,7 +744,7 @@ impl Plan { let mut tips = vec![]; match self.role { Role::Hands => tips.push(format!("在大腦上跟 Bot 說「用 {} 的瀏覽器打開…」或「看一下 {} 的畫面」試試。", r.host_name, r.host_name)), - Role::Brain => tips.push("其他電腦:AnyBot › 設定 › 手機與帳號,登入同一個帳號,在「大腦」選這台。沒有視窗的 Linux:anybot setup 選「手腳」。".into()), + Role::Brain => tips.push("其他電腦:AnyBot › 設定 › 一般 › 手機與帳號,登入同一個帳號,在「大腦」選這台。沒有視窗的 Linux:anybot setup 選「手腳」。".into()), } if started { tips.push(String::new()); @@ -1150,5 +1152,10 @@ mod tests { w.url.clear(); w.type_text("anybot setup https://anybot.example.com 482913"); assert_eq!((w.url.as_str(), w.code.as_str()), ("https://anybot.example.com", "482913")); + let mut w = Wiz::new(Role::Hands); + w.focus = 0; + w.url.clear(); + w.type_text(&crate::remote::pair_line("https://anybot.example.com/", "135790")); + assert_eq!((w.url.as_str(), w.code.as_str()), ("https://anybot.example.com", "135790")); } }