Compare commits

...

2 Commits

Author SHA1 Message Date
王性驊 71e45f054f Merge pull request 'One-step install: install.sh, fixed Linux and server builds, clearer README' (#4) from easy-install into main
Reviewed-on: #4
2026-11-04 17:10:25 +00:00
王性驊 472a004f87 One-step install: install.sh, fixed Linux and server builds, clearer README
- 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-05 00:52:10 +08:00
15 changed files with 613 additions and 130 deletions

View File

@ -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"

View File

@ -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)

181
README.md
View File

@ -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/ 技術文件
```

View File

@ -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; }

View File

@ -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"]

View File

@ -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:

View File

@ -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 ""

View File

@ -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 分鐘,之後只會重編有改的部分。

View File

@ -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` 會出現一個畫面:填網址和配對碼(或改用帳號密碼)。給程式用:

315
install.sh Executable file
View File

@ -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

View File

@ -230,7 +230,7 @@ header .sub{font-size:12px;color:var(--text3);font-weight:400}
<div class="sheet-bg" id="pair"><div class="sheet pair">
<div class="pair-t">加一台電腦</div>
<div class="note">在那台電腦的終端機貼上這一行執行,它就會變成「<b id="pair-for"></b>」的手腳,開機自動執行。不用帳號密碼。</div>
<div class="note">在那台電腦(Linux 或 Mac)的終端機貼上這一行執行:還沒裝 AnyBot 會先自動裝好,接著變成「<b id="pair-for"></b>」的手腳,開機自動執行。不用帳號密碼。</div>
<div class="pair-code" id="pair-code">······</div>
<code class="pair-line" id="pair-line"></code>
<button class="btn main" id="pair-copy">複製這一行</button>
@ -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"); };

View File

@ -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

View File

@ -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),
};
});

View File

@ -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('/'))
}

View File

@ -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"));
}
}