AnyBot/install.sh

762 lines
37 KiB
Bash
Raw Normal View History

#!/bin/sh
# AnyBot 一鍵安裝。什麼都沒裝的電腦也可以:會自己裝好需要的東西,並做第一次設定。
#
# curl -fsSL https://code.30cm.net/daniel.w/AnyBot/raw/branch/main/install.sh | sh
# 有終端機時會問要裝哪一種,並引導第一次設定。沒有終端機時自己判斷。
# … | sh -s -- app
# 桌面 App(Mac、有螢幕的 Linux:Debian、Ubuntu、Fedora、RHEL、Arch、openSUSE、Alpine…)
# … | sh -s -- hands [網址 配對碼]
# 手腳:讓大腦操控這台(有沒有螢幕都可以)。Mac 用真實螢幕;Linux 沒螢幕會裝虛擬螢幕。
# … | sh -s -- server
# 伺服器:任何有 Docker 的 Linux,帳號伺服器 + 大腦 + HTTPS
# … | sh -s -- deps
# 只裝建置需要的東西
# … | sh -s -- --print
# 不安裝,印出這台的架構和一鍵指令(給代理程式解析,純 JSON)
#
# Windows(x64、arm64)不要用這支,用 install.ps1(在 Git Bash 裡執行這支會自動交過去):
# irm https://code.30cm.net/daniel.w/AnyBot/raw/branch/main/install.ps1 | iex
#
# 已經 clone 下來的話:./install.sh [app|hands|server|deps]
# 代理程式不要問問題,用環境變數:
# ANYBOT_NONINTERACTIVE=1 ANYBOT_MODE=hands ANYBOT_PAIR_URL=https://… ANYBOT_PAIR_CODE=482913
# ANYBOT_PROVIDER=openai ANYBOT_API_KEY=… 桌面的第一次金鑰
# ANYBOT_SETUP_USER / ANYBOT_SETUP_PASSWORD / ANYBOT_SETUP_DOMAIN 伺服器免問
# ANYBOT_SKIP_PACKAGES=1 套件已經裝好(NixOS 進了 nix-shell 之後)
# 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}
PS1_URL=${ANYBOT_PS1_URL:-https://code.30cm.net/daniel.w/AnyBot/raw/branch/main/install.ps1}
INSTALL_URL=${ANYBOT_INSTALL_URL:-https://code.30cm.net/daniel.w/AnyBot/raw/branch/main/install.sh}
RUST_MIN=95
# Git Bash / MSYS / Cygwin:這支 sh 裝不好 Windows 的手腳,交給 PowerShell。
case "$(uname -s 2>/dev/null || echo unknown)" in
MINGW*|MSYS*|CYGWIN*)
here=$(cd "$(dirname "$0")" 2>/dev/null && pwd || true)
if [ -n "$here" ] && [ -f "$here/install.ps1" ]; then
ps1=$here/install.ps1
else
ps1="${TMPDIR:-/tmp}/anybot-install-$$.ps1"
curl -fsSL "$PS1_URL" -o "$ps1" || { echo "下載 Windows 安裝腳本失敗:$PS1_URL"; exit 1; }
fi
command -v powershell.exe >/dev/null 2>&1 || { echo "找不到 powershell.exe"; exit 1; }
exec powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$ps1" "$@"
;;
esac
MODE=${ANYBOT_MODE:-${1:-auto}}
if [ "${1:-}" = "$MODE" ] || [ "${1:-}" = "auto" ] || [ "${1:-}" = "--print" ] || [ "${1:-}" = "print" ] || [ "${1:-}" = "doctor" ] || [ "${1:-}" = "guide" ]; then
[ $# -gt 0 ] && shift
fi
# 後面的函式不能用 set --:那會清掉「網址 配對碼」,手腳會裝完卻沒接上。
SAVED_1=${1:-}
SAVED_2=${2:-}
case $MODE in
app|hands|headless|server|deps|auto|guide|--print|print|doctor) ;;
-h|--help|help)
usage=$(sed -n '2,22p' "$0" 2>/dev/null | sed 's/^# \{0,1\}//' || true)
[ -n "$usage" ] || usage='AnyBot 一鍵安裝:sh install.sh [app|hands|server|deps]。代理程式:sh install.sh --print'
printf '%s\n' "$usage"; exit 0 ;;
*) echo "不認得「${MODE}」。可以用:app、hands、server、deps、--print"; exit 1 ;;
esac
[ "$MODE" = headless ] && MODE=hands
PRINT=0
[ "$MODE" = "--print" ] || [ "$MODE" = print ] || [ "$MODE" = doctor ] && PRINT=1
GUIDE=0
[ "$MODE" = guide ] && GUIDE=1 && MODE=auto
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; }
js() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g'; }
TTY=/dev/tty
[ -r "$TTY" ] && [ -w "$TTY" ] && (exec <"$TTY") 2>/dev/null || TTY=
# 代理程式、CI、沒有終端機:不要問。有 ANYBOT_NONINTERACTIVE 也不問。
INTERACTIVE=0
if [ -n "$TTY" ] && [ -z "${ANYBOT_NONINTERACTIVE:-}" ] && [ -z "${CI:-}" ]; then INTERACTIVE=1; fi
START_PATH=$PATH
# ---------- 這台是什麼 ----------
OS=$(uname -s 2>/dev/null || echo unknown)
ARCH=$(uname -m 2>/dev/null || echo unknown)
WSL=0
MUSL=0
DISTRO=
DISTRO_ID=
PRETTY=
if [ -r /etc/os-release ]; then
# shellcheck disable=SC1091
. /etc/os-release
DISTRO_ID=${ID:-}
DISTRO=${ID_LIKE:-$DISTRO_ID}
PRETTY=${PRETTY_NAME:-$DISTRO_ID}
fi
case "$(uname -r 2>/dev/null || true)" in *Microsoft*|*microsoft*|*WSL*) WSL=1 ;; esac
[ -n "${WSL_DISTRO_NAME:-}" ] && WSL=1
if [ -f /etc/alpine-release ] || have apk; then
if [ "$DISTRO_ID" = alpine ] || [ -f /etc/alpine-release ]; then MUSL=1; fi
fi
if have ldd && ldd --version 2>&1 | grep -qi musl; then MUSL=1; fi
case $OS in
Darwin) PLAT=mac ;;
Linux) PLAT=linux ;;
*) die "這個腳本只支援 macOS 和 Linux(含各種發行版)。" "Windows:irm $PS1_URL | iex" ;;
esac
DESKTOP=0
if [ "$PLAT" = mac ]; then DESKTOP=1; fi
if [ -n "${DISPLAY:-}${WAYLAND_DISPLAY:-}${XDG_CURRENT_DESKTOP:-}" ]; then DESKTOP=1; fi
if [ -d /mnt/wslg ] || [ -n "${WAYLAND_DISPLAY:-}" ]; then DESKTOP=1; fi
for w in gnome-shell plasmashell startxfce4 sway Hyprland cinnamon mate-session xfce4-session; do
if have "$w" || pgrep -x "$w" >/dev/null 2>&1; then DESKTOP=1; fi
done
# 有裝桌面環境、但這次是 SSH(沒有 DISPLAY):不算「正在用桌面」,避免裝錯。
SSH_TO_DESKTOP=0
if [ "$PLAT" = linux ] && [ "$DESKTOP" = 0 ]; then
if ls /usr/share/xsessions/*.desktop /usr/share/wayland-sessions/*.desktop >/dev/null 2>&1; then SSH_TO_DESKTOP=1; fi
fi
RUST_HOST=
case $PLAT in
mac)
case $ARCH in
arm64|aarch64) RUST_HOST=aarch64-apple-darwin; ARCH_ID=arm64 ;;
x86_64) RUST_HOST=x86_64-apple-darwin; ARCH_ID=x86_64 ;;
*) ARCH_ID=$ARCH ;;
esac
if [ "$(sysctl -n sysctl.proc_translated 2>/dev/null || echo 0)" = 1 ]; then
ROSETTA=1
else
ROSETTA=0
fi
;;
linux)
case $ARCH in
x86_64|amd64) ARCH_ID=x86_64; RUST_HOST=x86_64-unknown-linux-gnu; [ "$MUSL" = 1 ] && RUST_HOST=x86_64-unknown-linux-musl ;;
aarch64|arm64) ARCH_ID=arm64; RUST_HOST=aarch64-unknown-linux-gnu; [ "$MUSL" = 1 ] && RUST_HOST=aarch64-unknown-linux-musl ;;
armv7l|armv6l) ARCH_ID=armv7; RUST_HOST=armv7-unknown-linux-gnueabihf ;;
riscv64) ARCH_ID=riscv64; RUST_HOST=riscv64gc-unknown-linux-gnu ;;
i386|i686) ARCH_ID=i686; RUST_HOST= ;;
*) ARCH_ID=$ARCH; RUST_HOST= ;;
esac
ROSETTA=0
;;
esac
PM=
if [ "$PLAT" = linux ]; then
for p in apt-get dnf yum pacman zypper apk xbps-install; do have $p && { PM=$p; break; }; done
# dnf 和 yum 同時在時用 dnf。
if have dnf && have yum; then PM=dnf; fi
fi
SYSTEMD=0
if [ "$PLAT" = linux ] && [ -d /run/systemd/system ]; then SYSTEMD=1; fi
REC=app
if [ "$PLAT" = linux ] && [ "$DESKTOP" = 0 ]; then REC=hands; fi
SERVER_OK=0
if [ "$PLAT" = linux ]; then SERVER_OK=1; fi
NOTES=
note() {
if [ -n "$NOTES" ]; then
NOTES="$NOTES
$1"
else
NOTES=$1
fi
}
[ -z "$RUST_HOST" ] && note "這個 CPU 架構($ARCH)沒有對應的 Rust 目標,一鍵安裝不會編譯。"
[ "$MUSL" = 1 ] && note "這是 musl(多半是 Alpine)。可以編譯,桌面視窗比 glibc 發行版容易缺函式庫;當手腳比較穩。"
[ "$WSL" = 1 ] && [ "$DESKTOP" = 0 ] && note "這是沒有 WSLg 的 WSL。這裡裝的是 Linux 手腳(虛擬螢幕)。要操控 Windows 桌面,請在 Windows 的 PowerShell 執行 install.ps1,不要在 WSL 裡裝。"
[ "$WSL" = 1 ] && [ "$DESKTOP" = 1 ] && note "偵測到 WSLg,會用 Linux 桌面。要操控 Windows 自己的視窗,另外在 Windows 裝 install.ps1。"
[ "$SSH_TO_DESKTOP" = 1 ] && note "這次連線沒有螢幕,但系統裝了桌面環境。手腳會用虛擬螢幕(Bot 看得到);若要裝進選單的桌面 App,請選 app。"
[ "$ROSETTA" = 1 ] && note "正在用 Rosetta。會編成 Intel 版。要 Apple Silicon 原生版,請用 arch -arm64 再開一個終端機執行。"
[ "$SERVER_OK" = 0 ] && note "伺服器(帳號+大腦+HTTPS)要在 Linux 上跑。這台當大腦:裝桌面 App 後執行 anybot setup --brain。"
if [ "$DISTRO_ID" = nixos ]; then
note "NixOS 不能用套件管理員改系統。請先 nix-shell -p gcc pkg-config alsa-lib pkg-config xorg.libX11 libxkbcommon wayland,再 ANYBOT_SKIP_PACKAGES=1 執行這支腳本。"
fi
if [ "$DISTRO_ID" = gentoo ]; then
note "Gentoo 請先自行 emerge 編譯器、pkg-config、alsa-lib,再 ANYBOT_SKIP_PACKAGES=1 執行。"
fi
# ---------- 給代理程式看的報告(不安裝)----------
if [ "$PRINT" = 1 ]; then
supported=true
[ -z "$RUST_HOST" ] && supported=false
if [ "$MODE" = doctor ]; then
printf 'AnyBot 安裝檢查 %s %s\n' "$OS" "$ARCH"
printf ' 發行版 %s\n' "${PRETTY:-(沒有 /etc/os-release)}"
printf ' Rust 目標 %s\n' "${RUST_HOST:-不支援}"
printf ' 套件 %s\n' "${PM:-沒有認出來的套件管理員}"
printf ' 桌面 %s\n' "$([ "$DESKTOP" = 1 ] && echo 有 || echo 沒有)"
printf ' systemd %s\n' "$([ "$SYSTEMD" = 1 ] && echo 有 || echo 沒有)"
printf ' 建議 %s\n' "$REC"
[ "$SERVER_OK" = 1 ] && printf ' 伺服器 可以(要 Docker)\n' || printf ' 伺服器 請改在 Linux 上架\n'
printf '%s\n' "$NOTES" | sed '/^$/d; s/^/ /'
printf '\n一鍵:\n 桌面 curl -fsSL %s | sh -s -- app\n 手腳 curl -fsSL %s | sh -s -- hands 網址 配對碼\n 伺服器 curl -fsSL %s | sh -s -- server\n' "$INSTALL_URL" "$INSTALL_URL" "$INSTALL_URL"
exit 0
fi
notes_json=$(printf '%s\n' "$NOTES" | awk 'NF{gsub(/\\/,"\\\\"); gsub(/"/,"\\\""); printf "%s\"%s\"", (n?",":""), $0; n=1}')
printf '{"ok":%s,"os":"%s","arch":"%s","arch_id":"%s","rust_host":"%s","distro":"%s","distro_id":"%s","libc":"%s","pm":"%s","desktop":%s,"headless":%s,"wsl":%s,"systemd":%s,"recommended":"%s","server_ok":%s,"modes":["app","hands","server","deps"],"notes":[%s],"one_click":{"app":"%s","hands":"%s","server":"%s","windows":"%s","print":"%s"}}\n' \
"$supported" "$(js "$OS")" "$(js "$ARCH")" "$(js "${ARCH_ID:-}")" "$(js "${RUST_HOST:-}")" \
"$(js "${PRETTY:-}")" "$(js "${DISTRO_ID:-}")" "$([ "$PLAT" = mac ] && echo native || { [ "$MUSL" = 1 ] && echo musl || echo glibc; })" "$(js "${PM:-}")" \
"$([ "$DESKTOP" = 1 ] && echo true || echo false)" "$([ "$DESKTOP" = 1 ] && echo false || echo true)" \
"$([ "$WSL" = 1 ] && echo true || echo false)" "$([ "$SYSTEMD" = 1 ] && echo true || echo false)" \
"$REC" "$([ "$SERVER_OK" = 1 ] && echo true || echo false)" "$notes_json" \
"$(js "curl -fsSL $INSTALL_URL | sh -s -- app")" \
"$(js "curl -fsSL $INSTALL_URL | sh -s -- hands 網址 配對碼")" \
"$(js "curl -fsSL $INSTALL_URL | sh -s -- server")" \
"$(js "\$env:ANYBOT_MODE='hands'; \$env:ANYBOT_PAIR_URL='網址'; \$env:ANYBOT_PAIR_CODE='配對碼'; irm $PS1_URL | iex")" \
"$(js "curl -fsSL $INSTALL_URL | sh -s -- --print")"
if [ "$supported" = true ]; then exit 0; fi
exit 1
fi
[ -n "$RUST_HOST" ] || die "不支援的 CPU 架構:$OS $ARCH" "支援:macOS arm64/x86_64,Linux x86_64、aarch64、armv7、riscv64(glibc 或 musl)。Windows x64/arm64 用 install.ps1。"
if [ "$MODE" = auto ] && { [ "$GUIDE" = 1 ] || [ "$INTERACTIVE" = 1 ]; }; then
printf '\n %s◆ AnyBot%s %s%s %s · %s%s\n' "$C$B" "$N" "$D" "$OS" "$ARCH" "${PRETTY:+$PRETTY}" "$N"
[ -n "$NOTES" ] && printf '%s\n' "$NOTES" | sed "s/^/ $Y!$N /"
printf ' 建議裝:%s%s%s\n' "$B" "$([ "$REC" = hands ] && echo 手腳 || echo 桌面 App)" "$N"
printf ' 1 桌面 App(自己在這台用)\n'
printf ' 2 手腳(讓另一台的 Bot 操控這台的螢幕、檔案、瀏覽器)\n'
printf ' 3 伺服器(帳號 + 大腦 + HTTPS,要 Linux)\n'
printf ' 4 只裝編譯需要的東西\n'
printf ' 選 1/2/3/4,直接 Enter 用建議:'
choice=; read choice <"$TTY" || choice=
case $choice in
1) MODE=app ;;
2) MODE=hands ;;
3) MODE=server ;;
4) MODE=deps ;;
"") MODE=$REC ;;
*) die "請選 1、2、3 或 4。" ;;
esac
fi
if [ "$MODE" = auto ]; then
MODE=$REC
fi
SUDO=
if [ "$PLAT" = linux ] && [ "$(id -u)" != 0 ] && [ "${ANYBOT_SKIP_PACKAGES:-}" != 1 ]; then
if [ -n "$PM" ]; then
have sudo || die "需要 sudo 才能安裝系統套件。" "請用 root 執行,或先裝 sudo,或套件裝好後 ANYBOT_SKIP_PACKAGES=1。"
SUDO=sudo
if [ "$INTERACTIVE" = 0 ]; then
$SUDO -n true >/dev/null 2>&1 || die "沒有終端機,sudo 卻要密碼。" "請使用者先 sudo true,或用 root,或 ANYBOT_SKIP_PACKAGES=1。"
fi
fi
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"
[ "$ROSETTA" = 1 ] && warn "Rosetta:編成 $RUST_HOST"
# ---------- 磁碟 ----------
disk_check() {
[ "${ANYBOT_IGNORE_DISK:-}" = 1 ] && return 0
kb=$(df -Pk "${HOME:-/}" 2>/dev/null | awk 'NR==2 {print $4}')
[ -n "${kb:-}" ] || return 0
if [ "$kb" -lt 1500000 ]; then
die "家目錄剩不到 1.5 GB,編譯 Rust 不夠。" "清出空間,或 ANYBOT_IGNORE_DISK=1 仍要試(多半會失敗)。"
fi
if [ "$kb" -lt 4000000 ]; then
warn "家目錄只剩約 $((kb / 1024)) MB,編譯會很緊。記憶體小的話請加 swap。"
fi
}
# ---------- 系統套件 ----------
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
true
}
zypper_pick() {
for p in "$@"; do
zypper --non-interactive info "$p" >/dev/null 2>&1 && echo "$p"
done
true
}
dnf_pick() {
tool=$1; shift
for p in "$@"; do
$tool info -q "$p" >/dev/null 2>&1 && echo "$p"
done
true
}
apk_pick() {
for p in "$@"; do
apk search -e "$p" >/dev/null 2>&1 && echo "$p"
done
true
}
xbps_pick() {
for p in "$@"; do
xbps-query -Rs "^$p\$" >/dev/null 2>&1 && echo "$p"
done
true
}
apt_get() {
i=0
while [ "$i" -lt 8 ]; do
if $SUDO env DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a APT_LISTCHANGES_FRONTEND=none \
apt-get "$@" -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold; then
return 0
fi
if have fuser && { $SUDO fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1 || $SUDO fuser /var/lib/apt/lists/lock >/dev/null 2>&1; }; then
warn "套件鎖被佔用,15 秒後再試($((i + 1))/8)"
sleep 15
i=$((i + 1))
continue
fi
return 1
done
return 1
}
linux_packages() {
# $1: build | app | hands | server
[ "${ANYBOT_SKIP_PACKAGES:-}" = 1 ] && { info "略過系統套件(ANYBOT_SKIP_PACKAGES=1)"; return 0; }
[ "$DISTRO_ID" = nixos ] || [ "$DISTRO_ID" = gentoo ] && {
warn "這套系統不自動裝套件。請看上面的說明,裝好後加 ANYBOT_SKIP_PACKAGES=1。"
return 0
}
case $PM in
apt-get)
case $1 in
server) pkgs_in="curl ca-certificates git" ;;
build) pkgs_in="build-essential pkg-config git curl ca-certificates libasound2-dev|libasound-dev" ;;
# 手腳:虛擬螢幕、視窗管理員、輔助使用(讀按鈕)、session bus。各發行版名字不同,有的才裝。
hands) pkgs_in="build-essential pkg-config git curl ca-certificates libasound2-dev|libasound-dev libasound2t64|libasound2 xvfb openbox at-spi2-core libatk-adaptor dbus-x11 dbus-user-session dbus xauth fonts-noto-cjk fonts-noto-cjk-extra" ;;
app) pkgs_in="build-essential pkg-config git curl ca-certificates libasound2-dev|libasound-dev libasound2t64|libasound2 libxkbcommon-x11-0 libxkbcommon0 libgl1|libgl1-mesa-glx libegl1|libegl1-mesa libvulkan1 libgtk-3-0t64|libgtk-3-0 libx11-6 libxcb1 libxcursor1 libxi6 libxrandr2 libwayland-client0 libwayland-cursor0 libwayland-egl1 xdg-utils libnotify-bin libsecret-tools espeak-ng zenity xclip wl-clipboard ffmpeg fonts-noto-cjk" ;;
esac
if ! ls /var/lib/apt/lists/*_Packages >/dev/null 2>&1; then
apt_get update -qq || warn "apt-get update 有錯誤,繼續試試看"
UPDATED=1
fi
# shellcheck disable=SC2086
pkgs=$(apt_pick $pkgs_in | tr '\n' ' ')
missing=""
for p in $pkgs; do dpkg -s "$p" >/dev/null 2>&1 || missing="$missing $p"; done
if [ -z "$missing" ]; then info "系統套件都已經裝好了"; return 0; fi
pkgs=$missing
[ -n "${UPDATED:-}" ] || apt_get update -qq || warn "apt-get update 有錯誤,繼續試試看"
info "apt-get install$pkgs"
apt_get install -y -qq $pkgs >/dev/null || die "安裝系統套件失敗。" "請看上面的錯誤,或手動:sudo apt-get install $pkgs"
;;
dnf|yum)
base="gcc gcc-c++ make pkgconf-pkg-config git curl ca-certificates alsa-lib-devel"
extra=""
[ "$1" = app ] && extra="libxkbcommon libxkbcommon-x11 mesa-libGL mesa-libEGL vulkan-loader libwayland-client libX11 libXcursor libXi libXrandr xdg-utils libnotify libsecret espeak-ng zenity xclip wl-clipboard ffmpeg-free google-noto-sans-cjk-fonts gtk3"
[ "$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" && extra=""
[ "$1" = build ] && extra=""
picked=$(dnf_pick "$PM" $base $extra | tr '\n' ' ')
[ -n "$picked" ] || picked="$base $extra"
info "$PM install $picked"
$SUDO $PM install -y -q $picked || die "安裝系統套件失敗。" "sudo $PM install $picked"
;;
pacman)
base="base-devel git curl alsa-lib"
[ "$1" = app ] && base="$base libxkbcommon libxkbcommon-x11 mesa vulkan-icd-loader wayland libx11 libxcursor libxi libxrandr xdg-utils libnotify libsecret espeak-ng zenity xclip wl-clipboard ffmpeg noto-fonts-cjk gtk3"
[ "$1" = hands ] && base="$base xorg-server-xvfb openbox at-spi2-core dbus xorg-xauth noto-fonts-cjk"
[ "$1" = server ] && base="curl git ca-certificates"
$SUDO pacman -Sy --needed --noconfirm $base || die "安裝系統套件失敗。" "sudo pacman -S --needed $base"
;;
zypper)
base="gcc gcc-c++ make pkg-config git curl alsa-devel"
extra=""
[ "$1" = app ] && extra="libxkbcommon0 libxkbcommon-x11-0 Mesa-libGL1 libGL1 xdg-utils libnotify-tools libsecret-1-0 espeak zenity xclip wl-clipboard ffmpeg fonts-noto gtk3-tools"
[ "$1" = hands ] && extra="xorg-x11-server-Xvfb openbox at-spi2-core at-spi2-atk-common dbus-1-x11 xauth fonts-noto"
[ "$1" = server ] && base="curl git ca-certificates" && extra=""
pkgs=$(zypper_pick $base $extra)
$SUDO zypper -n install $pkgs || die "安裝系統套件失敗。" "sudo zypper install $pkgs"
;;
apk)
base="build-base pkgconf git curl ca-certificates alsa-lib-dev"
extra=""
[ "$1" = app ] && extra="alsa-lib libxkbcommon libxkbcommon-x11 mesa-egl mesa-gl libx11 libxcursor libxi libxrandr wayland-libs-client xdg-utils libnotify font-noto-cjk ffmpeg gtk+3.0"
[ "$1" = hands ] && extra="xvfb openbox at-spi2-core dbus xauth font-noto-cjk"
[ "$1" = server ] && base="curl git ca-certificates" && extra=""
$SUDO apk update >/dev/null || warn "apk update 有錯誤,繼續試試看"
picked=$(apk_pick $base $extra | tr '\n' ' ')
[ -n "$picked" ] || picked="$base $extra"
info "apk add $picked"
$SUDO apk add $picked || die "安裝系統套件失敗。" "sudo apk add $picked"
;;
xbps-install)
base="base-devel pkg-config git curl alsa-lib-devel"
extra=""
[ "$1" = app ] && extra="alsa-lib libxkbcommon libxkbcommon-x11 MesaLib wayland libX11 libXcursor libXi libXrandr xdg-utils libnotify font-noto-cjk ffmpeg gtk+3"
[ "$1" = hands ] && extra="xorg-server openbox at-spi2-core dbus font-noto"
[ "$1" = server ] && base="curl git" && extra=""
$SUDO xbps-install -S || warn "xbps-install -S 有錯誤,繼續試試看"
picked=$(xbps_pick $base $extra | tr '\n' ' ')
[ -n "$picked" ] || picked="$base $extra"
info "xbps-install $picked"
$SUDO xbps-install -y $picked || die "安裝系統套件失敗。" "sudo xbps-install -y $picked"
;;
*)
if have cc && have pkg-config; then
warn "認不出套件管理員,但已有 C 編譯器和 pkg-config,繼續編譯。"
warn "若編譯抱怨缺 alsa,請自行安裝對應的開發套件後再執行。"
return 0
fi
die "認不出套件管理員(apt、dnf、yum、pacman、zypper、apk、xbps 都沒有)。" \
"請自己裝:C 編譯器、pkg-config、git、curl、ALSA 開發套件,再 ANYBOT_SKIP_PACKAGES=1 執行一次。NixOS / Gentoo 見 --print 的 notes。"
;;
esac
}
hands_runtime_check() {
missing=""
have xvfb-run || have Xvfb || missing="$missing xvfb"
have openbox || missing="$missing openbox"
have dbus-run-session || have dbus-launch || missing="$missing dbus"
if [ -n "$missing" ]; then
warn "手腳還缺:$missing"
warn "Bot 會連得上,但看不到這台的畫面。用你的套件管理員裝好後再執行一次。"
if [ "$PM" = dnf ] || [ "$PM" = yum ]; then
info "RHEL、Rocky、Alma 的 openbox / Xvfb 常常在 EPEL:sudo $PM install epel-release 後再執行一次。"
fi
else
ok "手腳看畫面需要的東西都在(虛擬螢幕、視窗管理員、dbus)"
fi
}
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
major=$(rustc --version 2>/dev/null | sed -n 's/^rustc \([0-9][0-9]*\)\.\([0-9][0-9]*\).*/\1/p')
minor=$(rustc --version 2>/dev/null | sed -n 's/^rustc \([0-9][0-9]*\)\.\([0-9][0-9]*\).*/\2/p')
[ -n "$major" ] && [ -n "$minor" ] || return 1
[ "$major" -gt 1 ] && return 0
[ "$major" -eq 1 ] && [ "$minor" -ge "$RUST_MIN" ]
}
ensure_rust() {
[ -f "$HOME/.cargo/env" ] && . "$HOME/.cargo/env"
export PATH="$HOME/.cargo/bin:$PATH"
if rust_ok; then ok "Rust $(rustc --version | cut -d' ' -f2)($(rustc -vV 2>/dev/null | awk '/^host:/{print $2}'))"; 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
[ -f "$HOME/.cargo/env" ] && . "$HOME/.cargo/env"
else
if have rustc; then warn "系統的 Rust $(rustc --version | cut -d' ' -f2) 太舊(要 1.$RUST_MIN 以上),另外用 rustup 裝新版"; fi
info "安裝 Rust(rustup,目標 $RUST_HOST,裝在 ~/.cargo,不用 root)…"
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y -q --profile minimal --default-host "$RUST_HOST" --no-modify-path >/dev/null 2>&1 \
|| die "安裝 Rust 失敗。" "手動:curl --proto '=https' -sSf https://sh.rustup.rs | sh"
. "$HOME/.cargo/env"
fi
# 有人用 Rosetta 或跨架構:確保這個目標有標準庫。
if ! rustup target list --installed 2>/dev/null | grep -q "^${RUST_HOST}$"; then
rustup target add "$RUST_HOST" >/dev/null 2>&1 || warn "沒能加上 Rust 目標 $RUST_HOST,仍用 rustup 預設的主機目標試試"
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 嗎?在代理後面請設 https_proxy。"
fi
SRC=$DIR; ok "原始碼:${SRC}($(git -C "$SRC" log -1 --format=%h))"
}
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)
elif [ "$PLAT" = mac ]; then
mem_mb=$(( $(sysctl -n hw.memsize 2>/dev/null || echo 0) / 1024 / 1024 ))
fi
[ "${mem_mb:-0}" -gt 0 ] || { echo ""; return; }
j=$((mem_mb / 2000)); [ "$j" -lt 1 ] && j=1
cpus=$(nproc 2>/dev/null || sysctl -n hw.ncpu 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 分鐘($RUST_HOST;之後更新只重編改過的部分)…"
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
}
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/(選你的發行版,不限 Ubuntu)"
fi
if have systemctl; then
$SUDO systemctl enable --now docker >/dev/null 2>&1 || true
elif have service; then
$SUDO service docker start >/dev/null 2>&1 || true
elif have rc-service; then
$SUDO rc-service docker start >/dev/null 2>&1 || $SUDO rc-update add docker default >/dev/null 2>&1 || true
fi
docker compose version >/dev/null 2>&1 || $SUDO docker compose version >/dev/null 2>&1 || {
case $PM in
apt-get) apt_get install -y -qq docker-compose-plugin >/dev/null 2>&1 || apt_get install -y -qq docker-compose-v2 >/dev/null 2>&1 || true ;;
dnf|yum) $SUDO $PM install -y -q docker-compose-plugin >/dev/null 2>&1 || true ;;
apk) $SUDO apk add docker-cli-compose >/dev/null 2>&1 || true ;;
pacman) $SUDO pacman -S --needed --noconfirm docker-compose >/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
}
anybot_bin() {
if [ "$PLAT" = mac ]; then
printf '%s\n' "$HOME/Applications/AnyBot.app/Contents/MacOS/AnyBot"
else
printf '%s\n' "$HOME/.local/bin/anybot"
fi
}
link_mac_bin() {
mkdir -p "$HOME/.local/bin"
ln -sfn "$HOME/Applications/AnyBot.app/Contents/MacOS/AnyBot" "$HOME/.local/bin/anybot"
# 權限是跟著 App 套件走的。背景服務也要叫這支,不要叫 target/ 裡的另一份。
}
mac_permissions() {
printf '\n %s第一次要用這台的螢幕,macOS 會問兩次權限:%s\n' "$B" "$N"
printf ' 系統設定 › 隱私權與安全性 › 螢幕錄製、輔助使用,打開 AnyBot。\n'
open "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture" >/dev/null 2>&1 || true
}
seed_or_pair() {
bin=$(anybot_bin)
[ -x "$bin" ] || bin=$(command -v anybot || true)
[ -n "$bin" ] || return 0
export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH"
if [ -n "${ANYBOT_PAIR_URL:-}" ] && [ -n "${ANYBOT_PAIR_CODE:-}" ]; then
info "用環境變數配對,不另外詢問。"
"$bin" setup "$ANYBOT_PAIR_URL" "$ANYBOT_PAIR_CODE" || die "配對失敗。" "配對碼 10 分鐘內有效、只能用一次。到大腦按「加一台電腦」再拿一組。"
return 0
fi
if [ -n "${ANYBOT_PROVIDER:-}" ]; then
info "寫入第一次的模型設定。"
"$bin" setup --seed || die "寫入第一次設定失敗。"
return 0
fi
[ "$INTERACTIVE" = 1 ] || {
info "尚未做第一次設定。代理程式請設 ANYBOT_PAIR_URL 與 ANYBOT_PAIR_CODE,或 ANYBOT_PROVIDER 與 ANYBOT_API_KEY,再執行一次。"
return 0
}
printf '\n %s第一次設定%s(可略過,之後打開 AnyBot 也會引導)\n' "$B" "$N"
printf ' 1 稍後在 App 裡設定\n'
printf ' 2 貼上 API 金鑰,建立第一個 Bot\n'
printf ' 3 接到大腦,這台當手腳\n'
printf ' 選 1/2/3 [1]: '
choice=; read choice <"$TTY" || choice=
case $choice in
2)
printf ' 服務 openai / anthropic / xai / gemini / openrouter / opencode / ollama / custom:'
read ANYBOT_PROVIDER <"$TTY" || ANYBOT_PROVIDER=
printf ' API 金鑰(ollama 可直接 Enter):'
stty -echo <"$TTY" 2>/dev/null || true
read ANYBOT_API_KEY <"$TTY" || ANYBOT_API_KEY=
stty echo <"$TTY" 2>/dev/null || true
printf '\n'
export ANYBOT_PROVIDER ANYBOT_API_KEY
"$bin" setup --seed || warn "沒寫入,請打開 App 再設一次"
;;
3)
printf ' 網址:'; read url <"$TTY" || url=
printf ' 配對碼:'; read code <"$TTY" || code=
[ -n "$url" ] && [ -n "$code" ] && "$bin" setup "$url" "$code" || warn "略過配對。之後:anybot setup 網址 配對碼"
;;
esac
}
# =====================================================================
disk_check
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"
info "這個終端機還找不到 cargo 的話:. \"\$HOME/.cargo/env\""
;;
server)
[ "$PLAT" = linux ] || die "伺服器模式要在 Linux 上跑(任何有 Docker 的發行版)。" "Mac 或 Windows 當大腦:裝桌面 App,再 anybot setup --brain。見 docs/multi-device.md。"
step "系統套件和 Docker"
linux_packages server
ensure_docker
step "原始碼"
find_source
step "設定和啟動"
info "接下來會問網址、帳號、密碼。免問:ANYBOT_SETUP_USER、ANYBOT_SETUP_PASSWORD、ANYBOT_SETUP_DOMAIN。"
cd "$SRC/deploy/server"
IN=$TTY
if [ -n "${ANYBOT_SETUP_USER:-}" ] && [ -n "${ANYBOT_SETUP_PASSWORD:-}" ]; then IN=/dev/null
else [ -n "$TTY" ] || die "需要一個終端機回答問題。" "在伺服器上直接執行,或用 ANYBOT_SETUP_USER、ANYBOT_SETUP_PASSWORD(不給網域就用 Tailscale)。"; fi
if [ "${DOCKER_SUDO:-0}" = 1 ]; then
if [ -n "$SAVED_1" ]; then exec $SUDO -E ./setup.sh "$SAVED_1" ${SAVED_2:+"$SAVED_2"} <"$IN"; else exec $SUDO -E ./setup.sh <"$IN"; fi
else
if [ -n "$SAVED_1" ]; then exec ./setup.sh "$SAVED_1" ${SAVED_2:+"$SAVED_2"} <"$IN"; else exec ./setup.sh <"$IN"; fi
fi
;;
app|hands)
step "系統套件"
if [ "$PLAT" = mac ]; then mac_tools
elif [ "$MODE" = hands ] && have anybot && [ -n "$SAVED_1" ]; then ok "AnyBot 已經裝好,跳過編譯"
else
linux_packages "$MODE"
# 手腳也要能編譯(ALSA),上面的 hands 清單已含編譯套件。桌面再補執行期函式庫已在 app。
[ "$MODE" = hands ] && hands_runtime_check
ok "系統套件"
fi
if [ "$MODE" = hands ] && have anybot && [ -n "$SAVED_1" ]; 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 看錯誤。"
link_mac_bin
ok "AnyBot.app 放在 ~/Applications(終端機指令:anybot)"
mac_permissions
else
sh "$SRC/deploy/linux/install.sh" >/dev/null || die "安裝失敗。" "執行 sh $SRC/deploy/linux/install.sh 看錯誤。"
ok "程式放在 ~/.local/bin/anybot"
[ "$MODE" = app ] && [ "$DESKTOP" = 1 ] && ok "應用程式選單裡有 AnyBot"
if [ -f "$HOME/.local/bin/anybot-run" ] && have systemctl && systemctl --user is-enabled --quiet anybot 2>/dev/null; then
"$HOME/.local/bin/anybot" setup --refresh-service >/dev/null 2>&1 && ok "重新啟動背景執行的 AnyBot(換成新版)"
fi
if [ -f "$HOME/Library/LaunchAgents/com.anybot.serve.plist" ]; then
"$HOME/.local/bin/anybot" setup --refresh-service >/dev/null 2>&1 || true
fi
fi
fi
PATH="$HOME/.local/bin:$PATH"
if [ "$(id -u)" = 0 ]; then
export XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR:-/run/user/0}
mkdir -p "$XDG_RUNTIME_DIR" 2>/dev/null || true
loginctl enable-linger root >/dev/null 2>&1 || true
fi
if [ "$MODE" = hands ]; then
if [ -z "$SAVED_1" ] && [ -z "${ANYBOT_PAIR_URL:-}" ] && [ "$INTERACTIVE" = 1 ]; then
printf '\n 把這台接到大腦(在 AnyBot 按「加一台電腦」會看到網址和配對碼)\n'
printf ' 網址(直接 Enter 先跳過):'
url=; read url <"$TTY" || url=
if [ -n "$url" ]; then
printf ' 配對碼:'
code=; read code <"$TTY" || code=
[ -n "$code" ] && SAVED_1=$url && SAVED_2=$code
fi
fi
if [ -n "$SAVED_1" ]; then
printf '\n'
if [ -n "$SAVED_2" ]; then
[ -n "$TTY" ] && exec anybot setup "$SAVED_1" "$SAVED_2" <"$TTY"
exec anybot setup "$SAVED_1" "$SAVED_2"
fi
[ -n "$TTY" ] && exec anybot setup "$SAVED_1" <"$TTY"
exec anybot setup "$SAVED_1"
fi
if [ -n "${ANYBOT_PAIR_URL:-}" ] && [ -n "${ANYBOT_PAIR_CODE:-}" ]; then
exec anybot setup "$ANYBOT_PAIR_URL" "$ANYBOT_PAIR_CODE"
fi
printf '\n %s裝好了。%s接著把這台接到你的大腦:\n' "$G$B" "$N"
printf ' 1. 在手機或電腦的 AnyBot 按「加一台電腦」\n'
printf ' 2. 在這裡執行:%sanybot setup 網址 配對碼%s\n' "$B" "$N"
if [ "$PLAT" = linux ] && [ "$DESKTOP" = 0 ]; then
info "沒有螢幕:配對後會開一塊虛擬螢幕,Bot 看得到、點得到。要改用已登入的桌面:anybot setup --real-screen …"
fi
else
seed_or_pair
printf '\n %s裝好了。%s' "$G$B" "$N"
if [ "$PLAT" = mac ]; then printf '打開 ~/Applications/AnyBot(第一次:按右鍵 › 打開)。\n'
elif [ "$DESKTOP" = 1 ]; then printf '從應用程式選單打開 AnyBot,或在終端機輸入 %sanybot%s。\n' "$B" "$N"
else printf '這台沒有桌面。當手腳:%sanybot setup 網址 配對碼%s。當大腦:%sanybot setup --brain%s。\n' "$B" "$N" "$B" "$N"; fi
if ! have docker; then info "Bot 自己的電腦(一個隔離的桌面)需要 Docker,要用時 AnyBot 會告訴你怎麼裝。"; fi
info "要更新:再執行一次同一行指令。"
fi
case ":$START_PATH:" in *":$HOME/.local/bin:"*) ;; *) info "(終端機找不到 anybot 的話,重新登入,或把 ~/.local/bin 加進 PATH)" ;; esac
;;
esac