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