AnyBot/install.sh

762 lines
37 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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