diff --git a/Makefile b/Makefile index 42720e0..46debdb 100644 --- a/Makefile +++ b/Makefile @@ -97,7 +97,13 @@ ifeq ($(UNAME),Darwin) mkdir -p "$(APPDIR)" rm -rf "$(APPDIR)/AnyBot.app" mv dist/AnyBot.app "$(APPDIR)/AnyBot.app" + mkdir -p "$(HOME)/.local/bin" + ln -sfn "$(APPDIR)/AnyBot.app/Contents/MacOS/AnyBot" "$(HOME)/.local/bin/anybot" @echo "已安裝 $(APPDIR)/AnyBot.app(第一次打開:按右鍵 › 打開)" +else ifneq ($(findstring MINGW,$(UNAME)),) + powershell.exe -NoProfile -ExecutionPolicy Bypass -File install.ps1 app +else ifeq ($(OS),Windows_NT) + powershell.exe -NoProfile -ExecutionPolicy Bypass -File install.ps1 app else sh deploy/linux/install.sh endif diff --git a/docs/en/install.md b/docs/en/install.md index fe87bd7..ace8268 100644 --- a/docs/en/install.md +++ b/docs/en/install.md @@ -2,7 +2,7 @@ Canonical document: [docs/install.md](../install.md) (Chinese). -`install.sh` does four steps: system packages (per-distro names: apt/dnf/pacman/zypper), toolchain (rustup → `~/.cargo`; Docker for the server mode), sources into `~/AnyBot`, then build + install. No root for the app; it picks desktop vs. hands automatically (Mac / Linux-with-desktop → app, headless Linux → hands). Low-RAM machines compile with fewer parallel jobs (one per 2 GB). Env: `ANYBOT_DIR`, `ANYBOT_BRANCH`. +`install.sh` covers macOS (arm64, x86_64) and Linux of any common distro (apt, dnf, yum, pacman, zypper, apk, xbps — not Ubuntu only). `install.ps1` covers Windows 10/11 x64 and arm64. A terminal gets a short guide (desktop / hands / server) and first-run config (API key or pairing code). Agents: `sh install.sh --print` (one JSON line), then `ANYBOT_NONINTERACTIVE=1` — see [install/AGENT.md](../../install/AGENT.md). Server mode is any Linux with Docker. Hands: real screen on Mac and signed-in Windows, virtual screen (Xvfb) on headless Linux. ## Requirements @@ -11,7 +11,7 @@ Canonical document: [docs/install.md](../install.md) (Chinese). | All | Rust **≥ 1.95** via rustup (apt's is too old), git, make, C compiler | | macOS ≥ 12 | Xcode Command Line Tools (`xcode-select --install`) | | Linux | `build-essential pkg-config libasound2-dev` (+ X11/Wayland, notify, clipboard libs — install.sh installs them) | -| Windows | VS Build Tools (C++); `cargo build --release` works, installer packaging is not done | +| Windows 10/11 x64, arm64 | `install.ps1` (Git + VS Build Tools; winget installs them). Do not host the account server here | ## Make targets diff --git a/docs/install.md b/docs/install.md index 68aed47..d3271e4 100644 --- a/docs/install.md +++ b/docs/install.md @@ -11,7 +11,9 @@ | 3. 原始碼 | 下載到 `~/AnyBot`(已存在就更新) | 同左 | 同左 | | 4. 安裝 | 編譯,放到 `~/Applications`(Mac)或 `~/.local/bin`(Linux) | 編譯、放到 `~/.local/bin`,帶網址和配對碼時接著執行 `anybot setup` | 執行 `deploy/server/setup.sh`:問網域、帳號、密碼,用 Docker 建置並啟動 | -- 系統套件會依發行版挑對的名字(例如 Ubuntu 24.04 的 `libasound2` 叫 `libasound2t64`)。支援 apt(Ubuntu、Debian)、dnf(Fedora)、pacman(Arch)、zypper(openSUSE)。 +- 系統套件會依發行版挑對的名字(例如 Ubuntu 24.04 的 `libasound2` 叫 `libasound2t64`;沒有的名字會跳過)。支援 apt(Debian、Ubuntu、Mint、Raspberry Pi OS…)、dnf / yum(Fedora、RHEL、Rocky、Alma、Amazon Linux)、pacman(Arch、Manjaro)、zypper(openSUSE)、apk(Alpine)、xbps(Void)。NixOS 和 Gentoo 看 `sh install.sh --print` 的 notes。 +- 架構:macOS arm64、x86_64;Linux x86_64、aarch64、armv7、riscv64(glibc 或 musl);Windows x64、arm64。 +- 有終端機會先問要裝桌面、手腳、伺服器或只裝工具,裝完再引導第一次設定(API 金鑰或配對碼)。沒有終端機(代理程式)不要問:`ANYBOT_NONINTERACTIVE=1`,見 [install/AGENT.md](../install/AGENT.md)。`sh install.sh --print` 印出這台適不適合、以及每種模式的一行指令。 - 已經有 Rust 但版本低於 1.95:有 rustup 就更新;是系統套件裝的就另外用 rustup 裝新版。 - 記憶體小的機器會自動減少同時編譯的數量(每 2 GB 一個)。 - 不帶參數時自己判斷:Mac 和有桌面的 Linux 裝桌面 App,沒有桌面的 Linux 裝手腳。 @@ -27,7 +29,7 @@ | 全部 | Rust **1.95 以上**(用 rustup 安裝;apt 裝的通常太舊)、git、make | | macOS 12 以後 | Xcode Command Line Tools(`xcode-select --install`) | | Linux | `build-essential pkg-config libasound2-dev`;執行用的套件見下面 | -| Windows | Visual Studio Build Tools(C++)。可以 `cargo build --release`,安裝包還沒做 | +| Windows 10/11(x64、arm64) | `install.ps1`(或 `install.cmd`)。需要 Git,以及 Visual Studio Build Tools 的 C++(沒有的話腳本會用 winget 裝)。伺服器請改在 Linux 上架 | `make deps` 會把這些都裝好(等於 `./install.sh deps`)。`make doctor` 檢查還缺什麼。 diff --git a/install.cmd b/install.cmd new file mode 100644 index 0000000..e4fa946 --- /dev/null +++ b/install.cmd @@ -0,0 +1,3 @@ +@echo off +rem Double-click or: install.cmd [app|hands|deps] [url code] +powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0install.ps1" %* diff --git a/install.ps1 b/install.ps1 new file mode 100644 index 0000000..adec573 --- /dev/null +++ b/install.ps1 @@ -0,0 +1,307 @@ +# AnyBot Windows 一鍵安裝(x64、arm64)。桌面、手腳、第一次設定。 +# +# irm https://code.30cm.net/daniel.w/AnyBot/raw/branch/main/install.ps1 | iex +# powershell -NoProfile -ExecutionPolicy Bypass -File install.ps1 app +# powershell -File install.ps1 hands https://你的網域 482913 +# powershell -File install.ps1 -Print +# +# 代理程式不要問問題: +# $env:ANYBOT_NONINTERACTIVE='1' +# $env:ANYBOT_MODE='hands' +# $env:ANYBOT_PAIR_URL='https://…' +# $env:ANYBOT_PAIR_CODE='482913' +# irm https://code.30cm.net/daniel.w/AnyBot/raw/branch/main/install.ps1 | iex +# +# 伺服器(帳號+大腦+HTTPS)請在 Linux 上用 install.sh server。Windows 可以當大腦或手腳。 +#Requires -Version 5.1 +param( + [Parameter(Position = 0)][string]$Mode = "", + [Parameter(Position = 1)][string]$PairUrl = "", + [Parameter(Position = 2)][string]$PairCode = "", + [switch]$Print +) + +$ErrorActionPreference = "Stop" +$Repo = if ($env:ANYBOT_REPO) { $env:ANYBOT_REPO } else { "https://code.30cm.net/daniel.w/AnyBot.git" } +$Branch = if ($env:ANYBOT_BRANCH) { $env:ANYBOT_BRANCH } else { "main" } +$Src = if ($env:ANYBOT_DIR) { $env:ANYBOT_DIR } else { Join-Path $env:USERPROFILE "AnyBot" } +$RustMin = 95 + +if (-not $Mode -and $env:ANYBOT_MODE) { $Mode = $env:ANYBOT_MODE } +if (-not $PairUrl -and $env:ANYBOT_PAIR_URL) { $PairUrl = $env:ANYBOT_PAIR_URL } +if (-not $PairCode -and $env:ANYBOT_PAIR_CODE) { $PairCode = $env:ANYBOT_PAIR_CODE } +if ($env:ANYBOT_PRINT -eq "1") { $Print = $true } +if ($Mode -eq "--print" -or $Mode -eq "print" -or $Mode -eq "doctor") { $Print = $true; if ($Mode -eq "doctor") { $script:Doctor = $true } } + +function Say($msg) { Write-Host " $msg" } +function Ok($msg) { Write-Host " ✓ $msg" -ForegroundColor Green } +function Warn($msg) { Write-Host " ! $msg" -ForegroundColor Yellow } +function Die($msg, $hint) { Write-Host "`n ✗ $msg" -ForegroundColor Red; if ($hint) { Write-Host " $hint" }; exit 1 } + +$ArchRaw = $env:PROCESSOR_ARCHITECTURE +if ($env:PROCESSOR_ARCHITEW6432) { $ArchRaw = $env:PROCESSOR_ARCHITEW6432 } +$ArchId = "x86_64" +$RustHost = "x86_64-pc-windows-msvc" +if ($ArchRaw -eq "ARM64") { $ArchId = "arm64"; $RustHost = "aarch64-pc-windows-msvc" } +elseif ($ArchRaw -ne "AMD64") { $ArchId = $ArchRaw; $RustHost = "" } + +$Interactive = (-not $env:ANYBOT_NONINTERACTIVE) -and (-not $env:CI) -and [Environment]::UserInteractive -and -not [Console]::IsInputRedirected + +function Report { + $ok = [bool]$RustHost + if ($script:Doctor) { + Write-Host "AnyBot 安裝檢查 Windows $ArchRaw" + Write-Host " Rust 目標 $(if ($RustHost) { $RustHost } else { '不支援' })" + Write-Host " 建議 app(有桌面)或 hands(讓大腦操控這台登入中的桌面)" + Write-Host " 伺服器 請在 Linux 上架(install.sh server)。這台當大腦:裝完 anybot setup --brain" + return + } + $obj = [ordered]@{ + ok = $ok + os = "Windows" + arch = "$ArchRaw" + arch_id = "$ArchId" + rust_host = "$RustHost" + distro = "Windows" + libc = "msvc" + pm = "winget" + desktop = $true + headless = $false + wsl = $false + systemd = $false + recommended = "app" + server_ok = $false + modes = @("app", "hands", "deps") + notes = @( + "操控的是登入中的 Windows 桌面(工作排程器只在使用者登入時執行)。", + "伺服器請用 Linux 的 install.sh server。" + ) + one_click = @{ + app = "irm https://code.30cm.net/daniel.w/AnyBot/raw/branch/main/install.ps1 | iex" + hands = "`$env:ANYBOT_MODE='hands'; `$env:ANYBOT_PAIR_URL='網址'; `$env:ANYBOT_PAIR_CODE='配對碼'; irm https://code.30cm.net/daniel.w/AnyBot/raw/branch/main/install.ps1 | iex" + server = "curl -fsSL https://code.30cm.net/daniel.w/AnyBot/raw/branch/main/install.sh | sh -s -- server" + print = "powershell -File install.ps1 -Print" + } + } + $obj | ConvertTo-Json -Compress + if (-not $ok) { exit 1 } +} + +if ($Print) { Report; exit 0 } +if (-not $RustHost) { Die "不支援的 Windows 架構:$ArchRaw" "支援 x64 與 arm64。" } + +if (-not $Mode -or $Mode -eq "auto" -or $Mode -eq "guide") { + if ($Interactive) { + Write-Host "" + Write-Host " ◆ AnyBot Windows $ArchRaw" -ForegroundColor Cyan + Write-Host " 1 桌面 App" + Write-Host " 2 手腳(讓另一台的 Bot 操控這台登入中的桌面)" + Write-Host " 3 只裝編譯需要的東西" + Write-Host " 伺服器請在 Linux 上安裝。選 1/2/3 [1]: " -NoNewline + $c = Read-Host + switch ($c) { + "2" { $Mode = "hands" } + "3" { $Mode = "deps" } + default { $Mode = "app" } + } + } else { $Mode = "app" } +} +if ($Mode -eq "server") { Die "Windows 不架帳號伺服器。" "請在任何有 Docker 的 Linux 執行 install.sh server。這台當大腦或手腳即可。" } +if ($Mode -notin @("app", "hands", "deps")) { Die "不認得「$Mode」。" "可以用 app、hands、deps、-Print。" } + +Write-Host "" +Write-Host " ◆ AnyBot 安裝 $Mode · Windows $ArchRaw" -ForegroundColor Cyan + +function Have($name) { [bool](Get-Command $name -ErrorAction SilentlyContinue) } + +function Ensure-WingetPackage($id, $override) { + if (-not (Have "winget")) { return $false } + $args = @("install", "--id", $id, "-e", "--accept-package-agreements", "--accept-source-agreements") + if ($override) { $args += @("--override", $override) } + & winget @args + return $LASTEXITCODE -eq 0 +} + +function Ensure-Git { + if (Have "git") { Ok "git"; return } + Say "安裝 Git…" + if (-not (Ensure-WingetPackage "Git.Git" $null)) { + Die "沒有 git。" "請安裝 https://git-scm.com/download/win 後再開一個 PowerShell。" + } + $git = "C:\Program Files\Git\cmd" + if (Test-Path $git) { $env:Path = "$git;" + $env:Path } + if (-not (Have "git")) { Die "Git 裝了,但這個視窗還沒看到。" "請關掉 PowerShell 再開一次,然後重跑同一行。" } + Ok "git" +} + +function Rust-Ok { + if (-not (Have "rustc")) { return $false } + $v = & rustc --version + if ($v -match "rustc (\d+)\.(\d+)") { + $maj = [int]$Matches[1]; $min = [int]$Matches[2] + return ($maj -gt 1) -or ($maj -eq 1 -and $min -ge $RustMin) + } + return $false +} + +function Ensure-Msvc { + $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" + if (Test-Path $vswhere) { + $found = & $vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath 2>$null + if ($found) { Ok "Visual Studio C++ 工具"; return } + } + if (Have "cl") { Ok "cl.exe"; return } + Say "安裝 Visual Studio Build Tools(C++,編譯 Rust 需要,約幾個 GB)…" + $override = "--wait --passive --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended" + if (-not (Ensure-WingetPackage "Microsoft.VisualStudio.2022.BuildTools" $override)) { + Die "沒有 C++ 編譯器。" "請安裝 Visual Studio Build Tools 的「使用 C++ 的桌面開發」,再開一個 PowerShell:https://visualstudio.microsoft.com/visual-cpp-build-tools/" + } + Ok "已安裝 Build Tools。若接下來 rustc 仍找不到 link.exe,請關掉 PowerShell 再開一次。" +} + +function Ensure-Rust { + $cargoBin = Join-Path $env:USERPROFILE ".cargo\bin" + if (Test-Path $cargoBin) { $env:Path = "$cargoBin;" + $env:Path } + if (Rust-Ok) { Ok "Rust $((rustc --version).Split(' ')[1])"; return } + Ensure-Msvc + if (Have "rustup") { + Say "更新 Rust…" + & rustup update stable + & rustup default stable + } else { + Say "安裝 Rust(rustup,目標 $RustHost)…" + $init = Join-Path $env:TEMP "rustup-init.exe" + Invoke-WebRequest -Uri "https://win.rustup.rs/x86_64" -OutFile $init + if ($ArchId -eq "arm64") { + Invoke-WebRequest -Uri "https://win.rustup.rs/aarch64" -OutFile $init + } + & $init -y --profile minimal --default-host $RustHost + if ($LASTEXITCODE -ne 0) { Die "安裝 Rust 失敗。" "手動:https://rustup.rs" } + } + $env:Path = "$cargoBin;" + $env:Path + if (-not (Rust-Ok)) { Die "Rust 還是太舊或找不到。" "關掉視窗再開,執行 rustup update stable。" } + if (-not ((& rustup target list --installed) -contains $RustHost)) { + & rustup target add $RustHost + } + Ok "Rust $((rustc --version).Split(' ')[1])" +} + +function Find-Source { + $here = $PSScriptRoot + if ($here -and (Test-Path (Join-Path $here "Cargo.toml"))) { + $toml = Get-Content (Join-Path $here "Cargo.toml") -Raw + if ($toml -match 'name = "anybot"') { $script:Src = $here; Ok "用這份原始碼:$here"; return } + } + Ensure-Git + if (Test-Path (Join-Path $Src ".git")) { + Say "更新 $Src …" + & git -C $Src pull --ff-only + if ($LASTEXITCODE -ne 0) { Warn "沒辦法更新,用現有的版本" } + } else { + if (Test-Path $Src) { Die "$Src 已經存在,但不是 AnyBot 的原始碼。" "設 ANYBOT_DIR 換一個資料夾。" } + Say "下載原始碼到 $Src …" + & git clone --depth 1 -b $Branch $Repo $Src + if ($LASTEXITCODE -ne 0) { Die "下載原始碼失敗。" "網路連得到 $Repo 嗎?" } + } + $script:Src = $Src + Ok "原始碼:$Src" +} + +function Build-Anybot { + Say "編譯中,第一次約 5–15 分鐘…" + Push-Location $script:Src + & cargo build --release --bin anybot + $code = $LASTEXITCODE + Pop-Location + if ($code -ne 0) { Die "編譯失敗。" "把最後幾行錯誤貼給維護的人。常見原因:沒有 C++ 建置工具,或記憶體不夠。" } + Ok "編譯完成" +} + +function Install-Bin { + $from = Join-Path $script:Src "target\release\anybot.exe" + if (-not (Test-Path $from)) { Die "找不到 $from" "" } + $destDir = Join-Path $env:LOCALAPPDATA "AnyBot" + New-Item -ItemType Directory -Force -Path $destDir | Out-Null + $dest = Join-Path $destDir "anybot.exe" + Copy-Item $from $dest -Force + $userPath = [Environment]::GetEnvironmentVariable("Path", "User") + if ($userPath -notlike "*$destDir*") { + [Environment]::SetEnvironmentVariable("Path", "$userPath;$destDir", "User") + } + $env:Path = "$destDir;" + $env:Path + $script:Bin = $dest + Ok "已安裝 $dest" +} + +function First-Config { + if ($PairUrl -and $PairCode) { + Say "用配對碼接上大腦,不另外詢問。" + & $script:Bin setup $PairUrl $PairCode + if ($LASTEXITCODE -ne 0) { Die "配對失敗。" "配對碼 10 分鐘、一次。到大腦按「加一台電腦」再拿一組。" } + return + } + if ($env:ANYBOT_PROVIDER) { + Say "寫入第一次的模型設定。" + & $script:Bin setup --seed + if ($LASTEXITCODE -ne 0) { Die "寫入第一次設定失敗。" "" } + return + } + if (-not $Interactive) { + Say "尚未做第一次設定。請設 ANYBOT_PAIR_URL 與 ANYBOT_PAIR_CODE,或 ANYBOT_PROVIDER 與 ANYBOT_API_KEY。" + return + } + Write-Host "" + Write-Host " 第一次設定(可略過)" + Write-Host " 1 稍後在 App 裡設定" + Write-Host " 2 貼上 API 金鑰,建立第一個 Bot" + Write-Host " 3 接到大腦,這台當手腳" + Write-Host " 選 1/2/3 [1]: " -NoNewline + switch (Read-Host) { + "2" { + $env:ANYBOT_PROVIDER = Read-Host " 服務 openai / anthropic / xai / gemini / openrouter / opencode / ollama / custom" + $sec = Read-Host " API 金鑰(ollama 可直接 Enter)" -AsSecureString + $env:ANYBOT_API_KEY = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($sec)) + & $script:Bin setup --seed + } + "3" { + $u = Read-Host " 網址" + $c = Read-Host " 配對碼" + if ($u -and $c) { & $script:Bin setup $u $c } + } + } +} + +if ($Mode -eq "deps") { + Ensure-Git + Ensure-Rust + Write-Host "`n 好了。在原始碼資料夾:cargo build --release --bin anybot" -ForegroundColor Green + exit 0 +} + +Ensure-Git +Ensure-Rust +Find-Source +Build-Anybot +Install-Bin + +if ($Mode -eq "hands") { + if (-not $PairUrl -and $Interactive) { + Write-Host "" + Write-Host " 把這台接到大腦(AnyBot 按「加一台電腦」)" + $PairUrl = Read-Host " 網址(直接 Enter 先跳過)" + if ($PairUrl) { $PairCode = Read-Host " 配對碼" } + } + if ($PairUrl -and $PairCode) { + & $script:Bin setup $PairUrl $PairCode + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + } else { + Write-Host "`n 裝好了。接著:anybot setup 網址 配對碼" -ForegroundColor Green + Say "配對後會在登入時自動執行。Bot 操控的是這個已登入的桌面,不是鎖定畫面。" + } +} else { + First-Config + Write-Host "`n 裝好了。從開始功能表找不到時,直接執行:" -ForegroundColor Green + Write-Host " $($script:Bin)" + Say "要讓 Bot 操控這台:在大腦按「加一台電腦」,用裡面的 Windows 那一行。" +} +Say "要更新:再執行一次同一行指令。" diff --git a/install.sh b/install.sh index 7a58d2f..2cee4e7 100755 --- a/install.sh +++ b/install.sh @@ -1,43 +1,74 @@ #!/bin/sh -# AnyBot 一鍵安裝。什麼都沒裝的電腦也可以:會自己裝好需要的東西(系統套件、Rust、Docker)。 +# AnyBot 一鍵安裝。什麼都沒裝的電腦也可以:會自己裝好需要的東西,並做第一次設定。 # # curl -fsSL https://code.30cm.net/daniel.w/AnyBot/raw/branch/main/install.sh | sh -# 桌面 App(Mac、有螢幕的 Linux)。沒有螢幕的 Linux 會自動改成「手腳」 +# 有終端機時會問要裝哪一種,並引導第一次設定。沒有終端機時自己判斷。 +# … | sh -s -- app +# 桌面 App(Mac、有螢幕的 Linux:Debian、Ubuntu、Fedora、RHEL、Arch、openSUSE、Alpine…) # … | sh -s -- hands [網址 配對碼] -# 沒有螢幕的 Linux:裝好後接成大腦的手腳(網址和配對碼在 AnyBot 的「加一台電腦」) +# 手腳:讓大腦操控這台(有沒有螢幕都可以)。Mac 用真實螢幕;Linux 沒螢幕會裝虛擬螢幕。 # … | sh -s -- server -# 伺服器:裝 Docker,架好帳號伺服器 + 大腦 + HTTPS(只問網域、帳號、密碼) +# 伺服器:任何有 Docker 的 Linux,帳號伺服器 + 大腦 + HTTPS # … | sh -s -- deps -# 只裝建置需要的東西(之後自己 make) +# 只裝建置需要的東西 +# … | sh -s -- --print +# 不安裝,印出這台的架構和一鍵指令(給代理程式解析,純 JSON) # -# 已經 clone 下來的話,在資料夾裡執行 ./install.sh [app|hands|server|deps],效果一樣。 -# 環境變數:ANYBOT_DIR(放原始碼的地方,預設 ~/AnyBot)、ANYBOT_BRANCH(預設 main)。 +# 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} -RUST_MIN=95 # Rust 1.95 以上 +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 -MODE=${1:-auto} -[ $# -gt 0 ] && shift +# 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) ;; + app|hands|headless|server|deps|auto|guide|--print|print|doctor) ;; -h|--help|help) - # 用 curl | sh 執行時 $0 是「sh」,讀不到腳本原文:改印內嵌的說明。 - usage=$(sed -n '2,14p' "$0" 2>/dev/null | sed 's/^# \{0,1\}//' || true) - [ -n "$usage" ] || usage='AnyBot 一鍵安裝: - sh install.sh 桌面 App(Mac、有螢幕的 Linux) - sh install.sh hands [網址 配對碼] 沒有螢幕的 Linux,接成大腦的手腳 - sh install.sh server 伺服器:帳號伺服器 + 大腦 + HTTPS(Docker) - sh install.sh deps 只裝建置需要的東西(之後自己 make) -環境變數:ANYBOT_DIR(原始碼位置,預設 ~/AnyBot)、ANYBOT_BRANCH(預設 main)。' + 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"; exit 1 ;; + *) 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 @@ -47,56 +78,218 @@ 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'; } -# 互動輸入:用 curl | sh 時標準輸入是腳本本身,改從終端機讀。 TTY=/dev/tty [ -r "$TTY" ] && [ -w "$TTY" ] && (exec <"$TTY") 2>/dev/null || TTY= - -# PATH 提示要看的是使用者登入後的 PATH,不是腳本自己改過的(下面會加 ~/.local/bin)。 +# 代理程式、CI、沒有終端機:不要問。有 ANYBOT_NONINTERACTIVE 也不問。 +INTERACTIVE=0 +if [ -n "$TTY" ] && [ -z "${ANYBOT_NONINTERACTIVE:-}" ] && [ -z "${CI:-}" ]; then INTERACTIVE=1; fi START_PATH=$PATH # ---------- 這台是什麼 ---------- -OS=$(uname -s); ARCH=$(uname -m) +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:照 docs/install.md 用 cargo build --release。" ;; + *) die "這個腳本只支援 macOS 和 Linux(含各種發行版)。" "Windows:irm $PS1_URL | iex" ;; esac -if [ "$MODE" = auto ]; then - if [ $PLAT = linux ] && [ -z "${DISPLAY:-}${WAYLAND_DISPLAY:-}" ] && ! have gnome-shell && ! have plasmashell && ! have startxfce4; then - MODE=hands - else - MODE=app - fi + +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 -[ $PLAT = mac ] && [ "$MODE" = hands ] && MODE=app SUDO= -if [ $PLAT = linux ] && [ "$(id -u)" != 0 ]; then - have sudo || die "需要 sudo 才能安裝系統套件。" "請用 root 執行,或先裝 sudo。" - SUDO=sudo -fi -PM= -if [ $PLAT = linux ]; then - for p in apt-get dnf pacman zypper; do have $p && { PM=$p; break; }; done +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="沒有螢幕的電腦(手腳)" ;; + 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:只裝這個發行版有的名字(例如 Ubuntu 24.04 的 libasound2 叫 libasound2t64)。 -# 最後加 true:清單最後一個套件不存在時,最後一条 apt-cache 判斷會失敗, -# 在 set -e 下整個腳本會無訊息退出(呼叫端是 pkgs=$(apt_pick …))。 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 ;; @@ -105,78 +298,160 @@ apt_pick() { done true } - -# zypper:同 apt_pick,裝之前先過濾掉這台找不到的名字(SUSE 各版本套件名差異大, -# 一個名字不存在會讓整組安裝失敗)。 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) set -- curl ca-certificates git ;; - build) set -- build-essential pkg-config git curl ca-certificates libasound2-dev ;; - # 手腳:虛擬螢幕、視窗管理員、輔助使用服務(讀 App 的按鈕)、session bus,Bot 才看得到這台的畫面。 - hands) set -- build-essential pkg-config git curl ca-certificates libasound2-dev "libasound2t64|libasound2" \ - xvfb openbox at-spi2-core libatk-adaptor dbus-x11 dbus-user-session xauth fonts-noto-cjk ;; - app) set -- build-essential pkg-config git curl ca-certificates libasound2-dev "libasound2t64|libasound2" \ - libxkbcommon-x11-0 "libgl1|libgl1-mesa-glx" xdg-utils libnotify-bin libsecret-tools espeak-ng zenity \ - xclip wl-clipboard ffmpeg fonts-noto-cjk ;; + 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 - # A fresh system has no package lists yet: fetch them first, or nothing can be picked. if ! ls /var/lib/apt/lists/*_Packages >/dev/null 2>&1; then - $SUDO apt-get update -qq || warn "apt-get update 有錯誤,繼續試試看" + apt_get update -qq || warn "apt-get update 有錯誤,繼續試試看" UPDATED=1 fi - pkgs=$(apt_pick "$@" | tr '\n' ' ') - # Already there (an update): nothing to install, no sudo password needed. + # 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 + if [ -z "$missing" ]; then info "系統套件都已經裝好了"; return 0; fi pkgs=$missing - [ -n "${UPDATED:-}" ] || $SUDO apt-get update -qq || warn "apt-get update 有錯誤,繼續試試看" + [ -n "${UPDATED:-}" ] || apt_get update -qq || warn "apt-get update 有錯誤,繼續試試看" info "apt-get install$pkgs" - $SUDO env DEBIAN_FRONTEND=noninteractive apt-get install -y -qq $pkgs >/dev/null || die "安裝系統套件失敗。" "請看上面的錯誤,或手動執行:sudo apt-get install $pkgs" + apt_get install -y -qq $pkgs >/dev/null || die "安裝系統套件失敗。" "請看上面的錯誤,或手動:sudo apt-get install $pkgs" ;; - dnf) + dnf|yum) base="gcc gcc-c++ make pkgconf-pkg-config git curl ca-certificates alsa-lib-devel" extra="" - [ "$1" = app ] && extra="libxkbcommon-x11 mesa-libGL xdg-utils libnotify libsecret espeak-ng zenity xclip wl-clipboard ffmpeg-free google-noto-sans-cjk-fonts" + [ "$1" = 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" - $SUDO dnf install -y -q $base $extra || die "安裝系統套件失敗。" "sudo dnf install $base $extra" + [ "$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-x11 mesa xdg-utils libnotify libsecret espeak-ng zenity xclip wl-clipboard ffmpeg noto-fonts-cjk" + [ "$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" - $SUDO pacman -S --needed --noconfirm $base || die "安裝系統套件失敗。" "sudo pacman -S --needed $base" + [ "$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="" - # app/hands 也要有這台该有的套件(之前只有 base,桌面模式缺 xkbcommon/GL/通知…, - # 編譯或執行時才炸)。名字逐個過濾,找不到的跳過。 - [ "$1" = app ] && extra="libxkbcommon-x11-0 Mesa libGL1 xdg-utils libnotify-tools libnotify4 libsecret-1-0 espeak zenity xclip wl-clipboard ffmpeg fonts-noto" + [ "$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" + [ "$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" + ;; *) - die "認不出這台的套件管理程式(apt、dnf、pacman、zypper 都沒有)。" \ - "請自己裝:C 編譯器、pkg-config、git、curl、ALSA 開發套件,再執行一次。" + 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,正在打開安裝視窗…" @@ -187,27 +462,31 @@ mac_tools() { # ---------- Rust ---------- rust_ok() { have cargo && have rustc || return 1 - # 抓「主版號 次版號」:舊寫法死鎖在 1.x,rustc 2.x 會被当成「太舊」。 - set -- $(rustc --version 2>/dev/null | sed -n 's/^rustc \([0-9][0-9]*\)\.\([0-9][0-9]*\).*/\1 \2/p') - [ $# -ge 2 ] || return 1 - [ "$1" -gt 1 ] && return 0 - [ "$1" -eq 1 ] && [ "$2" -ge "$RUST_MIN" ] + 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" - if rust_ok; then ok "Rust $(rustc --version | cut -d' ' -f2)"; return; fi + 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 - # rustup 裝在 ~/.cargo/bin;這個終端機可能還沒有它(--no-modify-path 裝的)。 [ -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,裝在 ~/.cargo,不用 root)…" - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y -q --profile minimal --no-modify-path >/dev/null 2>&1 \ + 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:"*) ;; *) @@ -231,21 +510,23 @@ find_source() { else [ -e "$DIR" ] && die "$DIR 已經存在,但不是 AnyBot 的原始碼。" "換個地方:ANYBOT_DIR=~/別的資料夾 再執行一次。" info "下載原始碼到 $DIR …" - git clone -q --depth 1 -b "$BRANCH" "$REPO" "$DIR" || die "下載原始碼失敗。" "網路連得到 $REPO 嗎?" + 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))" } -# 記憶體小的機器同時編太多會被系統砍掉:每 2 GB 一個工作。 build_jobs() { mem_mb=0 if [ -r /proc/meminfo ]; then mem_mb=$(awk '/MemTotal/ {m=$2} /SwapTotal/ {s=$2} END {print int((m + s / 2) / 1024)}' /proc/meminfo) + elif [ "$PLAT" = mac ]; then + mem_mb=$(( $(sysctl -n hw.memsize 2>/dev/null || echo 0) / 1024 / 1024 )) fi - [ "$mem_mb" -gt 0 ] || { echo ""; return; } - j=$((mem_mb / 2000)); [ $j -lt 1 ] && j=1 - cpus=$(nproc 2>/dev/null || echo $j); [ $j -gt "$cpus" ] && j=$cpus - echo $j + [ "${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 @@ -254,27 +535,34 @@ build_jobs() { build() { jobs=$(build_jobs) - info "編譯中,第一次約 5–15 分鐘(之後更新只重編改過的部分)…" + 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 + 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 的「常見問題」。" + die "編譯失敗(完整紀錄:${log})" "把紀錄的最後幾行貼給維護的人;常見原因見 docs/install.md。" fi } -# ---------- Docker(伺服器)---------- ensure_docker() { if ! have docker; then info "安裝 Docker(官方腳本 get.docker.com)…" - curl -fsSL https://get.docker.com | $SUDO sh >/dev/null || die "安裝 Docker 失敗。" "手動:https://docs.docker.com/engine/install/" + 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 - $SUDO systemctl enable --now docker >/dev/null 2>&1 || true docker compose version >/dev/null 2>&1 || $SUDO docker compose version >/dev/null 2>&1 || { case $PM in - apt-get) $SUDO apt-get install -y -qq docker-compose-plugin >/dev/null 2>&1 || $SUDO apt-get install -y -qq docker-compose-v2 >/dev/null 2>&1 || true ;; - dnf) $SUDO dnf install -y -q docker-compose-plugin >/dev/null 2>&1 || true ;; + 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/" @@ -288,41 +576,117 @@ ensure_docker() { 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 + if [ "$PLAT" = mac ]; then mac_tools; else linux_packages app; ok "系統套件"; fi step "Rust" ensure_rust printf '\n %s好了。%s在原始碼資料夾裡:%smake%s 建置、%smake install%s 安裝。\n' "$G$B" "$N" "$B" "$N" "$B" "$N" - [ -n "${PS1:-}" ] || info "這個終端機還找不到 cargo 的話:. \"\$HOME/.cargo/env\"" + info "這個終端機還找不到 cargo 的話:. \"\$HOME/.cargo/env\"" ;; server) - [ $PLAT = linux ] || die "伺服器模式要在 Linux 上跑。" "Mac 當大腦的做法見 docs/multi-device.md。" + [ "$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 "接下來會問網域、帳號、密碼。" + 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 "需要一個終端機回答問題。" "在伺服器上直接執行:cd $SRC && ./install.sh server(或用 ANYBOT_SETUP_USER、ANYBOT_SETUP_PASSWORD 免問)"; fi - if [ "${DOCKER_SUDO:-0}" = 1 ]; then exec $SUDO -E ./setup.sh "$@" <"$IN"; else exec ./setup.sh "$@" <"$IN"; fi + 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 && [ $# -gt 0 ]; then ok "AnyBot 已經裝好,跳過" - else linux_packages $MODE; ok "系統套件"; fi + 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 && [ $# -gt 0 ]; then - STEP=3; info "已經有 $(command -v anybot),直接接上。(要更新:不帶網址再執行一次)" + if [ "$MODE" = hands ] && have anybot && [ -n "$SAVED_1" ]; then + STEP=3 + info "已經有 $(command -v anybot),直接接上。(要更新:不帶網址再執行一次)" else step "Rust" ensure_rust @@ -330,37 +694,68 @@ case $MODE in find_source build step "安裝" - if [ $PLAT = mac ]; then + if [ "$PLAT" = mac ]; then (cd "$SRC" && make install >/dev/null) || die "安裝失敗。" "在 $SRC 執行 make install 看錯誤。" - ok "AnyBot.app 放在 ~/Applications" + 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 ] && ok "應用程式選單裡有 AnyBot" - # 更新: the one running in the background (anybot setup's service) switches to the new one. - if [ -f "$HOME/.local/bin/anybot-run" ] && systemctl --user is-enabled --quiet anybot 2>/dev/null; then + [ "$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 [ $# -gt 0 ]; 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' - [ -n "$TTY" ] && exec anybot setup "$@" <"$TTY" - exec anybot setup "$@" + 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 ' 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' - else printf '從應用程式選單打開 AnyBot,或在終端機輸入 %sanybot%s。\n' "$B" "$N"; fi + 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:"*) ;; *) [ $PLAT = linux ] && info "(終端機找不到 anybot 的話,重新登入,或把 ~/.local/bin 加進 PATH)" ;; esac + case ":$START_PATH:" in *":$HOME/.local/bin:"*) ;; *) info "(終端機找不到 anybot 的話,重新登入,或把 ~/.local/bin 加進 PATH)" ;; esac ;; esac diff --git a/install/AGENT.md b/install/AGENT.md new file mode 100644 index 0000000..f87b1ff --- /dev/null +++ b/install/AGENT.md @@ -0,0 +1,60 @@ +# 給代理程式的一鍵安裝 + +不要問使用者選單。先看這台是什麼,再執行一行。沒有終端機回答時一定要設 `ANYBOT_NONINTERACTIVE=1`,否則腳本會停下來等。 + +## 1. 看這台 + +```sh +curl -fsSL https://code.30cm.net/daniel.w/AnyBot/raw/branch/main/install.sh | sh -s -- --print +``` + +stdout 是一行 JSON:`os`、`arch`、`rust_host`、`distro`、`pm`、`desktop`、`recommended`、`server_ok`、`notes`、`one_click`。`ok: false` 就不要裝,把 `notes` 告訴使用者。 + +已經 clone 時:`sh install.sh --print`。Windows:`powershell -File install.ps1 -Print`。 + +## 2. 裝哪一種 + +| 要什麼 | 系統 | 一行 | +|---|---|---| +| 桌面 App | macOS(arm64、x86_64)、任何 Linux 有桌面 | `curl -fsSL …/install.sh \| sh -s -- app` | +| 手腳(操控螢幕、檔案、瀏覽器) | macOS、任何 Linux(含沒螢幕)、Windows 已登入桌面 | 見下面 | +| 伺服器(帳號+大腦+HTTPS) | 任何有 Docker 的 Linux(不限 Ubuntu) | `ANYBOT_NONINTERACTIVE=1 ANYBOT_SETUP_USER=… ANYBOT_SETUP_PASSWORD=… ANYBOT_SETUP_DOMAIN=網域 sh install.sh server` | +| 只裝編譯工具 | 同上 | `sh install.sh deps` | + +Linux 套件管理員:apt、dnf、yum、pacman、zypper、apk、xbps。認不出來但已有 gcc 和 pkg-config 會繼續編譯。NixOS / Gentoo 看 `--print` 的 `notes`,進好 shell 後加 `ANYBOT_SKIP_PACKAGES=1`。 + +架構:macOS arm64 / x86_64;Linux x86_64、aarch64、armv7、riscv64(glibc 或 musl);Windows x64 / arm64。32 位元 x86 不支援。 + +## 3. 手腳(含第一次配對,不要問) + +配對碼在大腦的 AnyBot 按「加一台電腦」。10 分鐘、一次。 + +```sh +# macOS、任何 Linux(沒螢幕會裝虛擬螢幕、openbox、AT-SPI) +ANYBOT_NONINTERACTIVE=1 curl -fsSL https://code.30cm.net/daniel.w/AnyBot/raw/branch/main/install.sh | sh -s -- hands https://你的網域 482913 +``` + +```powershell +# Windows(操控的是登入中的桌面,不是 Session 0) +$env:ANYBOT_NONINTERACTIVE='1' +$env:ANYBOT_MODE='hands' +$env:ANYBOT_PAIR_URL='https://你的網域' +$env:ANYBOT_PAIR_CODE='482913' +irm https://code.30cm.net/daniel.w/AnyBot/raw/branch/main/install.ps1 | iex +``` + +配對成功後:Linux 用 systemd 使用者服務(沒有 systemd 會直接拉起 `anybot-run`);macOS 用 launchd;Windows 用工作排程器「登入時」。登出後還要跑的 Linux:`sudo loginctl enable-linger $USER`(免密碼 sudo 時安裝程式會自己做)。 + +## 4. 桌面的第一次金鑰(不要問) + +```sh +ANYBOT_NONINTERACTIVE=1 ANYBOT_PROVIDER=openai ANYBOT_API_KEY=sk-… sh install.sh app +``` + +`ANYBOT_PROVIDER`:`openai`、`anthropic`、`xai`、`gemini`、`openrouter`、`opencode`、`ollama`、`custom`。自訂位址加 `ANYBOT_BASE_URL`。沒設這兩個,有終端機才會問;沒有終端機就只裝好,並把缺的變數印出來。 + +## 5. 不要做的事 + +- 不要在 WSL 裡裝了就以為能操控 Windows 視窗。WSL 是 Linux 手腳;Windows 桌面要另外跑 `install.ps1`。 +- 不要在 Mac / Windows 上跑 `install.sh server`。伺服器是 Linux + Docker。 +- 不要用 `set` 改安裝腳本的參數。網址和配對碼要原樣傳到 `anybot setup`。 diff --git a/mobile/index.html b/mobile/index.html index 2517c69..7e78d82 100644 --- a/mobile/index.html +++ b/mobile/index.html @@ -1966,10 +1966,10 @@ $("k-right").onclick = () => { S.right = !S.right; $("k-right").textContent = " // ---------- 加一台電腦, notifications ---------- // A code for a new computer (ten minutes, once), as hands for this AnyBot. async function pair() { - const el = sheet("加一台電腦", [], `
正在產生…`);
+ const el = sheet("加一台電腦", [], `正在產生…`);
try {
const v = await api("relay/pair", {json: {for: S.host}});
- const line = `curl -fsSL https://code.30cm.net/daniel.w/AnyBot/raw/branch/main/install.sh | sh -s -- hands ${location.origin} ${v.code}`;
+ const line = `Windows PowerShell:\n$env:ANYBOT_NONINTERACTIVE='1'; $env:ANYBOT_MODE='hands'; $env:ANYBOT_PAIR_URL='${location.origin}'; $env:ANYBOT_PAIR_CODE='${v.code}'; irm https://code.30cm.net/daniel.w/AnyBot/raw/branch/main/install.ps1 | iex\n\nMac / Linux(任何發行版,沒有螢幕也可以):\ncurl -fsSL https://code.30cm.net/daniel.w/AnyBot/raw/branch/main/install.sh | sh -s -- hands ${location.origin} ${v.code}`;
el.querySelector("#pair-code").textContent = v.code.slice(0, 3) + " " + v.code.slice(3);
el.querySelector("#pair-line").textContent = line;
el.querySelector("#pair-copy").onclick = () => copyText(line);
diff --git a/src/app.rs b/src/app.rs
index 26ebd80..ea46174 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -4371,6 +4371,7 @@ fn system_prompt(b: &Bot, st: &State, computers: &[tools::Target], default_compu
s.push_str("\n\n你做過的頁面(更新時用 write_page 帶 id,先 read_page 看現在的內容):\n");
s.push_str(&mine.join("\n"));
}
+ s.push_str("\n- 要再加一台手腳(Mac、任何 Linux,含沒有桌面的、已登入的 Windows):請使用者在 AnyBot 按「加一台電腦」,把對應的那一行貼到那台。還沒裝也會裝好並完成第一次配對。代理程式不要問:Mac/Linux 是 `curl -fsSL https://code.30cm.net/daniel.w/AnyBot/raw/branch/main/install.sh | sh -s -- hands 網址 配對碼`;Windows PowerShell 是 `$env:ANYBOT_MODE='hands'; $env:ANYBOT_PAIR_URL='網址'; $env:ANYBOT_PAIR_CODE='配對碼'; irm https://code.30cm.net/daniel.w/AnyBot/raw/branch/main/install.ps1 | iex`。先看那台:同一支腳本加 `--print`。\n");
if !computers.is_empty() {
let list: Vec