762 lines
37 KiB
Bash
Executable File
762 lines
37 KiB
Bash
Executable File
#!/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
|