One installer for every platform, first run included
install.sh now covers every common Linux distro (apt, dnf, yum, pacman, zypper, apk, xbps), macOS on arm64/x86_64, and hands off to install.ps1 under Git Bash. install.ps1 + install.cmd give Windows a double-click. Autostart: systemd user unit on Linux, launchd on macOS, Task Scheduler on Windows. A terminal guides the role choice and the first key or bot; agents get --print JSON and ANYBOT_NONINTERACTIVE=1 with env seeding. `加一台電腦` shows the Windows and the Mac/Linux line together.
This commit is contained in:
parent
a514b33fb3
commit
f006aeee35
6
Makefile
6
Makefile
|
|
@ -97,7 +97,13 @@ ifeq ($(UNAME),Darwin)
|
||||||
mkdir -p "$(APPDIR)"
|
mkdir -p "$(APPDIR)"
|
||||||
rm -rf "$(APPDIR)/AnyBot.app"
|
rm -rf "$(APPDIR)/AnyBot.app"
|
||||||
mv dist/AnyBot.app "$(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(第一次打開:按右鍵 › 打開)"
|
@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
|
else
|
||||||
sh deploy/linux/install.sh
|
sh deploy/linux/install.sh
|
||||||
endif
|
endif
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
Canonical document: [docs/install.md](../install.md) (Chinese).
|
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
|
## 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 |
|
| All | Rust **≥ 1.95** via rustup (apt's is too old), git, make, C compiler |
|
||||||
| macOS ≥ 12 | Xcode Command Line Tools (`xcode-select --install`) |
|
| 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) |
|
| 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
|
## Make targets
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,9 @@
|
||||||
| 3. 原始碼 | 下載到 `~/AnyBot`(已存在就更新) | 同左 | 同左 |
|
| 3. 原始碼 | 下載到 `~/AnyBot`(已存在就更新) | 同左 | 同左 |
|
||||||
| 4. 安裝 | 編譯,放到 `~/Applications`(Mac)或 `~/.local/bin`(Linux) | 編譯、放到 `~/.local/bin`,帶網址和配對碼時接著執行 `anybot setup` | 執行 `deploy/server/setup.sh`:問網域、帳號、密碼,用 Docker 建置並啟動 |
|
| 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 裝新版。
|
- 已經有 Rust 但版本低於 1.95:有 rustup 就更新;是系統套件裝的就另外用 rustup 裝新版。
|
||||||
- 記憶體小的機器會自動減少同時編譯的數量(每 2 GB 一個)。
|
- 記憶體小的機器會自動減少同時編譯的數量(每 2 GB 一個)。
|
||||||
- 不帶參數時自己判斷:Mac 和有桌面的 Linux 裝桌面 App,沒有桌面的 Linux 裝手腳。
|
- 不帶參數時自己判斷:Mac 和有桌面的 Linux 裝桌面 App,沒有桌面的 Linux 裝手腳。
|
||||||
|
|
@ -27,7 +29,7 @@
|
||||||
| 全部 | Rust **1.95 以上**(用 rustup 安裝;apt 裝的通常太舊)、git、make |
|
| 全部 | Rust **1.95 以上**(用 rustup 安裝;apt 裝的通常太舊)、git、make |
|
||||||
| macOS 12 以後 | Xcode Command Line Tools(`xcode-select --install`) |
|
| macOS 12 以後 | Xcode Command Line Tools(`xcode-select --install`) |
|
||||||
| Linux | `build-essential pkg-config libasound2-dev`;執行用的套件見下面 |
|
| 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` 檢查還缺什麼。
|
`make deps` 會把這些都裝好(等於 `./install.sh deps`)。`make doctor` 檢查還缺什麼。
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
@echo off
|
||||||
|
rem Double-click or: install.cmd [app|hands|deps] [url code]
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0install.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 "要更新:再執行一次同一行指令。"
|
||||||
637
install.sh
637
install.sh
|
|
@ -1,43 +1,74 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# AnyBot 一鍵安裝。什麼都沒裝的電腦也可以:會自己裝好需要的東西(系統套件、Rust、Docker)。
|
# AnyBot 一鍵安裝。什麼都沒裝的電腦也可以:會自己裝好需要的東西,並做第一次設定。
|
||||||
#
|
#
|
||||||
# curl -fsSL https://code.30cm.net/daniel.w/AnyBot/raw/branch/main/install.sh | sh
|
# 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 [網址 配對碼]
|
# … | sh -s -- hands [網址 配對碼]
|
||||||
# 沒有螢幕的 Linux:裝好後接成大腦的手腳(網址和配對碼在 AnyBot 的「加一台電腦」)
|
# 手腳:讓大腦操控這台(有沒有螢幕都可以)。Mac 用真實螢幕;Linux 沒螢幕會裝虛擬螢幕。
|
||||||
# … | sh -s -- server
|
# … | sh -s -- server
|
||||||
# 伺服器:裝 Docker,架好帳號伺服器 + 大腦 + HTTPS(只問網域、帳號、密碼)
|
# 伺服器:任何有 Docker 的 Linux,帳號伺服器 + 大腦 + HTTPS
|
||||||
# … | sh -s -- deps
|
# … | sh -s -- deps
|
||||||
# 只裝建置需要的東西(之後自己 make)
|
# 只裝建置需要的東西
|
||||||
|
# … | sh -s -- --print
|
||||||
|
# 不安裝,印出這台的架構和一鍵指令(給代理程式解析,純 JSON)
|
||||||
#
|
#
|
||||||
# 已經 clone 下來的話,在資料夾裡執行 ./install.sh [app|hands|server|deps],效果一樣。
|
# Windows(x64、arm64)不要用這支,用 install.ps1(在 Git Bash 裡執行這支會自動交過去):
|
||||||
# 環境變數:ANYBOT_DIR(放原始碼的地方,預設 ~/AnyBot)、ANYBOT_BRANCH(預設 main)。
|
# 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
|
set -eu
|
||||||
|
|
||||||
REPO=${ANYBOT_REPO:-https://code.30cm.net/daniel.w/AnyBot.git}
|
REPO=${ANYBOT_REPO:-https://code.30cm.net/daniel.w/AnyBot.git}
|
||||||
BRANCH=${ANYBOT_BRANCH:-main}
|
BRANCH=${ANYBOT_BRANCH:-main}
|
||||||
DIR=${ANYBOT_DIR:-$HOME/AnyBot}
|
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}
|
# Git Bash / MSYS / Cygwin:這支 sh 裝不好 Windows 的手腳,交給 PowerShell。
|
||||||
[ $# -gt 0 ] && shift
|
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
|
case $MODE in
|
||||||
app|hands|headless|server|deps|auto) ;;
|
app|hands|headless|server|deps|auto|guide|--print|print|doctor) ;;
|
||||||
-h|--help|help)
|
-h|--help|help)
|
||||||
# 用 curl | sh 執行時 $0 是「sh」,讀不到腳本原文:改印內嵌的說明。
|
usage=$(sed -n '2,22p' "$0" 2>/dev/null | sed 's/^# \{0,1\}//' || true)
|
||||||
usage=$(sed -n '2,14p' "$0" 2>/dev/null | sed 's/^# \{0,1\}//' || true)
|
[ -n "$usage" ] || usage='AnyBot 一鍵安裝:sh install.sh [app|hands|server|deps]。代理程式:sh install.sh --print'
|
||||||
[ -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)。'
|
|
||||||
printf '%s\n' "$usage"; exit 0 ;;
|
printf '%s\n' "$usage"; exit 0 ;;
|
||||||
*) echo "不認得「${MODE}」。可以用:app、hands、server、deps"; exit 1 ;;
|
*) echo "不認得「${MODE}」。可以用:app、hands、server、deps、--print"; exit 1 ;;
|
||||||
esac
|
esac
|
||||||
[ "$MODE" = headless ] && MODE=hands
|
[ "$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')
|
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
|
else B=; D=; G=; Y=; R=; C=; N=; fi
|
||||||
STEP=0; TOTAL=4
|
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"; }
|
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; }
|
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; }
|
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
|
TTY=/dev/tty
|
||||||
[ -r "$TTY" ] && [ -w "$TTY" ] && (exec <"$TTY") 2>/dev/null || TTY=
|
[ -r "$TTY" ] && [ -w "$TTY" ] && (exec <"$TTY") 2>/dev/null || TTY=
|
||||||
|
# 代理程式、CI、沒有終端機:不要問。有 ANYBOT_NONINTERACTIVE 也不問。
|
||||||
# PATH 提示要看的是使用者登入後的 PATH,不是腳本自己改過的(下面會加 ~/.local/bin)。
|
INTERACTIVE=0
|
||||||
|
if [ -n "$TTY" ] && [ -z "${ANYBOT_NONINTERACTIVE:-}" ] && [ -z "${CI:-}" ]; then INTERACTIVE=1; fi
|
||||||
START_PATH=$PATH
|
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
|
case $OS in
|
||||||
Darwin) PLAT=mac ;;
|
Darwin) PLAT=mac ;;
|
||||||
Linux) PLAT=linux ;;
|
Linux) PLAT=linux ;;
|
||||||
*) die "這個腳本只支援 macOS 和 Linux。" "Windows:照 docs/install.md 用 cargo build --release。" ;;
|
*) die "這個腳本只支援 macOS 和 Linux(含各種發行版)。" "Windows:irm $PS1_URL | iex" ;;
|
||||||
esac
|
esac
|
||||||
if [ "$MODE" = auto ]; then
|
|
||||||
if [ $PLAT = linux ] && [ -z "${DISPLAY:-}${WAYLAND_DISPLAY:-}" ] && ! have gnome-shell && ! have plasmashell && ! have startxfce4; then
|
DESKTOP=0
|
||||||
MODE=hands
|
if [ "$PLAT" = mac ]; then DESKTOP=1; fi
|
||||||
else
|
if [ -n "${DISPLAY:-}${WAYLAND_DISPLAY:-}${XDG_CURRENT_DESKTOP:-}" ]; then DESKTOP=1; fi
|
||||||
MODE=app
|
if [ -d /mnt/wslg ] || [ -n "${WAYLAND_DISPLAY:-}" ]; then DESKTOP=1; fi
|
||||||
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
|
fi
|
||||||
[ $PLAT = mac ] && [ "$MODE" = hands ] && MODE=app
|
|
||||||
|
|
||||||
SUDO=
|
SUDO=
|
||||||
if [ $PLAT = linux ] && [ "$(id -u)" != 0 ]; then
|
if [ "$PLAT" = linux ] && [ "$(id -u)" != 0 ] && [ "${ANYBOT_SKIP_PACKAGES:-}" != 1 ]; then
|
||||||
have sudo || die "需要 sudo 才能安裝系統套件。" "請用 root 執行,或先裝 sudo。"
|
if [ -n "$PM" ]; then
|
||||||
|
have sudo || die "需要 sudo 才能安裝系統套件。" "請用 root 執行,或先裝 sudo,或套件裝好後 ANYBOT_SKIP_PACKAGES=1。"
|
||||||
SUDO=sudo
|
SUDO=sudo
|
||||||
fi
|
if [ "$INTERACTIVE" = 0 ]; then
|
||||||
PM=
|
$SUDO -n true >/dev/null 2>&1 || die "沒有終端機,sudo 卻要密碼。" "請使用者先 sudo true,或用 root,或 ANYBOT_SKIP_PACKAGES=1。"
|
||||||
if [ $PLAT = linux ]; then
|
fi
|
||||||
for p in apt-get dnf pacman zypper; do have $p && { PM=$p; break; }; done
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
case $MODE in
|
case $MODE in
|
||||||
app) TITLE="桌面 App" ;;
|
app) TITLE="桌面 App" ;;
|
||||||
hands) TITLE="沒有螢幕的電腦(手腳)" ;;
|
hands) TITLE="手腳(操控這台)" ;;
|
||||||
server) TITLE="伺服器(帳號伺服器 + 大腦 + HTTPS)"; TOTAL=3 ;;
|
server) TITLE="伺服器(帳號伺服器 + 大腦 + HTTPS)"; TOTAL=3 ;;
|
||||||
deps) TITLE="只裝建置需要的東西"; TOTAL=2 ;;
|
deps) TITLE="只裝建置需要的東西"; TOTAL=2 ;;
|
||||||
esac
|
esac
|
||||||
printf '\n %s◆ AnyBot 安裝%s %s%s · %s %s%s\n' "$C$B" "$N" "$D" "$TITLE" "$OS" "$ARCH" "$N"
|
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() {
|
apt_pick() {
|
||||||
for p in "$@"; do
|
for p in "$@"; do
|
||||||
case $p in
|
case $p in
|
||||||
*\|*) # 幾個名字選第一個有的
|
*\|*)
|
||||||
for alt in $(echo "$p" | tr '|' ' '); do
|
for alt in $(echo "$p" | tr '|' ' '); do
|
||||||
if apt-cache show "$alt" >/dev/null 2>&1; then echo "$alt"; break; fi
|
if apt-cache show "$alt" >/dev/null 2>&1; then echo "$alt"; break; fi
|
||||||
done ;;
|
done ;;
|
||||||
|
|
@ -105,78 +298,160 @@ apt_pick() {
|
||||||
done
|
done
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
# zypper:同 apt_pick,裝之前先過濾掉這台找不到的名字(SUSE 各版本套件名差異大,
|
|
||||||
# 一個名字不存在會讓整組安裝失敗)。
|
|
||||||
zypper_pick() {
|
zypper_pick() {
|
||||||
for p in "$@"; do
|
for p in "$@"; do
|
||||||
zypper --non-interactive info "$p" >/dev/null 2>&1 && echo "$p"
|
zypper --non-interactive info "$p" >/dev/null 2>&1 && echo "$p"
|
||||||
done
|
done
|
||||||
true
|
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() {
|
linux_packages() {
|
||||||
# $1: build | app | hands | server
|
# $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
|
case $PM in
|
||||||
apt-get)
|
apt-get)
|
||||||
case $1 in
|
case $1 in
|
||||||
server) set -- curl ca-certificates git ;;
|
server) pkgs_in="curl ca-certificates git" ;;
|
||||||
build) set -- build-essential pkg-config git curl ca-certificates libasound2-dev ;;
|
build) pkgs_in="build-essential pkg-config git curl ca-certificates libasound2-dev|libasound-dev" ;;
|
||||||
# 手腳:虛擬螢幕、視窗管理員、輔助使用服務(讀 App 的按鈕)、session bus,Bot 才看得到這台的畫面。
|
# 手腳:虛擬螢幕、視窗管理員、輔助使用(讀按鈕)、session bus。各發行版名字不同,有的才裝。
|
||||||
hands) set -- build-essential pkg-config git curl ca-certificates libasound2-dev "libasound2t64|libasound2" \
|
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" ;;
|
||||||
xvfb openbox at-spi2-core libatk-adaptor dbus-x11 dbus-user-session xauth fonts-noto-cjk ;;
|
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" ;;
|
||||||
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 ;;
|
|
||||||
esac
|
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
|
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
|
UPDATED=1
|
||||||
fi
|
fi
|
||||||
pkgs=$(apt_pick "$@" | tr '\n' ' ')
|
# shellcheck disable=SC2086
|
||||||
# Already there (an update): nothing to install, no sudo password needed.
|
pkgs=$(apt_pick $pkgs_in | tr '\n' ' ')
|
||||||
missing=""
|
missing=""
|
||||||
for p in $pkgs; do dpkg -s "$p" >/dev/null 2>&1 || missing="$missing $p"; done
|
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
|
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"
|
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"
|
base="gcc gcc-c++ make pkgconf-pkg-config git curl ca-certificates alsa-lib-devel"
|
||||||
extra=""
|
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" = 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"
|
[ "$1" = server ] && base="curl git ca-certificates" && extra=""
|
||||||
$SUDO dnf install -y -q $base $extra || die "安裝系統套件失敗。" "sudo dnf install $base $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)
|
pacman)
|
||||||
base="base-devel git curl alsa-lib"
|
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" = hands ] && base="$base xorg-server-xvfb openbox at-spi2-core dbus xorg-xauth noto-fonts-cjk"
|
||||||
[ "$1" = server ] && base="curl git"
|
[ "$1" = server ] && base="curl git ca-certificates"
|
||||||
$SUDO pacman -S --needed --noconfirm $base || die "安裝系統套件失敗。" "sudo pacman -S --needed $base"
|
$SUDO pacman -Sy --needed --noconfirm $base || die "安裝系統套件失敗。" "sudo pacman -S --needed $base"
|
||||||
;;
|
;;
|
||||||
zypper)
|
zypper)
|
||||||
base="gcc gcc-c++ make pkg-config git curl alsa-devel"
|
base="gcc gcc-c++ make pkg-config git curl alsa-devel"
|
||||||
extra=""
|
extra=""
|
||||||
# app/hands 也要有這台该有的套件(之前只有 base,桌面模式缺 xkbcommon/GL/通知…,
|
[ "$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" = app ] && extra="libxkbcommon-x11-0 Mesa libGL1 xdg-utils libnotify-tools libnotify4 libsecret-1-0 espeak zenity xclip wl-clipboard ffmpeg fonts-noto"
|
|
||||||
[ "$1" = hands ] && extra="xorg-x11-server-Xvfb openbox at-spi2-core at-spi2-atk-common dbus-1-x11 xauth fonts-noto"
|
[ "$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)
|
pkgs=$(zypper_pick $base $extra)
|
||||||
$SUDO zypper -n install $pkgs || die "安裝系統套件失敗。" "sudo zypper install $pkgs"
|
$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 都沒有)。" \
|
if have cc && have pkg-config; then
|
||||||
"請自己裝:C 編譯器、pkg-config、git、curl、ALSA 開發套件,再執行一次。"
|
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
|
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() {
|
mac_tools() {
|
||||||
if xcode-select -p >/dev/null 2>&1; then ok "Xcode Command Line Tools"; return; fi
|
if xcode-select -p >/dev/null 2>&1; then ok "Xcode Command Line Tools"; return; fi
|
||||||
warn "需要 Xcode Command Line Tools,正在打開安裝視窗…"
|
warn "需要 Xcode Command Line Tools,正在打開安裝視窗…"
|
||||||
|
|
@ -187,27 +462,31 @@ mac_tools() {
|
||||||
# ---------- Rust ----------
|
# ---------- Rust ----------
|
||||||
rust_ok() {
|
rust_ok() {
|
||||||
have cargo && have rustc || return 1
|
have cargo && have rustc || return 1
|
||||||
# 抓「主版號 次版號」:舊寫法死鎖在 1.x,rustc 2.x 會被当成「太舊」。
|
major=$(rustc --version 2>/dev/null | sed -n 's/^rustc \([0-9][0-9]*\)\.\([0-9][0-9]*\).*/\1/p')
|
||||||
set -- $(rustc --version 2>/dev/null | sed -n 's/^rustc \([0-9][0-9]*\)\.\([0-9][0-9]*\).*/\1 \2/p')
|
minor=$(rustc --version 2>/dev/null | sed -n 's/^rustc \([0-9][0-9]*\)\.\([0-9][0-9]*\).*/\2/p')
|
||||||
[ $# -ge 2 ] || return 1
|
[ -n "$major" ] && [ -n "$minor" ] || return 1
|
||||||
[ "$1" -gt 1 ] && return 0
|
[ "$major" -gt 1 ] && return 0
|
||||||
[ "$1" -eq 1 ] && [ "$2" -ge "$RUST_MIN" ]
|
[ "$major" -eq 1 ] && [ "$minor" -ge "$RUST_MIN" ]
|
||||||
}
|
}
|
||||||
ensure_rust() {
|
ensure_rust() {
|
||||||
[ -f "$HOME/.cargo/env" ] && . "$HOME/.cargo/env"
|
[ -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
|
if have rustup; then
|
||||||
info "Rust 太舊,更新中(rustup update stable)…"
|
info "Rust 太舊,更新中(rustup update stable)…"
|
||||||
rustup update stable >/dev/null 2>&1 && rustup default stable >/dev/null 2>&1 || true
|
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"
|
[ -f "$HOME/.cargo/env" ] && . "$HOME/.cargo/env"
|
||||||
else
|
else
|
||||||
if have rustc; then warn "系統的 Rust $(rustc --version | cut -d' ' -f2) 太舊(要 1.$RUST_MIN 以上),另外用 rustup 裝新版"; fi
|
if have rustc; then warn "系統的 Rust $(rustc --version | cut -d' ' -f2) 太舊(要 1.$RUST_MIN 以上),另外用 rustup 裝新版"; fi
|
||||||
info "安裝 Rust(rustup,裝在 ~/.cargo,不用 root)…"
|
info "安裝 Rust(rustup,目標 $RUST_HOST,裝在 ~/.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 \
|
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"
|
|| die "安裝 Rust 失敗。" "手動:curl --proto '=https' -sSf https://sh.rustup.rs | sh"
|
||||||
. "$HOME/.cargo/env"
|
. "$HOME/.cargo/env"
|
||||||
fi
|
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 套件。"
|
rust_ok || die "Rust 還是太舊:$(rustc --version 2>/dev/null)" "執行 rustup update stable,或移除系統的 rustc/cargo 套件。"
|
||||||
ok "Rust $(rustc --version | cut -d' ' -f2)"
|
ok "Rust $(rustc --version | cut -d' ' -f2)"
|
||||||
case ":$PATH:" in *":$HOME/.cargo/bin:"*) ;; *)
|
case ":$PATH:" in *":$HOME/.cargo/bin:"*) ;; *)
|
||||||
|
|
@ -231,21 +510,23 @@ find_source() {
|
||||||
else
|
else
|
||||||
[ -e "$DIR" ] && die "$DIR 已經存在,但不是 AnyBot 的原始碼。" "換個地方:ANYBOT_DIR=~/別的資料夾 再執行一次。"
|
[ -e "$DIR" ] && die "$DIR 已經存在,但不是 AnyBot 的原始碼。" "換個地方:ANYBOT_DIR=~/別的資料夾 再執行一次。"
|
||||||
info "下載原始碼到 $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
|
fi
|
||||||
SRC=$DIR; ok "原始碼:${SRC}($(git -C "$SRC" log -1 --format=%h))"
|
SRC=$DIR; ok "原始碼:${SRC}($(git -C "$SRC" log -1 --format=%h))"
|
||||||
}
|
}
|
||||||
|
|
||||||
# 記憶體小的機器同時編太多會被系統砍掉:每 2 GB 一個工作。
|
|
||||||
build_jobs() {
|
build_jobs() {
|
||||||
mem_mb=0
|
mem_mb=0
|
||||||
if [ -r /proc/meminfo ]; then
|
if [ -r /proc/meminfo ]; then
|
||||||
mem_mb=$(awk '/MemTotal/ {m=$2} /SwapTotal/ {s=$2} END {print int((m + s / 2) / 1024)}' /proc/meminfo)
|
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
|
fi
|
||||||
[ "$mem_mb" -gt 0 ] || { echo ""; return; }
|
[ "${mem_mb:-0}" -gt 0 ] || { echo ""; return; }
|
||||||
j=$((mem_mb / 2000)); [ $j -lt 1 ] && j=1
|
j=$((mem_mb / 2000)); [ "$j" -lt 1 ] && j=1
|
||||||
cpus=$(nproc 2>/dev/null || echo $j); [ $j -gt "$cpus" ] && j=$cpus
|
cpus=$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo "$j")
|
||||||
echo $j
|
[ "$j" -gt "$cpus" ] && j=$cpus
|
||||||
|
echo "$j"
|
||||||
if [ "$mem_mb" -lt 3000 ]; then
|
if [ "$mem_mb" -lt 3000 ]; then
|
||||||
warn "這台記憶體只有 ${mem_mb} MB,編譯會比較久(一次編一個)。若被中斷,加 2 GB swap 再試:" >&2
|
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
|
info " sudo fallocate -l 2G /swapfile && sudo chmod 600 /swapfile && sudo mkswap /swapfile && sudo swapon /swapfile" >&2
|
||||||
|
|
@ -254,27 +535,34 @@ build_jobs() {
|
||||||
|
|
||||||
build() {
|
build() {
|
||||||
jobs=$(build_jobs)
|
jobs=$(build_jobs)
|
||||||
info "編譯中,第一次約 5–15 分鐘(之後更新只重編改過的部分)…"
|
info "編譯中,第一次約 5–15 分鐘($RUST_HOST;之後更新只重編改過的部分)…"
|
||||||
log="$SRC/target/install-build.log"; mkdir -p "$SRC/target"
|
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 "編譯完成"
|
ok "編譯完成"
|
||||||
else
|
else
|
||||||
tail -n 25 "$log" | sed 's/^/ /'
|
tail -n 25 "$log" | sed 's/^/ /'
|
||||||
die "編譯失敗(完整紀錄:${log})" "把紀錄的最後幾行貼給維護的人;常見原因見 docs/install.md 的「常見問題」。"
|
die "編譯失敗(完整紀錄:${log})" "把紀錄的最後幾行貼給維護的人;常見原因見 docs/install.md。"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------- Docker(伺服器)----------
|
|
||||||
ensure_docker() {
|
ensure_docker() {
|
||||||
if ! have docker; then
|
if ! have docker; then
|
||||||
info "安裝 Docker(官方腳本 get.docker.com)…"
|
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
|
fi
|
||||||
|
if have systemctl; then
|
||||||
$SUDO systemctl enable --now docker >/dev/null 2>&1 || true
|
$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 || {
|
docker compose version >/dev/null 2>&1 || $SUDO docker compose version >/dev/null 2>&1 || {
|
||||||
case $PM in
|
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 ;;
|
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) $SUDO dnf install -y -q docker-compose-plugin >/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
|
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/"
|
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
|
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
|
case $MODE in
|
||||||
deps)
|
deps)
|
||||||
step "系統套件"
|
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"
|
step "Rust"
|
||||||
ensure_rust
|
ensure_rust
|
||||||
printf '\n %s好了。%s在原始碼資料夾裡:%smake%s 建置、%smake install%s 安裝。\n' "$G$B" "$N" "$B" "$N" "$B" "$N"
|
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)
|
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"
|
step "系統套件和 Docker"
|
||||||
linux_packages server
|
linux_packages server
|
||||||
ensure_docker
|
ensure_docker
|
||||||
step "原始碼"
|
step "原始碼"
|
||||||
find_source
|
find_source
|
||||||
step "設定和啟動"
|
step "設定和啟動"
|
||||||
info "接下來會問網域、帳號、密碼。"
|
info "接下來會問網址、帳號、密碼。免問:ANYBOT_SETUP_USER、ANYBOT_SETUP_PASSWORD、ANYBOT_SETUP_DOMAIN。"
|
||||||
cd "$SRC/deploy/server"
|
cd "$SRC/deploy/server"
|
||||||
IN=$TTY
|
IN=$TTY
|
||||||
if [ -n "${ANYBOT_SETUP_USER:-}" ] && [ -n "${ANYBOT_SETUP_PASSWORD:-}" ]; then IN=/dev/null # 免問模式
|
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
|
else [ -n "$TTY" ] || die "需要一個終端機回答問題。" "在伺服器上直接執行,或用 ANYBOT_SETUP_USER、ANYBOT_SETUP_PASSWORD(不給網域就用 Tailscale)。"; fi
|
||||||
if [ "${DOCKER_SUDO:-0}" = 1 ]; then exec $SUDO -E ./setup.sh "$@" <"$IN"; else exec ./setup.sh "$@" <"$IN"; 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)
|
app|hands)
|
||||||
step "系統套件"
|
step "系統套件"
|
||||||
if [ $PLAT = mac ]; then mac_tools
|
if [ "$PLAT" = mac ]; then mac_tools
|
||||||
elif [ "$MODE" = hands ] && have anybot && [ $# -gt 0 ]; then ok "AnyBot 已經裝好,跳過"
|
elif [ "$MODE" = hands ] && have anybot && [ -n "$SAVED_1" ]; then ok "AnyBot 已經裝好,跳過編譯"
|
||||||
else linux_packages $MODE; ok "系統套件"; fi
|
else
|
||||||
|
linux_packages "$MODE"
|
||||||
|
# 手腳也要能編譯(ALSA),上面的 hands 清單已含編譯套件。桌面再補執行期函式庫已在 app。
|
||||||
|
[ "$MODE" = hands ] && hands_runtime_check
|
||||||
|
ok "系統套件"
|
||||||
|
fi
|
||||||
|
|
||||||
if [ "$MODE" = hands ] && have anybot && [ $# -gt 0 ]; then
|
if [ "$MODE" = hands ] && have anybot && [ -n "$SAVED_1" ]; then
|
||||||
STEP=3; info "已經有 $(command -v anybot),直接接上。(要更新:不帶網址再執行一次)"
|
STEP=3
|
||||||
|
info "已經有 $(command -v anybot),直接接上。(要更新:不帶網址再執行一次)"
|
||||||
else
|
else
|
||||||
step "Rust"
|
step "Rust"
|
||||||
ensure_rust
|
ensure_rust
|
||||||
|
|
@ -330,37 +694,68 @@ case $MODE in
|
||||||
find_source
|
find_source
|
||||||
build
|
build
|
||||||
step "安裝"
|
step "安裝"
|
||||||
if [ $PLAT = mac ]; then
|
if [ "$PLAT" = mac ]; then
|
||||||
(cd "$SRC" && make install >/dev/null) || die "安裝失敗。" "在 $SRC 執行 make install 看錯誤。"
|
(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
|
else
|
||||||
sh "$SRC/deploy/linux/install.sh" >/dev/null || die "安裝失敗。" "執行 sh $SRC/deploy/linux/install.sh 看錯誤。"
|
sh "$SRC/deploy/linux/install.sh" >/dev/null || die "安裝失敗。" "執行 sh $SRC/deploy/linux/install.sh 看錯誤。"
|
||||||
ok "程式放在 ~/.local/bin/anybot"
|
ok "程式放在 ~/.local/bin/anybot"
|
||||||
[ "$MODE" = app ] && ok "應用程式選單裡有 AnyBot"
|
[ "$MODE" = app ] && [ "$DESKTOP" = 1 ] && ok "應用程式選單裡有 AnyBot"
|
||||||
# 更新: the one running in the background (anybot setup's service) switches to the new one.
|
if [ -f "$HOME/.local/bin/anybot-run" ] && have systemctl && systemctl --user is-enabled --quiet anybot 2>/dev/null; then
|
||||||
if [ -f "$HOME/.local/bin/anybot-run" ] && systemctl --user is-enabled --quiet anybot 2>/dev/null; then
|
|
||||||
"$HOME/.local/bin/anybot" setup --refresh-service >/dev/null 2>&1 && ok "重新啟動背景執行的 AnyBot(換成新版)"
|
"$HOME/.local/bin/anybot" setup --refresh-service >/dev/null 2>&1 && ok "重新啟動背景執行的 AnyBot(換成新版)"
|
||||||
fi
|
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
|
||||||
fi
|
fi
|
||||||
PATH="$HOME/.local/bin:$PATH"
|
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 [ "$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'
|
printf '\n'
|
||||||
[ -n "$TTY" ] && exec anybot setup "$@" <"$TTY"
|
if [ -n "$SAVED_2" ]; then
|
||||||
exec anybot setup "$@"
|
[ -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
|
fi
|
||||||
printf '\n %s裝好了。%s接著把這台接到你的大腦:\n' "$G$B" "$N"
|
printf '\n %s裝好了。%s接著把這台接到你的大腦:\n' "$G$B" "$N"
|
||||||
printf ' 1. 在手機或電腦的 AnyBot 按「加一台電腦」,取得配對碼\n'
|
printf ' 1. 在手機或電腦的 AnyBot 按「加一台電腦」\n'
|
||||||
printf ' 2. 在這裡執行:%sanybot setup 網址 配對碼%s\n' "$B" "$N"
|
printf ' 2. 在這裡執行:%sanybot setup 網址 配對碼%s\n' "$B" "$N"
|
||||||
|
if [ "$PLAT" = linux ] && [ "$DESKTOP" = 0 ]; then
|
||||||
|
info "沒有螢幕:配對後會開一塊虛擬螢幕,Bot 看得到、點得到。要改用已登入的桌面:anybot setup --real-screen …"
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
|
seed_or_pair
|
||||||
printf '\n %s裝好了。%s' "$G$B" "$N"
|
printf '\n %s裝好了。%s' "$G$B" "$N"
|
||||||
if [ $PLAT = mac ]; then printf '打開 ~/Applications/AnyBot(第一次:按右鍵 › 打開)。\n'
|
if [ "$PLAT" = mac ]; then printf '打開 ~/Applications/AnyBot(第一次:按右鍵 › 打開)。\n'
|
||||||
else printf '從應用程式選單打開 AnyBot,或在終端機輸入 %sanybot%s。\n' "$B" "$N"; fi
|
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
|
if ! have docker; then info "Bot 自己的電腦(一個隔離的桌面)需要 Docker,要用時 AnyBot 會告訴你怎麼裝。"; fi
|
||||||
info "要更新:再執行一次同一行指令。"
|
info "要更新:再執行一次同一行指令。"
|
||||||
fi
|
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
|
esac
|
||||||
|
|
|
||||||
|
|
@ -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`。
|
||||||
|
|
@ -1966,10 +1966,10 @@ $("k-right").onclick = () => { S.right = !S.right; $("k-right").textContent = "
|
||||||
// ---------- 加一台電腦, notifications ----------
|
// ---------- 加一台電腦, notifications ----------
|
||||||
// A code for a new computer (ten minutes, once), as hands for this AnyBot.
|
// A code for a new computer (ten minutes, once), as hands for this AnyBot.
|
||||||
async function pair() {
|
async function pair() {
|
||||||
const el = sheet("加一台電腦", [], `<div class="note">在那台電腦(Linux 或 Mac)的終端機貼上這一行執行:還沒裝 AnyBot 會先自動裝好,接著變成「${esc(S.hostName || "這台 AnyBot")}」的手腳,開機自動執行。不用帳號密碼。</div><div class="pair-code" id="pair-code">······</div><code class="pair-line" id="pair-line">正在產生…</code><div class="form"><button class="btn main" id="pair-copy">複製這一行</button></div><div class="note" id="pair-left" style="text-align:center"></div>`);
|
const el = sheet("加一台電腦", [], `<div class="note">Mac、任何 Linux(含沒有螢幕的)貼終端機那一行;Windows 貼 PowerShell 那一行。還沒裝會先裝好,並完成第一次配對,變成「${esc(S.hostName || "這台 AnyBot")}」的手腳。不用帳號密碼。</div><div class="pair-code" id="pair-code">······</div><code class="pair-line" id="pair-line" style="white-space:pre-wrap">正在產生…</code><div class="form"><button class="btn main" id="pair-copy">複製全部</button></div><div class="note" id="pair-left" style="text-align:center"></div>`);
|
||||||
try {
|
try {
|
||||||
const v = await api("relay/pair", {json: {for: S.host}});
|
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-code").textContent = v.code.slice(0, 3) + " " + v.code.slice(3);
|
||||||
el.querySelector("#pair-line").textContent = line;
|
el.querySelector("#pair-line").textContent = line;
|
||||||
el.querySelector("#pair-copy").onclick = () => copyText(line);
|
el.querySelector("#pair-copy").onclick = () => copyText(line);
|
||||||
|
|
|
||||||
|
|
@ -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("\n\n你做過的頁面(更新時用 write_page 帶 id,先 read_page 看現在的內容):\n");
|
||||||
s.push_str(&mine.join("\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() {
|
if !computers.is_empty() {
|
||||||
let list: Vec<String> = lines.iter().map(|l| format!("- {l}")).collect();
|
let list: Vec<String> = lines.iter().map(|l| format!("- {l}")).collect();
|
||||||
let mine = match &roles.person {
|
let mine = match &roles.person {
|
||||||
|
|
|
||||||
|
|
@ -165,15 +165,15 @@ impl App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 加一台電腦: one line to run on the new computer (a Linux box, a server); it joins as hands for
|
/// 加一台電腦: lines for Mac, any Linux (desktop or headless) and Windows. Each installs
|
||||||
/// the brain, with no password and nothing to answer.
|
/// AnyBot if needed and joins as hands, including the first pairing, with nothing to answer.
|
||||||
fn pair_section(&mut self, ui: &mut egui::Ui) {
|
fn pair_section(&mut self, ui: &mut egui::Ui) {
|
||||||
let p = self.pal;
|
let p = self.pal;
|
||||||
ui.add_space(6.0);
|
ui.add_space(6.0);
|
||||||
ui.separator();
|
ui.separator();
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label(RichText::new(t("加一台電腦")).size(13.5).strong());
|
ui.label(RichText::new(t("加一台電腦")).size(13.5).strong());
|
||||||
help_mark(ui, "讓另一台電腦(例如沒有螢幕的 Linux)也當大腦的手腳:在那台的終端機貼上這一行就好:還沒裝 AnyBot 會先自動裝好,不用帳號密碼、不用回答問題。它會設成開機自動執行,只讓這個大腦使用。", p);
|
help_mark(ui, "讓另一台電腦當大腦的手腳:Mac、任何 Linux(含沒有螢幕的)、已登入的 Windows。貼上對應的那一行就會裝好並完成第一次配對,不用帳號密碼。它會在開機或登入後自動執行,只讓這個大腦使用。", p);
|
||||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||||
if ui.add(secondary_button("產生配對碼")).clicked() {
|
if ui.add(secondary_button("產生配對碼")).clicked() {
|
||||||
let (s, slot) = (self.state.remote.clone(), self.mir.pair.clone());
|
let (s, slot) = (self.state.remote.clone(), self.mir.pair.clone());
|
||||||
|
|
@ -181,7 +181,7 @@ impl App {
|
||||||
*slot.lock().unwrap() = (t("正在產生…"), false, None);
|
*slot.lock().unwrap() = (t("正在產生…"), false, None);
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
*slot.lock().unwrap() = match crate::remote::pair_code(&s, &for_host) {
|
*slot.lock().unwrap() = match crate::remote::pair_code(&s, &for_host) {
|
||||||
Ok(code) => (crate::remote::pair_line(&s.relay_url, &code), true, Some(Instant::now())),
|
Ok(code) => (crate::remote::pair_block(&s.relay_url, &code), true, Some(Instant::now())),
|
||||||
Err(e) => (e, false, None),
|
Err(e) => (e, false, None),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
@ -204,16 +204,16 @@ impl App {
|
||||||
}
|
}
|
||||||
Frame::new().fill(p.hover).corner_radius(8).inner_margin(Margin::symmetric(10, 8)).show(ui, |ui| {
|
Frame::new().fill(p.hover).corner_radius(8).inner_margin(Margin::symmetric(10, 8)).show(ui, |ui| {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.add(egui::Label::new(RichText::new(&line).monospace().size(13.0)).selectable(true).wrap());
|
ui.add(egui::Label::new(RichText::new(&line).monospace().size(12.0)).selectable(true).wrap());
|
||||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||||
if ui.add(secondary_button("複製")).clicked() {
|
if ui.add(secondary_button("複製")).clicked() {
|
||||||
ui.ctx().copy_text(line.clone());
|
ui.ctx().copy_text(line.clone());
|
||||||
self.notice = Some(t("已複製,貼到那台電腦的終端機執行"));
|
self.notice = Some(t("已複製。Mac 和 Linux 貼終端機那一行,Windows 貼 PowerShell 那一行"));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
ui.label(RichText::new(format!("{}(還有 {} 分 {:02} 秒,只能用一次)", t("在那台電腦的終端機貼上執行"), left / 60, left % 60)).size(12.0).color(p.text3));
|
ui.label(RichText::new(format!("{}(還有 {} 分 {:02} 秒,只能用一次)", t("在那台電腦貼上對應的那一行"), left / 60, left % 60)).size(12.0).color(p.text3));
|
||||||
// Just made: bring it into view.
|
// Just made: bring it into view.
|
||||||
if at.map(|t| t.elapsed().as_millis() < 800).unwrap_or(false) {
|
if at.map(|t| t.elapsed().as_millis() < 800).unwrap_or(false) {
|
||||||
ui.scroll_to_cursor(Some(Align::Max));
|
ui.scroll_to_cursor(Some(Align::Max));
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ if [ -r /etc/os-release ]; then . /etc/os-release; k os "$PRETTY_NAME"; elif com
|
||||||
[ "$(uname -s)" = Linux ] && { [ -d /run/systemd/system ] && k systemd yes || k systemd no; }
|
[ "$(uname -s)" = Linux ] && { [ -d /run/systemd/system ] && k systemd yes || k systemd no; }
|
||||||
if [ "$(id -u)" = 0 ]; then k sudo root; elif command -v sudo >/dev/null 2>&1; then if sudo -n true >/dev/null 2>&1; then k sudo yes; else k sudo password; fi; else k sudo none; fi
|
if [ "$(id -u)" = 0 ]; then k sudo root; elif command -v sudo >/dev/null 2>&1; then if sudo -n true >/dev/null 2>&1; then k sudo yes; else k sudo password; fi; else k sudo none; fi
|
||||||
if command -v curl >/dev/null 2>&1; then curl -s -m 4 -o /dev/null https://www.google.com/generate_204 && k net yes || k net no; fi
|
if command -v curl >/dev/null 2>&1; then curl -s -m 4 -o /dev/null https://www.google.com/generate_204 && k net yes || k net no; fi
|
||||||
for p in apt-get dnf yum pacman apk zypper brew; do command -v $p >/dev/null 2>&1 && k pkg $p; done
|
for p in apt-get dnf yum pacman apk zypper xbps-install brew winget; do command -v $p >/dev/null 2>&1 && k pkg $p; done
|
||||||
k desktop "${XDG_CURRENT_DESKTOP:-}"
|
k desktop "${XDG_CURRENT_DESKTOP:-}"
|
||||||
[ -n "${TZ:-}" ] && k tz "$TZ"
|
[ -n "${TZ:-}" ] && k tz "$TZ"
|
||||||
for w in fluxbox gnome-shell plasmashell xfce4-session mate-session cinnamon sway Hyprland; do pgrep -x $w >/dev/null 2>&1 && k wm $w; done
|
for w in fluxbox gnome-shell plasmashell xfce4-session mate-session cinnamon sway Hyprland; do pgrep -x $w >/dev/null 2>&1 && k wm $w; done
|
||||||
|
|
@ -124,6 +124,9 @@ pub fn describe(name: &str, f: &Facts, own_box: bool, mac_sandbox: bool) -> Stri
|
||||||
if let Some(p) = f.pkg.iter().find(|p| p.as_str() != "brew").or(f.pkg.first()) {
|
if let Some(p) = f.pkg.iter().find(|p| p.as_str() != "brew").or(f.pkg.first()) {
|
||||||
parts.push(format!("裝東西用 {p}"));
|
parts.push(format!("裝東西用 {p}"));
|
||||||
}
|
}
|
||||||
|
if f.kernel == "Linux" || f.kernel == "Darwin" {
|
||||||
|
parts.push("裝成 AnyBot 手腳(含第一次配對,不要問問題):ANYBOT_NONINTERACTIVE=1,再執行大腦「加一台電腦」的那一行。先看適不適合:sh install.sh --print".into());
|
||||||
|
}
|
||||||
if !f.tools.is_empty() {
|
if !f.tools.is_empty() {
|
||||||
parts.push(format!("指令有 {}", f.tools.join("、")));
|
parts.push(format!("指令有 {}", f.tools.join("、")));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -624,4 +624,7 @@ pub const ROWS: &[(&str, &str, &str)] = &[
|
||||||
("取消此要求且不執行該步驟;Bot 會跳過它並繼續", "Cancel this request without doing the step; the Bot continues without it", "この依頼を取り消し、その手順は行いません。Bot はそれを飛ばして続けます"),
|
("取消此要求且不執行該步驟;Bot 會跳過它並繼續", "Cancel this request without doing the step; the Bot continues without it", "この依頼を取り消し、その手順は行いません。Bot はそれを飛ばして続けます"),
|
||||||
("連結中", "Connecting", "接続中"),
|
("連結中", "Connecting", "接続中"),
|
||||||
("請到「{name}」上操作", "Please do it on “{name}”", "「{name}」で操作してください"),
|
("請到「{name}」上操作", "Please do it on “{name}”", "「{name}」で操作してください"),
|
||||||
|
("讓另一台電腦當大腦的手腳:Mac、任何 Linux(含沒有螢幕的)、已登入的 Windows。貼上對應的那一行就會裝好並完成第一次配對,不用帳號密碼。它會在開機或登入後自動執行,只讓這個大腦使用。", "Make another computer this brain's hands: Mac, any Linux (including headless), or a signed-in Windows PC. Paste the matching line — it installs AnyBot if needed, pairs once, and starts at login. No password. Only this brain can use it.", "別のコンピューターをこのブレインの手にする:Mac、任意の Linux(画面なしも)、ログイン中の Windows。対応する行を貼るとインストールと初回ペアリングまで終わり、ログイン時に起動します。パスワード不要。このブレインだけが使えます。"),
|
||||||
|
("已複製。Mac 和 Linux 貼終端機那一行,Windows 貼 PowerShell 那一行", "Copied. Paste the terminal line on Mac or Linux, the PowerShell line on Windows", "コピーしました。Mac と Linux はターミナルの行、Windows は PowerShell の行を貼ってください"),
|
||||||
|
("在那台電腦貼上對應的那一行", "Paste the matching line on that computer", "そのコンピューターに対応する行を貼る"),
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -1844,13 +1844,25 @@ mod tests {
|
||||||
|
|
||||||
/// Where the one-step installer lives (it also installs AnyBot on a computer that has nothing yet).
|
/// Where the one-step installer lives (it also installs AnyBot on a computer that has nothing yet).
|
||||||
pub const INSTALL_URL: &str = "https://code.30cm.net/daniel.w/AnyBot/raw/branch/main/install.sh";
|
pub const INSTALL_URL: &str = "https://code.30cm.net/daniel.w/AnyBot/raw/branch/main/install.sh";
|
||||||
|
/// Windows (x64 and arm64). A PowerShell one-liner; Git Bash running install.sh hands off to this.
|
||||||
|
pub const INSTALL_PS1: &str = "https://code.30cm.net/daniel.w/AnyBot/raw/branch/main/install.ps1";
|
||||||
|
|
||||||
/// The line 「加一台電腦」 shows: pasted into any Linux or Mac terminal, it installs AnyBot when it
|
/// The Unix line 「加一台電腦」 shows. Pasted into a macOS or any Linux terminal (desktop or
|
||||||
/// isn't there yet and joins this computer as hands (`anybot setup 網址 配對碼`).
|
/// headless), it installs AnyBot when it isn't there yet and joins as hands.
|
||||||
pub fn pair_line(relay_url: &str, code: &str) -> String {
|
pub fn pair_line(relay_url: &str, code: &str) -> String {
|
||||||
format!("curl -fsSL {INSTALL_URL} | sh -s -- hands {} {code}", relay_url.trim_end_matches('/'))
|
format!("curl -fsSL {INSTALL_URL} | sh -s -- hands {} {code}", relay_url.trim_end_matches('/'))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Every platform, Unix last so pasting the whole block still fills the address and the code
|
||||||
|
/// (the parser uses the last two words).
|
||||||
|
pub fn pair_block(relay_url: &str, code: &str) -> String {
|
||||||
|
let url = relay_url.trim_end_matches('/');
|
||||||
|
format!(
|
||||||
|
"Windows PowerShell:\n$env:ANYBOT_NONINTERACTIVE='1'; $env:ANYBOT_MODE='hands'; $env:ANYBOT_PAIR_URL='{url}'; $env:ANYBOT_PAIR_CODE='{code}'; irm {INSTALL_PS1} | iex\n\nMac / Linux(任何發行版,沒有螢幕也可以):\n{}",
|
||||||
|
pair_line(relay_url, code)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// 登入後同步 against a real account server: ANYBOT_RELAY_URL=http://127.0.0.1:PORT
|
/// 登入後同步 against a real account server: ANYBOT_RELAY_URL=http://127.0.0.1:PORT
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod account_sync_live {
|
mod account_sync_live {
|
||||||
|
|
|
||||||
306
src/setup.rs
306
src/setup.rs
|
|
@ -686,7 +686,7 @@ impl Plan {
|
||||||
// 1. Settings (this AnyBot's own file; the service reads it).
|
// 1. Settings (this AnyBot's own file; the service reads it).
|
||||||
say(0, Mark::Run, "");
|
say(0, Mark::Run, "");
|
||||||
if self.service {
|
if self.service {
|
||||||
let _ = systemctl(&["stop", UNIT]);
|
stop_running();
|
||||||
}
|
}
|
||||||
// No other AnyBot may be using this folder while the settings are written (a window
|
// No other AnyBot may be using this folder while the settings are written (a window
|
||||||
// would overwrite them with its own copy). The service was just stopped: give it a
|
// would overwrite them with its own copy). The service was just stopped: give it a
|
||||||
|
|
@ -730,13 +730,10 @@ impl Plan {
|
||||||
drop(folder);
|
drop(folder);
|
||||||
say(0, Mark::Ok, "");
|
say(0, Mark::Ok, "");
|
||||||
|
|
||||||
// 2. Start with the computer.
|
// 2. Start with the computer (Linux systemd, macOS launchd, Windows 登入工作).
|
||||||
say(1, Mark::Run, "");
|
say(1, Mark::Run, "");
|
||||||
let started = if !self.env.linux {
|
let started = if !self.service {
|
||||||
say(1, Mark::Warn, "Mac 和 Windows:打開 AnyBot App 就會用這些設定連上(設定 › 一般 › 手機與帳號)。");
|
say(1, Mark::Warn, "沒有設開機自動執行。要連上請開 AnyBot,或執行 anybot --serve。");
|
||||||
false
|
|
||||||
} else if !self.service {
|
|
||||||
say(1, Mark::Warn, "這台沒有 systemd:請用「anybot --serve」啟動,或把它加到開機執行。");
|
|
||||||
false
|
false
|
||||||
} else {
|
} else {
|
||||||
match self.install_service() {
|
match self.install_service() {
|
||||||
|
|
@ -768,7 +765,7 @@ impl Plan {
|
||||||
if seen {
|
if seen {
|
||||||
say(2, Mark::Ok, &format!("已連線({} 秒)", t0.elapsed().as_secs().max(1)));
|
say(2, Mark::Ok, &format!("已連線({} 秒)", t0.elapsed().as_secs().max(1)));
|
||||||
} else {
|
} else {
|
||||||
say(2, Mark::Fail, "40 秒內沒有連上。看看紀錄:journalctl --user -u anybot -n 50");
|
say(2, Mark::Fail, &format!("40 秒內沒有連上。看看紀錄:{}", log_hint()));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
say(2, Mark::Warn, "啟動 AnyBot 後就會連上。");
|
say(2, Mark::Warn, "啟動 AnyBot 後就會連上。");
|
||||||
|
|
@ -792,25 +789,43 @@ impl Plan {
|
||||||
}
|
}
|
||||||
if started {
|
if started {
|
||||||
tips.push(String::new());
|
tips.push(String::new());
|
||||||
tips.push("看狀態\tsystemctl --user status anybot".into());
|
tips.push(format!("看狀態\t{}", status_hint()));
|
||||||
tips.push("看紀錄\tjournalctl --user -u anybot -f".into());
|
tips.push(format!("看紀錄\t{}", log_hint()));
|
||||||
}
|
}
|
||||||
tips.push("改設定\tanybot setup".into());
|
tips.push("改設定\tanybot setup".into());
|
||||||
if started && !lingering() {
|
if started && self.env.linux && self.env.systemd && !lingering() {
|
||||||
tips.push(String::new());
|
tips.push(String::new());
|
||||||
tips.push(format!("登出或重開機後也要繼續跑,請執行一次:sudo loginctl enable-linger {}", std::env::var("USER").unwrap_or_default()));
|
tips.push(format!("登出或重開機後也要繼續跑,請執行一次:sudo loginctl enable-linger {}", std::env::var("USER").unwrap_or_default()));
|
||||||
}
|
}
|
||||||
|
if cfg!(target_os = "macos") && self.role == Role::Hands {
|
||||||
|
tips.push("權限\t系統設定 › 隱私權與安全性:螢幕錄製、輔助使用,打開 AnyBot(只問一次)".into());
|
||||||
|
}
|
||||||
|
if cfg!(target_os = "windows") && self.role == Role::Hands {
|
||||||
|
tips.push("畫面\tBot 操控的是你登入中的桌面。鎖定或登出後點不到,登入就會恢復。".into());
|
||||||
|
}
|
||||||
let _ = tx.send(Msg::Finished(tips));
|
let _ = tx.send(Msg::Finished(tips));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A small launcher and a systemd user service that runs it.
|
/// Start with the computer: systemd, launchd, or a Windows logon task. Linux without
|
||||||
|
/// systemd still gets a launcher and is started once.
|
||||||
fn install_service(&self) -> Result<String, String> {
|
fn install_service(&self) -> Result<String, String> {
|
||||||
let home = dirs::home_dir().ok_or("找不到家目錄")?;
|
|
||||||
let exe = std::env::current_exe().and_then(|p| p.canonicalize()).map_err(|e| format!("找不到 AnyBot 程式:{e}"))?;
|
let exe = std::env::current_exe().and_then(|p| p.canonicalize()).map_err(|e| format!("找不到 AnyBot 程式:{e}"))?;
|
||||||
|
if cfg!(target_os = "macos") {
|
||||||
|
return install_launchd(&exe);
|
||||||
|
}
|
||||||
|
if cfg!(target_os = "windows") {
|
||||||
|
return install_schtasks(&exe);
|
||||||
|
}
|
||||||
|
self.install_systemd(&exe)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A small launcher and a systemd user service that runs it.
|
||||||
|
fn install_systemd(&self, exe: &std::path::Path) -> Result<String, String> {
|
||||||
|
let home = dirs::home_dir().ok_or("找不到家目錄")?;
|
||||||
let bin = home.join(".local/bin");
|
let bin = home.join(".local/bin");
|
||||||
std::fs::create_dir_all(&bin).map_err(|e| format!("建立 {} 失敗:{e}", bin.display()))?;
|
std::fs::create_dir_all(&bin).map_err(|e| format!("建立 {} 失敗:{e}", bin.display()))?;
|
||||||
let launcher = bin.join("anybot-run");
|
let launcher = bin.join("anybot-run");
|
||||||
write_atomic(&launcher, launcher_script(self.role, &exe, &self.env)).map_err(|e| format!("寫入 {} 失敗:{e}", launcher.display()))?;
|
write_atomic(&launcher, launcher_script(self.role, exe, &self.env)).map_err(|e| format!("寫入 {} 失敗:{e}", launcher.display()))?;
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
|
@ -832,17 +847,150 @@ impl Plan {
|
||||||
systemd_quote(&launcher)
|
systemd_quote(&launcher)
|
||||||
);
|
);
|
||||||
write_atomic(&units.join(UNIT), unit).map_err(|e| e.to_string())?;
|
write_atomic(&units.join(UNIT), unit).map_err(|e| e.to_string())?;
|
||||||
|
if !self.env.systemd {
|
||||||
|
let _ = std::process::Command::new(&launcher).spawn();
|
||||||
|
return Ok(format!("這台沒有 systemd,已先啟動一次({})。請把這支加進開機(OpenRC、cron @reboot)。", launcher.display()));
|
||||||
|
}
|
||||||
systemctl(&["daemon-reload"])?;
|
systemctl(&["daemon-reload"])?;
|
||||||
systemctl(&["enable", UNIT])?;
|
systemctl(&["enable", UNIT])?;
|
||||||
systemctl(&["restart", UNIT])?;
|
systemctl(&["restart", UNIT])?;
|
||||||
|
let _ = std::process::Command::new("sudo").args(["-n", "loginctl", "enable-linger", &std::env::var("USER").unwrap_or_default()]).status();
|
||||||
Ok(format!("systemd 使用者服務「anybot」,已啟動({})", launcher.display()))
|
Ok(format!("systemd 使用者服務「anybot」,已啟動({})", launcher.display()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn log_hint() -> &'static str {
|
||||||
|
if cfg!(target_os = "macos") {
|
||||||
|
"tail -f ~/Library/Logs/anybot-serve.log"
|
||||||
|
} else if cfg!(target_os = "windows") {
|
||||||
|
"工作排程器 › AnyBot › 歷程記錄"
|
||||||
|
} else {
|
||||||
|
"journalctl --user -u anybot -n 50"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn status_hint() -> &'static str {
|
||||||
|
if cfg!(target_os = "macos") {
|
||||||
|
"launchctl print gui/$(id -u)/com.anybot.serve"
|
||||||
|
} else if cfg!(target_os = "windows") {
|
||||||
|
"schtasks /Query /TN AnyBot"
|
||||||
|
} else {
|
||||||
|
"systemctl --user status anybot"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn uid() -> String {
|
||||||
|
std::process::Command::new("id").arg("-u").output().ok().and_then(|o| String::from_utf8(o.stdout).ok()).map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).unwrap_or_else(|| "0".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stop_running() {
|
||||||
|
if cfg!(target_os = "linux") {
|
||||||
|
let _ = systemctl(&["stop", UNIT]);
|
||||||
|
} else if cfg!(target_os = "macos") {
|
||||||
|
let _ = std::process::Command::new("launchctl").args(["bootout", &format!("gui/{}/com.anybot.serve", uid())]).status();
|
||||||
|
} else if cfg!(target_os = "windows") {
|
||||||
|
let _ = std::process::Command::new("schtasks").args(["/End", "/TN", "AnyBot"]).status();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn xml_escape(s: &str) -> String {
|
||||||
|
s.replace('&', "&").replace('<', "<").replace('>', ">").replace('"', """)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn launchd_plist(exe: &std::path::Path) -> String {
|
||||||
|
let home = dirs::home_dir().map(|h| h.display().to_string()).unwrap_or_else(|| "/tmp".into());
|
||||||
|
let log = xml_escape(&format!("{home}/Library/Logs/anybot-serve.log"));
|
||||||
|
let bin = xml_escape(&exe.display().to_string());
|
||||||
|
format!(
|
||||||
|
r#"<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0"><dict>
|
||||||
|
<key>Label</key><string>com.anybot.serve</string>
|
||||||
|
<key>ProgramArguments</key>
|
||||||
|
<array><string>{bin}</string><string>--serve</string></array>
|
||||||
|
<key>RunAtLoad</key><true/>
|
||||||
|
<key>KeepAlive</key><true/>
|
||||||
|
<key>StandardErrorPath</key><string>{log}</string>
|
||||||
|
<key>StandardOutPath</key><string>{log}</string>
|
||||||
|
</dict></plist>
|
||||||
|
"#
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn install_launchd(exe: &std::path::Path) -> Result<String, String> {
|
||||||
|
let home = dirs::home_dir().ok_or("找不到家目錄")?;
|
||||||
|
let dir = home.join("Library/LaunchAgents");
|
||||||
|
std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
|
||||||
|
let _ = std::fs::create_dir_all(home.join("Library/Logs"));
|
||||||
|
let plist = dir.join("com.anybot.serve.plist");
|
||||||
|
write_atomic(&plist, launchd_plist(exe)).map_err(|e| e.to_string())?;
|
||||||
|
let uid = uid();
|
||||||
|
let target = format!("gui/{uid}/com.anybot.serve");
|
||||||
|
let _ = std::process::Command::new("launchctl").args(["bootout", &target]).status();
|
||||||
|
let boot = std::process::Command::new("launchctl").args(["bootstrap", &format!("gui/{uid}")]).arg(&plist).status();
|
||||||
|
if boot.map(|s| s.success()).unwrap_or(false) {
|
||||||
|
let _ = std::process::Command::new("launchctl").args(["enable", &target]).status();
|
||||||
|
let _ = std::process::Command::new("launchctl").args(["kickstart", "-k", &target]).status();
|
||||||
|
return Ok(format!("開機自動執行(launchd {})。權限請開給這個 App,不要開給另一份 anybot。", plist.display()));
|
||||||
|
}
|
||||||
|
let load = std::process::Command::new("launchctl").args(["load", "-w"]).arg(&plist).status();
|
||||||
|
if load.map(|s| s.success()).unwrap_or(false) {
|
||||||
|
return Ok(format!("開機自動執行(launchctl load {})", plist.display()));
|
||||||
|
}
|
||||||
|
Err("launchctl 沒能載入。請登入圖形介面後再執行 anybot setup。".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn schtasks_tr(exe: &std::path::Path) -> String {
|
||||||
|
format!("\"{}\" --serve", exe.display())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn install_schtasks(exe: &std::path::Path) -> Result<String, String> {
|
||||||
|
let tr = schtasks_tr(exe);
|
||||||
|
let o = std::process::Command::new("schtasks").args(["/Create", "/F", "/TN", "AnyBot", "/SC", "ONLOGON", "/RL", "LIMITED", "/IT", "/TR", &tr]).output().map_err(|e| format!("執行 schtasks 失敗:{e}"))?;
|
||||||
|
if !o.status.success() {
|
||||||
|
let err = String::from_utf8_lossy(&o.stderr);
|
||||||
|
let out = String::from_utf8_lossy(&o.stdout);
|
||||||
|
return Err(format!("工作排程器失敗:{}{}", err.trim(), out.trim()));
|
||||||
|
}
|
||||||
|
let _ = std::process::Command::new("schtasks").args(["/Run", "/TN", "AnyBot"]).status();
|
||||||
|
Ok("登入後自動執行(工作排程器 AnyBot)。操控的是這個已登入的桌面。".into())
|
||||||
|
}
|
||||||
|
|
||||||
/// `anybot setup --refresh-service` (install.sh runs it after an update): write the start script
|
/// `anybot setup --refresh-service` (install.sh runs it after an update): write the start script
|
||||||
/// again — a newer AnyBot may start differently — and restart. The computer stays joined as it was,
|
/// again — a newer AnyBot may start differently — and restart. The computer stays joined as it was,
|
||||||
/// and a screen remembered at setup stays.
|
/// and a screen remembered at setup stays.
|
||||||
fn refresh_service() -> i32 {
|
fn refresh_service() -> i32 {
|
||||||
|
let exe = match std::env::current_exe().and_then(|p| p.canonicalize()) {
|
||||||
|
Ok(e) => e,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("✗ 找不到 AnyBot 程式:{e}");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if cfg!(target_os = "macos") {
|
||||||
|
return match install_launchd(&exe) {
|
||||||
|
Ok(n) => {
|
||||||
|
println!("{n}");
|
||||||
|
0
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("✗ {e}");
|
||||||
|
1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if cfg!(target_os = "windows") {
|
||||||
|
return match install_schtasks(&exe) {
|
||||||
|
Ok(n) => {
|
||||||
|
println!("{n}");
|
||||||
|
0
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("✗ {e}");
|
||||||
|
1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
let Some(home) = dirs::home_dir() else { return 1 };
|
let Some(home) = dirs::home_dir() else { return 1 };
|
||||||
let launcher = home.join(".local/bin/anybot-run");
|
let launcher = home.join(".local/bin/anybot-run");
|
||||||
let Ok(old) = std::fs::read_to_string(&launcher) else {
|
let Ok(old) = std::fs::read_to_string(&launcher) else {
|
||||||
|
|
@ -852,13 +1000,6 @@ fn refresh_service() -> i32 {
|
||||||
let role = if old.contains("accessibility bus") { Role::Hands } else { Role::Brain };
|
let role = if old.contains("accessibility bus") { Role::Hands } else { Role::Brain };
|
||||||
let display = old.lines().find_map(|l| l.strip_prefix("export DISPLAY=\"${DISPLAY:-").and_then(|r| r.strip_suffix("}\""))).unwrap_or("").to_string();
|
let display = old.lines().find_map(|l| l.strip_prefix("export DISPLAY=\"${DISPLAY:-").and_then(|r| r.strip_suffix("}\""))).unwrap_or("").to_string();
|
||||||
let env = Env { linux: cfg!(target_os = "linux"), display, systemd: true };
|
let env = Env { linux: cfg!(target_os = "linux"), display, systemd: true };
|
||||||
let exe = match std::env::current_exe().and_then(|p| p.canonicalize()) {
|
|
||||||
Ok(e) => e,
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("✗ 找不到 AnyBot 程式:{e}");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if let Err(e) = write_atomic(&launcher, launcher_script(role, &exe, &env)) {
|
if let Err(e) = write_atomic(&launcher, launcher_script(role, &exe, &env)) {
|
||||||
eprintln!("✗ 寫入 {} 失敗:{e}", launcher.display());
|
eprintln!("✗ 寫入 {} 失敗:{e}", launcher.display());
|
||||||
return 1;
|
return 1;
|
||||||
|
|
@ -921,6 +1062,107 @@ fn lingering() -> bool {
|
||||||
std::process::Command::new("loginctl").args(["show-user", &user, "-p", "Linger"]).output().map(|o| String::from_utf8_lossy(&o.stdout).contains("Linger=yes")).unwrap_or(true)
|
std::process::Command::new("loginctl").args(["show-user", &user, "-p", "Linger"]).output().map(|o| String::from_utf8_lossy(&o.stdout).contains("Linger=yes")).unwrap_or(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn provider_from_name(s: &str) -> Option<crate::providers::ProviderKind> {
|
||||||
|
use crate::providers::ProviderKind::*;
|
||||||
|
match s.trim().to_ascii_lowercase().as_str() {
|
||||||
|
"xai" | "grok" => Some(Xai),
|
||||||
|
"openai" | "open-ai" => Some(OpenAi),
|
||||||
|
"anthropic" | "claude" => Some(Anthropic),
|
||||||
|
"gemini" | "google" => Some(Gemini),
|
||||||
|
"openrouter" => Some(OpenRouter),
|
||||||
|
"opencode" | "opencode-go" | "opencode_go" => Some(OpenCodeGo),
|
||||||
|
"ollama" => Some(Ollama),
|
||||||
|
"custom" => Some(Custom),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `anybot setup --seed`: the installer's first-run config. Writes the model key and, if there
|
||||||
|
/// is no bot yet, makes the first one. Does not ask.
|
||||||
|
fn seed_first() -> i32 {
|
||||||
|
use crate::model::{Bot, State};
|
||||||
|
let name = std::env::var("ANYBOT_PROVIDER").unwrap_or_default();
|
||||||
|
let Some(kind) = provider_from_name(&name) else {
|
||||||
|
eprintln!("✗ 請設 ANYBOT_PROVIDER(openai、anthropic、xai、gemini、openrouter、opencode、ollama、custom)");
|
||||||
|
return 2;
|
||||||
|
};
|
||||||
|
let key = std::env::var("ANYBOT_API_KEY").unwrap_or_default();
|
||||||
|
if kind.needs_key() && key.is_empty() {
|
||||||
|
eprintln!("✗ 請設 ANYBOT_API_KEY");
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
let _folder = match crate::lock::take(Duration::from_secs(10)) {
|
||||||
|
Ok(l) => l,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("✗ {e}");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut st = State::load();
|
||||||
|
{
|
||||||
|
let ps = st.provider_mut(kind);
|
||||||
|
if !key.is_empty() {
|
||||||
|
ps.api_key = key;
|
||||||
|
}
|
||||||
|
if let Ok(base) = std::env::var("ANYBOT_BASE_URL") {
|
||||||
|
if !base.trim().is_empty() {
|
||||||
|
ps.base_url = base;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let model = std::env::var("ANYBOT_MODEL").unwrap_or_default();
|
||||||
|
if !model.is_empty() {
|
||||||
|
ps.default_model = model;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let model = {
|
||||||
|
let chosen = std::env::var("ANYBOT_MODEL").unwrap_or_default();
|
||||||
|
if chosen.is_empty() { kind.suggested_model().to_string() } else { chosen }
|
||||||
|
};
|
||||||
|
if st.bots.is_empty() {
|
||||||
|
let bot_name = std::env::var("ANYBOT_BOT_NAME").unwrap_or_else(|_| "Bot".into());
|
||||||
|
st.bots.push(Bot {
|
||||||
|
id: 1,
|
||||||
|
name: bot_name,
|
||||||
|
description: String::new(),
|
||||||
|
instructions: String::new(),
|
||||||
|
shape: "squircle".into(),
|
||||||
|
color: "black".into(),
|
||||||
|
provider: kind,
|
||||||
|
model,
|
||||||
|
step_model: String::new(),
|
||||||
|
pinned: false,
|
||||||
|
folder: String::new(),
|
||||||
|
messages: vec![],
|
||||||
|
unread: false,
|
||||||
|
tools_enabled: true,
|
||||||
|
workdir: crate::model::default_workdir(),
|
||||||
|
allow_rules: vec![],
|
||||||
|
members: vec![],
|
||||||
|
threads: vec![],
|
||||||
|
memories: vec![],
|
||||||
|
speak_replies: false,
|
||||||
|
voice: String::new(),
|
||||||
|
voice_rate: 1.0,
|
||||||
|
voice_lang: String::new(),
|
||||||
|
demos: vec![],
|
||||||
|
context_tokens: crate::agent::DEFAULT_CONTEXT_TOKENS,
|
||||||
|
routines: vec![],
|
||||||
|
last_turn: None,
|
||||||
|
default_computer: crate::model::default_computer(),
|
||||||
|
});
|
||||||
|
st.primary_bot = 1;
|
||||||
|
st.selected = 1;
|
||||||
|
st.next_id = 2;
|
||||||
|
println!("已建立第一個 Bot,模型是 {}。", kind.label());
|
||||||
|
} else {
|
||||||
|
println!("已寫入 {} 的金鑰。既有的 Bot 沒有改。", kind.label());
|
||||||
|
}
|
||||||
|
if st.save() { 0 } else {
|
||||||
|
eprintln!("✗ 寫不進設定檔");
|
||||||
|
1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- entry ----------
|
// ---------- entry ----------
|
||||||
|
|
||||||
/// Flags that take a value (so the value isn't read as the address or the code).
|
/// Flags that take a value (so the value isn't read as the address or the code).
|
||||||
|
|
@ -929,12 +1171,15 @@ const VALUED: [&str; 4] = ["--server", "--user", "--name", "--brain-name"];
|
||||||
pub fn run(args: &[String]) -> i32 {
|
pub fn run(args: &[String]) -> i32 {
|
||||||
let flag = |k: &str| args.iter().position(|a| a == k).and_then(|i| args.get(i + 1)).cloned();
|
let flag = |k: &str| args.iter().position(|a| a == k).and_then(|i| args.get(i + 1)).cloned();
|
||||||
if args.iter().any(|a| a == "-h" || a == "--help") {
|
if args.iter().any(|a| a == "-h" || a == "--help") {
|
||||||
println!("anybot setup:把這台電腦接到你的 AnyBot。\n\n anybot setup 網址 配對碼 (手機或電腦的 AnyBot 按「加一台電腦」會給你這一行)\n anybot setup 一個畫面:填網址和配對碼(或帳號密碼)\n 加 --brain 這台當大腦,不當手腳\n 加 --no-service 不設開機自動執行\n 加 --real-screen 讓 Bot 用這台登入中的桌面(預設用自己的虛擬螢幕)\n\n給程式用:anybot setup --server 網址 --user 帳號 [--brain-name 大腦名字],密碼放在 ANYBOT_ACCOUNT_PASSWORD。");
|
println!("anybot setup:把這台電腦接到你的 AnyBot。\n\n anybot setup 網址 配對碼 (手機或電腦的 AnyBot 按「加一台電腦」會給你這一行)\n anybot setup 一個畫面:填網址和配對碼(或帳號密碼)\n 加 --brain 這台當大腦,不當手腳\n 加 --no-service 不設開機自動執行\n 加 --real-screen 讓 Bot 用這台登入中的桌面(預設用自己的虛擬螢幕)\n anybot setup --seed 寫入第一次的模型(ANYBOT_PROVIDER、ANYBOT_API_KEY)\n\n給程式用:anybot setup --server 網址 --user 帳號 [--brain-name 大腦名字],密碼放在 ANYBOT_ACCOUNT_PASSWORD。");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if args.iter().any(|a| a == "--refresh-service") {
|
if args.iter().any(|a| a == "--refresh-service") {
|
||||||
return refresh_service();
|
return refresh_service();
|
||||||
}
|
}
|
||||||
|
if args.iter().any(|a| a == "--seed") {
|
||||||
|
return seed_first();
|
||||||
|
}
|
||||||
let role = if args.iter().any(|a| a == "--brain") { Role::Brain } else { Role::Hands };
|
let role = if args.iter().any(|a| a == "--brain") { Role::Brain } else { Role::Hands };
|
||||||
let mut pos = vec![];
|
let mut pos = vec![];
|
||||||
let mut skip = false;
|
let mut skip = false;
|
||||||
|
|
@ -1126,7 +1371,7 @@ fn run_script(role: Role, server: &str, user: &str, brain_name: Option<String>,
|
||||||
/// Save, start with the computer, check; printed line by line.
|
/// Save, start with the computer, check; printed line by line.
|
||||||
fn finish(role: Role, s: Settings, brain: Option<Peer>, service: bool) -> i32 {
|
fn finish(role: Role, s: Settings, brain: Option<Peer>, service: bool) -> i32 {
|
||||||
let env = Env::detect();
|
let env = Env::detect();
|
||||||
let plan = Plan { role, settings: s, brain, service: service && env.systemd, env };
|
let plan = Plan { role, settings: s, brain, service, env };
|
||||||
let steps = plan.steps();
|
let steps = plan.steps();
|
||||||
let (tx, rx) = channel();
|
let (tx, rx) = channel();
|
||||||
std::thread::spawn(move || plan.run(&tx));
|
std::thread::spawn(move || plan.run(&tx));
|
||||||
|
|
@ -1246,5 +1491,20 @@ mod tests {
|
||||||
w.url.clear();
|
w.url.clear();
|
||||||
w.type_text(&crate::remote::pair_line("https://anybot.example.com/", "135790"));
|
w.type_text(&crate::remote::pair_line("https://anybot.example.com/", "135790"));
|
||||||
assert_eq!((w.url.as_str(), w.code.as_str()), ("https://anybot.example.com", "135790"));
|
assert_eq!((w.url.as_str(), w.code.as_str()), ("https://anybot.example.com", "135790"));
|
||||||
|
let mut w = Wiz::new(Role::Hands);
|
||||||
|
w.focus = 0;
|
||||||
|
w.url.clear();
|
||||||
|
w.type_text(&crate::remote::pair_block("https://anybot.example.com/", "135790"));
|
||||||
|
assert_eq!((w.url.as_str(), w.code.as_str()), ("https://anybot.example.com", "135790"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn provider_names_and_autostart_quoting() {
|
||||||
|
assert!(provider_from_name("OpenAI").is_some());
|
||||||
|
assert!(provider_from_name("nope").is_none());
|
||||||
|
let plist = launchd_plist(std::path::Path::new("/Applications/AnyBot.app/Contents/MacOS/AnyBot"));
|
||||||
|
assert!(plist.contains("<string>--serve</string>") && plist.contains("AnyBot"));
|
||||||
|
let tr = schtasks_tr(std::path::Path::new(r"C:\Users\a b\anybot.exe"));
|
||||||
|
assert!(tr.contains("anybot.exe") && tr.contains("--serve") && tr.contains('"'), "{tr}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue