多台電腦(大腦、手腳、螢幕)、一行加電腦、文件與 make #2
|
|
@ -8,6 +8,8 @@
|
|||
|
||||
# Local secrets (deploy/server/.env.example is the template)
|
||||
.env
|
||||
# Written by deploy/server/setup.sh
|
||||
/deploy/server/Caddyfile
|
||||
*.env.local
|
||||
|
||||
# macOS
|
||||
|
|
|
|||
|
|
@ -343,6 +343,7 @@ dependencies = [
|
|||
"chrono",
|
||||
"chrono-tz",
|
||||
"cpal",
|
||||
"crossterm",
|
||||
"dirs",
|
||||
"eframe",
|
||||
"flate2",
|
||||
|
|
@ -362,6 +363,7 @@ dependencies = [
|
|||
"serde_json",
|
||||
"tungstenite",
|
||||
"unicode-normalization",
|
||||
"unicode-width",
|
||||
"ureq",
|
||||
"wayland-client",
|
||||
"wayland-protocols",
|
||||
|
|
@ -396,7 +398,7 @@ dependencies = [
|
|||
"objc2-foundation 0.3.2",
|
||||
"parking_lot",
|
||||
"percent-encoding",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
"wl-clipboard-rs",
|
||||
"x11rb 0.13.2",
|
||||
]
|
||||
|
|
@ -1032,6 +1034,15 @@ version = "0.10.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cookie"
|
||||
version = "0.18.2"
|
||||
|
|
@ -1227,6 +1238,33 @@ version = "0.8.23"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"crossterm_winapi",
|
||||
"derive_more",
|
||||
"document-features",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"rustix 1.1.5",
|
||||
"signal-hook",
|
||||
"signal-hook-mio",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossterm_winapi"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.4"
|
||||
|
|
@ -1294,6 +1332,28 @@ version = "0.5.9"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e9de72ce2ad1f90dc62fa25f0f430ef85eb4b0d8fa0be4f30373bc40a21d28e"
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134"
|
||||
dependencies = [
|
||||
"derive_more-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more-impl"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustc_version",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
|
|
@ -1644,7 +1704,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2879,6 +2939,18 @@ dependencies = [
|
|||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "moxcms"
|
||||
version = "0.8.1"
|
||||
|
|
@ -3499,7 +3571,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -4205,7 +4277,7 @@ dependencies = [
|
|||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.12.1",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -4392,6 +4464,27 @@ version = "2.0.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"signal-hook-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-mio"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"mio",
|
||||
"signal-hook",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.8"
|
||||
|
|
@ -4855,10 +4948,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.3.4",
|
||||
"getrandom 0.4.3",
|
||||
"once_cell",
|
||||
"rustix 1.1.5",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -5904,15 +5997,37 @@ dependencies = [
|
|||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.62.2"
|
||||
|
|
|
|||
|
|
@ -14,6 +14,9 @@ ureq = { version = "3", features = ["json"] }
|
|||
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12", "logging"] }
|
||||
webpki-roots = "1"
|
||||
flate2 = "1"
|
||||
# `anybot setup`: the guided setup in a terminal (servers and Linux computers without a window).
|
||||
crossterm = "0.29"
|
||||
unicode-width = "0.2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
dirs = "7"
|
||||
|
|
|
|||
|
|
@ -1,29 +1,28 @@
|
|||
#!/bin/sh
|
||||
# Make this Linux computer a pair of hands for your AnyBot brain: the brain's bots can see and use
|
||||
# its screen, files and browser (asking you first, as usual). Works on a desktop and on a computer
|
||||
# without a screen (a virtual screen is made for it). Run from the app/ folder:
|
||||
# Make this Linux computer a pair of hands for your AnyBot brain (or a brain itself): installs what
|
||||
# it needs, then opens the guided setup (`anybot setup`), where you pick the brain from your
|
||||
# account's AnyBots. Works on a desktop and on a computer without a screen. Run from app/:
|
||||
#
|
||||
# ./deploy/linux/hands.sh --deps also install what a computer without a screen needs (sudo)
|
||||
# ./deploy/linux/hands.sh set up and start it (asks for your account the first time)
|
||||
# ./deploy/linux/hands.sh --stop stop it and don't start it with the computer any more
|
||||
# ./deploy/linux/hands.sh install AnyBot if needed, then the setup
|
||||
# ./deploy/linux/hands.sh --stop stop AnyBot and don't start it with the computer any more
|
||||
set -e
|
||||
cd "$(dirname "$0")/../.."
|
||||
UNIT="$HOME/.config/systemd/user/anybot-hands.service"
|
||||
ENVF="$HOME/.config/anybot/hands.env"
|
||||
RUN="$HOME/.local/bin/anybot-hands"
|
||||
|
||||
if [ "$1" = "--stop" ]; then
|
||||
systemctl --user disable --now anybot-hands.service 2>/dev/null || true
|
||||
rm -f "$UNIT"
|
||||
systemctl --user daemon-reload
|
||||
echo "Stopped. (Your account details stay in $ENVF; delete it if you like.)"
|
||||
for u in anybot.service anybot-hands.service; do
|
||||
systemctl --user disable --now "$u" 2>/dev/null || true
|
||||
rm -f "$HOME/.config/systemd/user/$u"
|
||||
done
|
||||
systemctl --user daemon-reload 2>/dev/null || true
|
||||
echo "Stopped. Run ./deploy/linux/hands.sh (or anybot setup) to set it up again."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$1" = "--deps" ]; then
|
||||
# A virtual screen, a window manager, the accessibility bus (for reading apps' buttons), a session bus.
|
||||
if command -v apt-get >/dev/null; then
|
||||
sudo apt-get update && sudo apt-get install -y xvfb openbox at-spi2-core libatk-adaptor dbus-x11 xauth fonts-noto-cjk
|
||||
sudo apt-get update && sudo apt-get install -y xvfb openbox at-spi2-core libatk-adaptor dbus-x11 dbus-user-session xauth fonts-noto-cjk
|
||||
elif command -v dnf >/dev/null; then
|
||||
sudo dnf install -y xorg-x11-server-Xvfb openbox at-spi2-core at-spi2-atk dbus-x11 xorg-x11-xauth google-noto-sans-cjk-fonts
|
||||
elif command -v pacman >/dev/null; then
|
||||
|
|
@ -33,71 +32,7 @@ if [ "$1" = "--deps" ]; then
|
|||
fi
|
||||
fi
|
||||
|
||||
command -v systemctl >/dev/null || { echo "This needs systemd (to keep AnyBot running)."; exit 1; }
|
||||
[ -x "$HOME/.local/bin/anybot" ] || sh deploy/linux/install.sh
|
||||
|
||||
if [ ! -f "$ENVF" ]; then
|
||||
echo "Your AnyBot account (the same one your brain uses):"
|
||||
printf " Account server address (e.g. https://anybot.example.com): "; read -r URL
|
||||
printf " Account name: "; read -r USERNAME
|
||||
printf " Password: "; stty -echo 2>/dev/null; read -r PASSWORD; stty echo 2>/dev/null; echo
|
||||
printf " Name for this computer [%s]: " "$(hostname)"; read -r NAME
|
||||
printf " Brain's name (empty = only lend hands, show no bots here): "; read -r BRAIN
|
||||
mkdir -p "$(dirname "$ENVF")"
|
||||
umask 077
|
||||
# Each value in single quotes, so a password with $ or spaces stays as typed.
|
||||
q() { printf "%s='%s'\n" "$1" "$(printf %s "$2" | sed "s/'/'\\\\''/g")"; }
|
||||
{
|
||||
q ANYBOT_ACCOUNT "${URL%/}|$USERNAME"
|
||||
q ANYBOT_ACCOUNT_PASSWORD "$PASSWORD"
|
||||
q ANYBOT_HOST_NAME "${NAME:-$(hostname)}"
|
||||
if [ -n "$BRAIN" ]; then q ANYBOT_BRAIN "$BRAIN"; fi
|
||||
} > "$ENVF"
|
||||
chmod 600 "$ENVF"
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$RUN")" "$(dirname "$UNIT")"
|
||||
cat > "$RUN" <<'SH'
|
||||
#!/bin/sh
|
||||
# Started by systemd (anybot-hands.service).
|
||||
set -a; . "$HOME/.config/anybot/hands.env"; set +a
|
||||
export ANYBOT_DEVICE_LINK=1
|
||||
export LANG="${LANG:-C.UTF-8}"
|
||||
# The accessibility bus, so bots can read apps' buttons instead of only looking at pixels.
|
||||
export NO_AT_BRIDGE=0 GTK_A11Y=atspi QT_LINUX_ACCESSIBILITY_ALWAYS_ON=1
|
||||
if [ -z "$DISPLAY" ] && [ -z "$WAYLAND_DISPLAY" ]; then
|
||||
# No screen: a virtual one with a window manager, and a session bus for the accessibility bus.
|
||||
exec xvfb-run -a -s "-screen 0 1440x900x24" dbus-run-session -- sh -c 'openbox >/dev/null 2>&1 & exec "$HOME/.local/bin/anybot" --serve'
|
||||
fi
|
||||
exec "$HOME/.local/bin/anybot" --serve
|
||||
SH
|
||||
chmod 755 "$RUN"
|
||||
|
||||
cat > "$UNIT" <<UNIT
|
||||
[Unit]
|
||||
Description=AnyBot hands (lets your AnyBot brain use this computer)
|
||||
After=network-online.target graphical-session.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
ExecStart=$RUN
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
UNIT
|
||||
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable --now anybot-hands.service
|
||||
sleep 3
|
||||
if systemctl --user is-active --quiet anybot-hands.service; then
|
||||
echo "Running. The brain's bots can now use this computer (they ask you first, as usual)."
|
||||
echo "Watch it: journalctl --user -u anybot-hands -f"
|
||||
else
|
||||
echo "It didn't start. See: journalctl --user -u anybot-hands -n 50"
|
||||
fi
|
||||
if ! loginctl show-user "$USER" 2>/dev/null | grep -q "Linger=yes"; then
|
||||
echo "To keep it running after you log out and after a restart, run once:"
|
||||
echo " sudo loginctl enable-linger $USER"
|
||||
fi
|
||||
# Older versions of this script kept the account in a file; the setup keeps only a key.
|
||||
rm -f "$HOME/.config/anybot/hands.env" "$HOME/.local/bin/anybot-hands"
|
||||
exec "$HOME/.local/bin/anybot" setup
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ command -v update-desktop-database >/dev/null && update-desktop-database "$APPS"
|
|||
command -v gtk-update-icon-cache >/dev/null && gtk-update-icon-cache -q "$ICONS" 2>/dev/null || true
|
||||
|
||||
echo "AnyBot is installed: find it in your app menu, or run: anybot"
|
||||
echo "To make this computer a brain, or hands for your brain (also without a screen): anybot setup"
|
||||
case ":$PATH:" in *":$BIN:"*) ;; *) echo "(Add $BIN to your PATH to start it from a terminal.)" ;; esac
|
||||
if [ "${XDG_SESSION_TYPE:-}" = "wayland" ]; then
|
||||
echo "Wayland: bots can see and use your screen. On GNOME/KDE the first time, a system window asks:"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
# Everything on one small server: the account server (relay), HTTPS (Caddy), and AnyBot itself.
|
||||
# 1. Point your domain at the server and put it in ../../relay/Caddyfile.
|
||||
# Easiest: ./setup.sh (asks for the domain, account and keys, writes .env and Caddyfile, starts
|
||||
# everything and checks it). By hand:
|
||||
# 1. Point your domain at the server; copy ../../relay/Caddyfile here and put the domain in it.
|
||||
# 2. Put your account name and password in .env (see .env.example).
|
||||
# 3. docker compose up -d
|
||||
# 4. On the phone open https://your-domain, create the account (first time), and use it.
|
||||
|
|
@ -10,7 +12,7 @@
|
|||
# and works even while your computers sleep. On each of your computers: 設定 › 手機與帳號 ›
|
||||
# 大腦 › 「改用它當大腦」. Those computers then show these bots, and the bots can use them as hands
|
||||
# (screen, files, browser), asking you first as usual. A Linux computer without a screen can be
|
||||
# hands too: deploy/linux/hands.sh.
|
||||
# hands too: run `anybot setup` there and pick this brain.
|
||||
services:
|
||||
relay:
|
||||
build:
|
||||
|
|
@ -28,7 +30,8 @@ services:
|
|||
restart: unless-stopped
|
||||
ports: ["80:80", "443:443"]
|
||||
volumes:
|
||||
- ../../relay/Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
# Written by setup.sh (your domain); ../../relay/Caddyfile is the template.
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- caddy-data:/data
|
||||
depends_on: [relay]
|
||||
anybot:
|
||||
|
|
@ -41,6 +44,9 @@ services:
|
|||
ANYBOT_ACCOUNT: "http://relay:8080|${ANYBOT_USER}"
|
||||
ANYBOT_ACCOUNT_PASSWORD: "${ANYBOT_PASSWORD}"
|
||||
ANYBOT_HOST_NAME: "${ANYBOT_HOST_NAME:-雲端 AnyBot}"
|
||||
# First start on a new server: make the account (the first one needs no invite).
|
||||
ANYBOT_ACCOUNT_CREATE: "1"
|
||||
ANYBOT_RELAY_INVITE: "${ANYBOT_RELAY_INVITE:-}"
|
||||
XAI_API_KEY: "${XAI_API_KEY:-}"
|
||||
ANTHROPIC_API_KEY: "${ANTHROPIC_API_KEY:-}"
|
||||
OPENAI_API_KEY: "${OPENAI_API_KEY:-}"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,283 @@
|
|||
#!/usr/bin/env bash
|
||||
# AnyBot 伺服器設定: the account server, HTTPS and the brain on this server, step by step.
|
||||
#
|
||||
# cd app/deploy/server && ./setup.sh ask, write .env and Caddyfile, start, check
|
||||
# ./setup.sh --dry-run ask and write the files, but don't start anything
|
||||
#
|
||||
# Run it again any time: it offers to keep the current settings (to update and restart).
|
||||
set -u
|
||||
cd "$(dirname "$0")"
|
||||
DRY=0
|
||||
[ "${1:-}" = "--dry-run" ] && DRY=1
|
||||
|
||||
# ---------- looks ----------
|
||||
if [ -t 1 ]; then
|
||||
B=$'\e[1m'; D=$'\e[90m'; A=$'\e[36m'; G=$'\e[32m'; Y=$'\e[33m'; R=$'\e[31m'; N=$'\e[0m'
|
||||
else
|
||||
B=""; D=""; A=""; G=""; Y=""; R=""; N=""
|
||||
fi
|
||||
STEPS=("檢查" "網址" "帳號" "金鑰" "啟動")
|
||||
AT=0
|
||||
|
||||
header() {
|
||||
printf '\e[2J\e[H\n'
|
||||
printf ' %s◆%s %sAnyBot 伺服器設定%s ' "$A" "$N" "$B" "$N"
|
||||
local i
|
||||
for i in "${!STEPS[@]}"; do
|
||||
[ "$i" -gt 0 ] && printf '%s ─ %s' "$D" "$N"
|
||||
if [ "$i" -lt "$AT" ]; then printf '%s✓ %s%s' "$G" "${STEPS[$i]}" "$N"
|
||||
elif [ "$i" -eq "$AT" ]; then printf '%s%s%d %s%s' "$A" "$B" $((i + 1)) "${STEPS[$i]}" "$N"
|
||||
else printf '%s%d %s%s' "$D" $((i + 1)) "${STEPS[$i]}" "$N"; fi
|
||||
done
|
||||
printf '\n\n'
|
||||
}
|
||||
# A card: title, then lines (each printed after the left edge).
|
||||
card_open() { printf ' %s╭─%s %s%s%s\n %s│%s\n' "$D" "$N" "$B" "$1" "$N" "$D" "$N"; }
|
||||
line() { printf ' %s│%s %s\n' "$D" "$N" "$*"; }
|
||||
note() { printf ' %s│%s %s%s%s\n' "$D" "$N" "$D" "$*" "$N"; }
|
||||
ok() { printf ' %s│%s %s✓%s %s\n' "$D" "$N" "$G" "$N" "$*"; }
|
||||
warn() { printf ' %s│%s %s!%s %s\n' "$D" "$N" "$Y" "$N" "$*"; }
|
||||
bad() { printf ' %s│%s %s✗ %s%s\n' "$D" "$N" "$R" "$*" "$N"; }
|
||||
card_close() { printf ' %s│%s\n %s╰────────────────────────────────────────────────%s\n' "$D" "$N" "$D" "$N"; [ -n "${1:-}" ] && printf ' %s%s%s\n' "$D" "$1" "$N"; }
|
||||
|
||||
# Pad a label to 10 columns (Chinese characters are two columns wide).
|
||||
padl() {
|
||||
local w
|
||||
w=$(printf '%s' "$1" | LC_ALL=C.UTF-8 wc -L 2>/dev/null || printf '%s' "$1" | wc -m)
|
||||
printf '%s%*s' "$1" $((10 - w > 1 ? 10 - w : 1)) ""
|
||||
}
|
||||
|
||||
# ask VAR "標籤" [default] [secret]: once answered, the line is redrawn without the marker.
|
||||
ask() {
|
||||
local __v=$1 label=$2 def=${3:-} secret=${4:-} got shown
|
||||
printf ' %s│%s %s❯%s %s%s%s' "$D" "$N" "$A" "$N" "$A" "$(padl "$label")" "$N"
|
||||
if [ -n "$secret" ]; then
|
||||
IFS= read -rs got; printf '\n'
|
||||
shown=$(printf '%s' "$got" | sed 's/./●/g')
|
||||
else
|
||||
IFS= read -r -e -i "$def" got || true
|
||||
shown=$got
|
||||
fi
|
||||
printf '\e[1A\r\e[K %s│%s %s%s%s%s\n' "$D" "$N" "$D" "$(padl "$label")" "$N" "$shown"
|
||||
printf -v "$__v" '%s' "$got"
|
||||
}
|
||||
|
||||
# choose VAR "問題" "選項|說明" ... (↑↓ and Enter)
|
||||
choose() {
|
||||
local __v=$1 title=$2; shift 2
|
||||
local opts=("$@") sel=0 n=$# key rest i
|
||||
while :; do
|
||||
header
|
||||
card_open "$title"
|
||||
for i in "${!opts[@]}"; do
|
||||
local name=${opts[$i]%%|*} desc=${opts[$i]#*|}
|
||||
if [ "$i" -eq "$sel" ]; then line "$A❯ ● $B$name$N"; else line " $D○$N $name"; fi
|
||||
[ "$desc" != "${opts[$i]}" ] && [ -n "$desc" ] && note " $desc"
|
||||
line ""
|
||||
done
|
||||
card_close "↑↓ 選擇 Enter 確定"
|
||||
IFS= read -rsn1 key
|
||||
if [ "$key" = $'\e' ]; then
|
||||
IFS= read -rsn2 -t 0.05 rest || true
|
||||
case "$rest" in "[A") sel=$(((sel + n - 1) % n)) ;; "[B") sel=$(((sel + 1) % n)) ;; esac
|
||||
elif [ "$key" = "k" ]; then sel=$(((sel + n - 1) % n))
|
||||
elif [ "$key" = "j" ]; then sel=$(((sel + 1) % n))
|
||||
elif [ -z "$key" ]; then break
|
||||
fi
|
||||
done
|
||||
printf -v "$__v" '%s' "$sel"
|
||||
}
|
||||
|
||||
spin() { # spin "做什麼" command… : runs it, shows a spinner and the newest log line
|
||||
local what=$1; shift
|
||||
local log; log=$(mktemp)
|
||||
"$@" >"$log" 2>&1 &
|
||||
local pid=$! f=0 c=("⠋" "⠙" "⠹" "⠸" "⠼" "⠴" "⠦" "⠧" "⠇" "⠏")
|
||||
while kill -0 "$pid" 2>/dev/null; do
|
||||
local last; last=$(tail -n 1 "$log" | tr -d '\r' | cut -c1-60)
|
||||
printf '\r\e[K %s│%s %s%s%s %s %s%s%s' "$D" "$N" "$A" "${c[$((f % 10))]}" "$N" "$what" "$D" "$last" "$N"
|
||||
f=$((f + 1)); sleep 0.15
|
||||
done
|
||||
wait "$pid"; local rc=$?
|
||||
printf '\r\e[K'
|
||||
if [ $rc -eq 0 ]; then ok "$what"; else bad "$what 失敗:"; tail -n 8 "$log" | while IFS= read -r l; do note " $l"; done; fi
|
||||
rm -f "$log"
|
||||
return $rc
|
||||
}
|
||||
|
||||
pause() { printf ' %s%s%s' "$D" "${1:-按 Enter 繼續}" "$N"; IFS= read -r _; }
|
||||
die() { card_close; printf '\n'; exit 1; }
|
||||
|
||||
# ---------- 0. this server ----------
|
||||
AT=0; header; card_open "檢查這台伺服器"
|
||||
FAIL=0
|
||||
if command -v docker >/dev/null; then ok "Docker 已安裝"; else bad "沒有 Docker:curl -fsSL https://get.docker.com | sh"; FAIL=1; fi
|
||||
if docker compose version >/dev/null 2>&1; then ok "docker compose 可以用"; elif [ $FAIL = 0 ]; then bad "沒有 docker compose(Docker 太舊?請更新 Docker)"; FAIL=1; fi
|
||||
if [ $FAIL = 0 ] && ! docker info >/dev/null 2>&1; then bad "沒有權限使用 Docker:sudo usermod -aG docker \$USER,登出再登入"; FAIL=1; fi
|
||||
if command -v ss >/dev/null; then
|
||||
busy=$(ss -ltnH 2>/dev/null | awk '{print $4}' | grep -E ':(80|443)$' | grep -v docker || true)
|
||||
if [ -n "$busy" ] && ! docker compose ps -q caddy 2>/dev/null | grep -q .; then warn "80 或 443 埠已被別的程式佔用(HTTPS 需要它們)"; else ok "80、443 埠可以用"; fi
|
||||
fi
|
||||
mem=$(awk '/MemTotal/ {print int($2/1024/1024+0.5)}' /proc/meminfo 2>/dev/null || echo 0)
|
||||
[ "${mem:-0}" -ge 2 ] && ok "記憶體 ${mem} GB" || warn "記憶體只有 ${mem:-?} GB(建議 2 GB 以上,第一次建置會比較久)"
|
||||
disk=$(df -Pk . 2>/dev/null | awk 'NR==2 {print int($4/1024/1024)}')
|
||||
[ "${disk:-0}" -ge 10 ] && ok "硬碟還有 ${disk} GB" || warn "硬碟只剩 ${disk:-?} GB(建議 10 GB 以上)"
|
||||
if [ $FAIL = 1 ] && [ $DRY = 0 ]; then note ""; note "修好上面的問題後再執行一次 ./setup.sh"; die; fi
|
||||
card_close; pause
|
||||
|
||||
# Keep the current settings?
|
||||
if [ -f .env ] && [ -f Caddyfile ]; then
|
||||
# shellcheck disable=SC1091
|
||||
set -a; . ./.env; set +a
|
||||
choose KEEP "這台已經設定過了(${ANYBOT_RELAY_PUBLIC_URL:-})" \
|
||||
"用現有設定,更新並重新啟動|拉下最新版、重建、重新啟動,帳號和 Bot 都不變" \
|
||||
"重新設定|網址、帳號或金鑰要改"
|
||||
if [ "$KEEP" = 0 ]; then
|
||||
DOMAIN=${ANYBOT_RELAY_PUBLIC_URL#https://}; USERNAME=$ANYBOT_USER; INVITE=${ANYBOT_RELAY_INVITE:-}; BRAIN=${ANYBOT_HOST_NAME:-}
|
||||
AT=4; SKIP=1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "${SKIP:-0}" != 1 ]; then
|
||||
# ---------- 1. the address ----------
|
||||
AT=1; MSG=""
|
||||
while :; do
|
||||
header; card_open "手機和其他電腦用哪個網址連到這台?"
|
||||
note "網域要先指到這台伺服器(DNS 的 A 紀錄)。HTTPS 憑證會自動申請、自動更新。"
|
||||
line ""
|
||||
[ -n "$MSG" ] && { printf '%s\n' "$MSG"; line ""; }
|
||||
ask DOMAIN "網域" "${DOMAIN:-}"
|
||||
DOMAIN=${DOMAIN#https://}; DOMAIN=${DOMAIN#http://}; DOMAIN=${DOMAIN%%/*}
|
||||
if ! printf '%s' "$DOMAIN" | grep -Eq '^[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'; then MSG=$(bad "請輸入像 anybot.example.com 這樣的網域"); continue; fi
|
||||
ips=$(getent ahosts "$DOMAIN" 2>/dev/null | awk '{print $1}' | sort -u | tr '\n' ' ')
|
||||
if [ -z "$ips" ]; then
|
||||
card_close
|
||||
choose GO "「$DOMAIN」還沒有指到任何位址" "重新輸入|" "仍然繼續|DNS 剛設好、還沒生效時可以先繼續;生效後憑證會自動申請"
|
||||
[ "$GO" = 0 ] && { MSG=""; continue; }
|
||||
else
|
||||
mine=$(hostname -I 2>/dev/null || true)
|
||||
hit=0; for ip in $ips; do case " $mine " in *" $ip "*) hit=1 ;; esac; done
|
||||
if [ $hit = 1 ]; then MSG=$(ok "$DOMAIN → $ips(就是這台)"); else MSG=$(warn "$DOMAIN → $ips(這台的位址:${mine:-不知道};在 NAT 或雲端主機後面時這是正常的)"); fi
|
||||
header; card_open "手機和其他電腦用哪個網址連到這台?"; printf '%s\n' "$MSG"; card_close; sleep 1
|
||||
fi
|
||||
break
|
||||
done
|
||||
|
||||
# ---------- 2. the account ----------
|
||||
AT=2; MSG=""
|
||||
while :; do
|
||||
header; card_open "你的 AnyBot 帳號"
|
||||
note "手機、電腦都用這個帳號登入。第一次啟動時會自動建立。"
|
||||
line ""
|
||||
[ -n "$MSG" ] && { printf '%s\n' "$MSG"; line ""; }
|
||||
ask USERNAME "帳號" "${USERNAME:-}"
|
||||
if ! printf '%s' "$USERNAME" | grep -Eq '^[A-Za-z0-9._@-]{2,64}$'; then MSG=$(bad "帳號只能用英文字母、數字和 . _ @ -(2–64 個字)"); continue; fi
|
||||
ask PW1 "密碼" "" secret
|
||||
ask PW2 "再一次" "" secret
|
||||
if [ "${#PW1}" -lt 8 ]; then MSG=$(bad "密碼至少 8 個字"); continue; fi
|
||||
if [ "$PW1" != "$PW2" ]; then MSG=$(bad "兩次密碼不一樣"); continue; fi
|
||||
case "$PW1" in *"'"*) MSG=$(bad "密碼裡不能有單引號 '"); continue ;; esac
|
||||
ask BRAIN "大腦名字" "${BRAIN:-雲端大腦}"
|
||||
[ -z "$BRAIN" ] && BRAIN="雲端大腦"
|
||||
break
|
||||
done
|
||||
choose OTHERS "別人可以在這台註冊帳號嗎?" \
|
||||
"只有我|最安全" \
|
||||
"給邀請碼的人可以|家人或同事拿邀請碼註冊,各自有自己的帳號和 Bot"
|
||||
INVITE=""
|
||||
[ "$OTHERS" = 1 ] && INVITE=$(LC_ALL=C tr -dc 'a-z0-9' </dev/urandom | head -c 10)
|
||||
|
||||
# ---------- 3. keys ----------
|
||||
AT=3
|
||||
choose KEYS "AI 模型的金鑰" \
|
||||
"之後在 App 裡設定|在手機或電腦的 AnyBot › 設定 › 模型 填,金鑰只存在大腦上" \
|
||||
"現在輸入|xAI、Anthropic、OpenAI、Gemini,有哪個填哪個" \
|
||||
"從我電腦的備份接手|帳號已經存在、電腦做過「備份到帳號」時:Bot、聊天、排程和金鑰一起搬過來"
|
||||
XAI=""; ANT=""; OAI=""; GEM=""; RESTORE=""
|
||||
if [ "$KEYS" = 1 ]; then
|
||||
header; card_open "AI 模型的金鑰(沒有的直接按 Enter 跳過)"; line ""
|
||||
ask XAI "xAI" "" secret; ask ANT "Anthropic" "" secret; ask OAI "OpenAI" "" secret; ask GEM "Gemini" "" secret
|
||||
card_close
|
||||
elif [ "$KEYS" = 2 ]; then RESTORE=1; fi
|
||||
|
||||
# ---------- confirm ----------
|
||||
AT=4
|
||||
header; card_open "確認一下"
|
||||
line "${D}網址 ${N}${B}https://$DOMAIN${N}"
|
||||
line "${D}帳號 ${N}${B}$USERNAME${N}"
|
||||
line "${D}大腦 ${N}${B}$BRAIN${N}"
|
||||
line "${D}註冊 ${N}${B}$([ -n "$INVITE" ] && echo "邀請碼 $INVITE" || echo "只有我")${N}"
|
||||
line "${D}金鑰 ${N}${B}$(case $KEYS in 0) echo "之後在 App 裡設定" ;; 1) n=0; for k in "$XAI" "$ANT" "$OAI" "$GEM"; do [ -n "$k" ] && n=$((n + 1)); done; echo "$n 個" ;; 2) echo "從電腦的備份接手" ;; esac)${N}"
|
||||
line ""
|
||||
note "設定會寫進 $(pwd)/.env(只有你看得到)和 Caddyfile。"
|
||||
card_close "Enter 開始 Ctrl+C 取消"
|
||||
IFS= read -r _
|
||||
|
||||
q() { printf "%s='%s'\n" "$1" "$2"; }
|
||||
umask 077
|
||||
{
|
||||
echo "# Written by setup.sh. Run ./setup.sh again to change."
|
||||
q ANYBOT_USER "$USERNAME"
|
||||
q ANYBOT_PASSWORD "$PW1"
|
||||
q ANYBOT_HOST_NAME "$BRAIN"
|
||||
q ANYBOT_RELAY_INVITE "$INVITE"
|
||||
q ANYBOT_RELAY_PUBLIC_URL "https://$DOMAIN"
|
||||
q ANYBOT_RESTORE_FROM_ACCOUNT "$RESTORE"
|
||||
q XAI_API_KEY "$XAI"
|
||||
q ANTHROPIC_API_KEY "$ANT"
|
||||
q OPENAI_API_KEY "$OAI"
|
||||
q GEMINI_API_KEY "$GEM"
|
||||
} >.env
|
||||
chmod 600 .env
|
||||
umask 022
|
||||
sed "s/^anybot.example.com {/$DOMAIN {/" ../../relay/Caddyfile >Caddyfile
|
||||
fi
|
||||
|
||||
# ---------- 4. start ----------
|
||||
AT=4; header; card_open "啟動"
|
||||
ok "寫好設定(.env、Caddyfile)"
|
||||
if [ $DRY = 1 ]; then
|
||||
warn "試跑(--dry-run):沒有啟動。要啟動:docker compose up -d --build"
|
||||
card_close; exit 0
|
||||
fi
|
||||
if [ "${SKIP:-0}" = 1 ] && [ -d ../../../.git ] && command -v git >/dev/null; then
|
||||
spin "拉下最新版" git -C ../../.. pull --ff-only || true
|
||||
fi
|
||||
spin "建置並啟動(第一次約 5–15 分鐘)" docker compose up -d --build || die
|
||||
|
||||
# The brain joins the account (and makes it, the first time).
|
||||
f=0; c=("⠋" "⠙" "⠹" "⠸" "⠼" "⠴" "⠦" "⠧" "⠇" "⠏"); joined=""
|
||||
for _ in $(seq 1 600); do
|
||||
logs=$(docker compose logs --no-color anybot 2>/dev/null | tail -n 40)
|
||||
if printf '%s' "$logs" | grep -q "透過帳號"; then joined=1; break; fi
|
||||
printf '\r\e[K %s│%s %s%s%s 大腦正在登入帳號…' "$D" "$N" "$A" "${c[$((f % 10))]}" "$N"; f=$((f + 1)); sleep 0.3
|
||||
done
|
||||
printf '\r\e[K'
|
||||
if [ -n "$joined" ]; then
|
||||
printf '%s' "$logs" | grep -q "已建立帳號" && ok "建立帳號 ${USERNAME}" || ok "大腦「${BRAIN}」已登入帳號"
|
||||
else
|
||||
bad "大腦沒有登入帳號。最近的紀錄:"
|
||||
printf '%s\n' "$logs" | grep "AnyBot:" | tail -n 4 | while IFS= read -r l; do note " ${l#*| }"; done
|
||||
fi
|
||||
|
||||
# HTTPS from outside (the certificate can take a minute).
|
||||
https=""
|
||||
for _ in $(seq 1 120); do
|
||||
if curl -fsS --max-time 5 "https://$DOMAIN/api/hello" 2>/dev/null | grep -q anybot-relay; then https=1; break; fi
|
||||
printf '\r\e[K %s│%s %s%s%s 等 HTTPS 憑證…' "$D" "$N" "$A" "${c[$((f % 10))]}" "$N"; f=$((f + 1)); sleep 1
|
||||
done
|
||||
printf '\r\e[K'
|
||||
if [ -n "$https" ]; then ok "https://$DOMAIN 可以連"; else warn "https://$DOMAIN 還連不到(DNS 還沒生效,或 80/443 埠被防火牆擋住)。看紀錄:docker compose logs caddy"; fi
|
||||
card_close
|
||||
|
||||
printf '\n'
|
||||
card_open "$([ -n "$joined" ] && echo "完成了" || echo "差一點")"
|
||||
line "${B}手機${N} 打開 ${A}https://$DOMAIN${N},用 ${B}$USERNAME${N} 登入,可以「加入主畫面」"
|
||||
line "${B}電腦${N} AnyBot › 設定 › 手機與帳號 登入,在「大腦」選「$BRAIN」"
|
||||
line "${B}Linux 手腳${N} anybot setup(選「手腳」,再選「$BRAIN」)"
|
||||
[ -n "${INVITE:-}" ] && line "${B}邀請碼${N} $INVITE(給要註冊的人)"
|
||||
line ""
|
||||
note "看紀錄 docker compose logs -f anybot"
|
||||
note "更新 ./setup.sh(選「用現有設定」)"
|
||||
card_close
|
||||
printf '\n'
|
||||
|
|
@ -282,12 +282,13 @@ impl Relay {
|
|||
|
||||
/// Hand a request to a host and wait for its answer.
|
||||
fn forward(&self, host_id: &str, method: &str, path: &str, query: &HashMap<String, String>, body: &[u8], who: (&str, &str)) -> Resp {
|
||||
self.forward_raw(host_id, method, path, query, body, false, who, false).into_resp()
|
||||
self.forward_raw(host_id, method, path, query, body, false, who, false, "").into_resp()
|
||||
}
|
||||
|
||||
/// `z`: the body is compressed; `z_ok`: the asker reads a compressed answer.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn forward_raw(&self, host_id: &str, method: &str, path: &str, query: &HashMap<String, String>, body: &[u8], z: bool, who: (&str, &str), z_ok: bool) -> Answer {
|
||||
/// `from`: the AnyBot of the account asking (empty for the phone).
|
||||
fn forward_raw(&self, host_id: &str, method: &str, path: &str, query: &HashMap<String, String>, body: &[u8], z: bool, who: (&str, &str), z_ok: bool, from: &str) -> Answer {
|
||||
use base64::Engine;
|
||||
if !self.online(host_id) {
|
||||
return Answer::error(503, OFFLINE);
|
||||
|
|
@ -295,7 +296,7 @@ impl Relay {
|
|||
let id = httpd::random_hex(12);
|
||||
let (tx, rx) = channel();
|
||||
self.waiting.lock().unwrap().insert(id.clone(), tx);
|
||||
let head = json!({"t": "job", "id": id, "method": method, "path": path, "query": query, "user": who.0, "role": who.1, "z": z, "z_ok": z_ok});
|
||||
let head = json!({"t": "job", "id": id, "method": method, "path": path, "query": query, "user": who.0, "role": who.1, "z": z, "z_ok": z_ok, "from": from});
|
||||
// Connected the lasting way: straight down that connection.
|
||||
let sock = self.sockets.lock().unwrap().get(host_id).map(|s| (s.tx.clone(), s.serial));
|
||||
if let Some((stx, serial)) = sock {
|
||||
|
|
@ -443,9 +444,9 @@ impl Relay {
|
|||
return;
|
||||
}
|
||||
let query: HashMap<String, String> = head["query"].as_object().map(|o| o.iter().map(|(k, v)| (k.clone(), v.as_str().unwrap_or("").to_string())).collect()).unwrap_or_default();
|
||||
let (me, tx, user, method, z) = (self.clone(), tx.clone(), h.user.clone(), s("method"), head["z"].as_bool().unwrap_or(false));
|
||||
let (me, tx, user, method, z, from) = (self.clone(), tx.clone(), h.user.clone(), s("method"), head["z"].as_bool().unwrap_or(false), h.id.clone());
|
||||
std::thread::spawn(move || {
|
||||
let a = me.forward_raw(&to, &method, &path, &query, &body, z, (&user, role), true);
|
||||
let a = me.forward_raw(&to, &method, &path, &query, &body, z, (&user, role), true, &from);
|
||||
let _ = tx.send(wire::pack(&json!({"t": "ret", "id": id, "status": a.status, "ctype": a.ctype, "z": a.z}), &a.body));
|
||||
});
|
||||
}
|
||||
|
|
@ -706,7 +707,7 @@ impl Relay {
|
|||
if !same || *target == me.id {
|
||||
return Resp::error(403, "不是同一個帳號的電腦");
|
||||
}
|
||||
self.forward(target, "POST", "/api/device/run", &HashMap::new(), &req.body, (&me.user, "device"))
|
||||
self.forward_raw(target, "POST", "/api/device/run", &HashMap::new(), &req.body, false, (&me.user, "device"), false, &me.id).into_resp()
|
||||
}
|
||||
// 大腦: one AnyBot of the account shows (and edits) another one's bots and chats.
|
||||
(m, ["relay", "hosts", target, "api", "sync", rest @ ..]) => {
|
||||
|
|
@ -716,7 +717,7 @@ impl Relay {
|
|||
return Resp::error(403, "不是同一個帳號的電腦");
|
||||
}
|
||||
let path = if rest.is_empty() { "/api/sync".to_string() } else { format!("/api/sync/{}", rest.join("/")) };
|
||||
self.forward(target, m, &path, &req.query, &req.body, (&me.user, "mirror"))
|
||||
self.forward_raw(target, m, &path, &req.query, &req.body, false, (&me.user, "mirror"), false, &me.id).into_resp()
|
||||
}
|
||||
// The phone, through to one of the account's AnyBots.
|
||||
(m, ["h", host, rest @ ..]) => {
|
||||
|
|
|
|||
11
src/app.rs
11
src/app.rs
|
|
@ -3061,7 +3061,16 @@ impl App {
|
|||
return;
|
||||
}
|
||||
let Some((url, user)) = acct.rsplit_once('|') else { return };
|
||||
match crate::remote::relay_join(&self.state.remote, url, user, &pw, false, "") {
|
||||
// ANYBOT_ACCOUNT_CREATE=1 (the server's own brain, first start): make the account if it isn't there.
|
||||
let mut r = crate::remote::relay_join(&self.state.remote, url, user, &pw, false, "");
|
||||
if r.is_err() && std::env::var("ANYBOT_ACCOUNT_CREATE").map(|v| v == "1").unwrap_or(false) {
|
||||
let invite = std::env::var("ANYBOT_RELAY_INVITE").unwrap_or_default();
|
||||
if let Ok(s) = crate::remote::relay_join(&self.state.remote, url, user, &pw, true, &invite) {
|
||||
eprintln!("AnyBot:已建立帳號 {user}");
|
||||
r = Ok(s);
|
||||
}
|
||||
}
|
||||
match r {
|
||||
Ok(s) => {
|
||||
self.state.remote = s;
|
||||
self.dirty = true;
|
||||
|
|
|
|||
|
|
@ -741,6 +741,8 @@ impl App {
|
|||
let mut remote = self.state.remote.clone();
|
||||
remote.brain_id.clear();
|
||||
remote.brain_name.clear();
|
||||
remote.hands_for.clear();
|
||||
remote.hands_for_name.clear();
|
||||
local.remote = remote;
|
||||
local.theme = self.state.theme;
|
||||
local.accent = self.state.accent;
|
||||
|
|
@ -751,6 +753,8 @@ impl App {
|
|||
} else {
|
||||
self.state.remote.brain_id.clear();
|
||||
self.state.remote.brain_name.clear();
|
||||
self.state.remote.hands_for.clear();
|
||||
self.state.remote.hands_for_name.clear();
|
||||
}
|
||||
self.state.save();
|
||||
self.dirty = false;
|
||||
|
|
@ -825,6 +829,9 @@ impl App {
|
|||
if self.mir.on {
|
||||
self.mirror_leave();
|
||||
}
|
||||
// This computer's hands are for that brain only.
|
||||
self.state.remote.hands_for = id.clone();
|
||||
self.state.remote.hands_for_name = name.clone();
|
||||
self.state.remote.brain_id = id;
|
||||
self.state.remote.brain_name = name;
|
||||
self.state.save();
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ mod preview;
|
|||
mod providers;
|
||||
mod remote;
|
||||
mod routines;
|
||||
mod setup;
|
||||
mod teach;
|
||||
mod share;
|
||||
mod teams;
|
||||
|
|
@ -218,6 +219,10 @@ fn main() -> eframe::Result<()> {
|
|||
}
|
||||
std::process::exit(0);
|
||||
}
|
||||
// `anybot setup`: make this computer a brain, or hands for a brain (in a terminal).
|
||||
if args.get(1).map(String::as_str) == Some("setup") {
|
||||
std::process::exit(setup::run(&args[2..]));
|
||||
}
|
||||
if args.get(1).map(String::as_str) == Some("--ask") {
|
||||
let text = args[2..].join(" ");
|
||||
let ctx = egui::Context::default();
|
||||
|
|
|
|||
|
|
@ -78,11 +78,16 @@ pub struct Settings {
|
|||
pub brain_id: String,
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub brain_name: String,
|
||||
/// 手腳: only this brain (host id) may use this computer (empty = any AnyBot of the account).
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub hands_for: String,
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub hands_for_name: String,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Settings { lan: false, password: String::new(), devices: vec![], mac_from_phone: false, device_link: false, relay_url: String::new(), relay_user: String::new(), host_id: String::new(), host_token: String::new(), host_name: default_host_name(), backup_key: String::new(), auto_backup: true, last_backup: 0, brain_id: String::new(), brain_name: String::new() }
|
||||
Settings { lan: false, password: String::new(), devices: vec![], mac_from_phone: false, device_link: false, relay_url: String::new(), relay_user: String::new(), host_id: String::new(), host_token: String::new(), host_name: default_host_name(), backup_key: String::new(), auto_backup: true, last_backup: 0, brain_id: String::new(), brain_name: String::new(), hands_for: String::new(), hands_for_name: String::new() }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -370,7 +375,7 @@ pub fn handle(req: Req, via_relay: bool) -> Resp {
|
|||
}
|
||||
("GET", ["api", "screen", which]) => screen(which, &s),
|
||||
("POST", ["api", "screen", which]) => screen_input(which, &req.json(), &s),
|
||||
("POST", ["api", "device", "run"]) => device_run(&req.json(), &s, via_relay),
|
||||
("POST", ["api", "device", "run"]) => device_run(&req.json(), &s, via_relay, req.header("x-anybot-from")),
|
||||
("GET", ["api", "sync"]) => {
|
||||
let since: u64 = req.query.get("since").and_then(|v| v.parse().ok()).unwrap_or(0);
|
||||
let client = req.query.get("client").cloned().unwrap_or_default();
|
||||
|
|
@ -573,10 +578,13 @@ fn screen_input(which: &str, v: &Value, s: &Settings) -> Resp {
|
|||
|
||||
/// 裝置連結: another AnyBot in this account (e.g. the one on the server) runs a screen tool on
|
||||
/// this Mac. Only through the relay, and only when this Mac allows it.
|
||||
fn device_run(v: &Value, s: &Settings, via_relay: bool) -> Resp {
|
||||
fn device_run(v: &Value, s: &Settings, via_relay: bool, from: &str) -> Resp {
|
||||
if !via_relay || !s.device_link {
|
||||
return Resp::error(403, "這台電腦沒有開放給帳號裡的其他 AnyBot 操作");
|
||||
}
|
||||
if !s.hands_for.is_empty() && from != s.hands_for {
|
||||
return Resp::error(403, &format!("這台電腦只讓大腦「{}」使用", s.hands_for_name));
|
||||
}
|
||||
let tool = v["tool"].as_str().unwrap_or("");
|
||||
let args = v["args"].as_str().unwrap_or("{}");
|
||||
let who = v["who"].as_str().unwrap_or("遠端 AnyBot");
|
||||
|
|
@ -843,7 +851,7 @@ pub fn req_from_json(job: &Value) -> Req {
|
|||
path: job["path"].as_str().unwrap_or("/").to_string(),
|
||||
query: job["query"].as_object().map(|o| o.iter().map(|(k, v)| (k.clone(), v.as_str().unwrap_or("").to_string())).collect()).unwrap_or_default(),
|
||||
// Who asked, as the relay vouches for it (團隊: owner / admin / member; "device" for 裝置連結).
|
||||
headers: [("x-anybot-user", job["user"].as_str().unwrap_or("")), ("x-anybot-role", job["role"].as_str().unwrap_or("owner"))].iter().map(|(k, v)| (k.to_string(), v.to_string())).collect(),
|
||||
headers: [("x-anybot-user", job["user"].as_str().unwrap_or("")), ("x-anybot-role", job["role"].as_str().unwrap_or("owner")), ("x-anybot-from", job["from"].as_str().unwrap_or(""))].iter().map(|(k, v)| (k.to_string(), v.to_string())).collect(),
|
||||
body: {
|
||||
let b = base64::engine::general_purpose::STANDARD.decode(job["body"].as_str().unwrap_or("")).unwrap_or_default();
|
||||
if job["z"].as_bool().unwrap_or(false) {
|
||||
|
|
@ -1082,6 +1090,36 @@ pub fn set_member(s: &Settings, user: &str, role: &str) -> Result<(), String> {
|
|||
}
|
||||
|
||||
/// The other AnyBots signed in to this account (for 裝置連結).
|
||||
/// The account's AnyBots as the account server sees them (for choosing a brain).
|
||||
pub fn peers(s: &Settings) -> Result<Vec<Value>, String> {
|
||||
let mut r = agent(15).get(&format!("{}/relay/host/peers", s.relay_url)).header("Authorization", &format!("Bearer {}", s.host_token)).call().map_err(|e| format!("連不到帳號伺服器:{}", why_unreachable(&e)))?;
|
||||
let st = r.status().as_u16();
|
||||
let v: Value = serde_json::from_str(&r.body_mut().read_to_string().unwrap_or_default()).unwrap_or(Value::Null);
|
||||
if !(200..300).contains(&st) {
|
||||
return Err(why_refused(st, &v, "帳號伺服器"));
|
||||
}
|
||||
Ok(v["hosts"].as_array().cloned().unwrap_or_default())
|
||||
}
|
||||
|
||||
/// Is this an AnyBot account server? (Gives the address to use.)
|
||||
pub fn check_server(url: &str) -> Result<String, String> {
|
||||
let mut u = url.trim().trim_end_matches('/').to_string();
|
||||
if u.is_empty() {
|
||||
return Err("請輸入帳號伺服器的網址".into());
|
||||
}
|
||||
if !u.contains("://") {
|
||||
let host = u.split(['/', ':']).next().unwrap_or("");
|
||||
let plain = host == "localhost" || host.parse::<std::net::IpAddr>().is_ok() || !host.contains('.');
|
||||
u = format!("{}{u}", if plain { "http://" } else { "https://" });
|
||||
}
|
||||
let mut r = agent(15).get(&format!("{u}/api/hello")).call().map_err(|e| format!("連不到 {u}:{}", why_unreachable(&e)))?;
|
||||
let v: Value = serde_json::from_str(&r.body_mut().read_to_string().unwrap_or_default()).unwrap_or(Value::Null);
|
||||
if v["app"] != "anybot-relay" {
|
||||
return Err(format!("{u} 有回應,但不是 AnyBot 帳號伺服器(網址打錯了?)"));
|
||||
}
|
||||
Ok(u)
|
||||
}
|
||||
|
||||
pub fn devices_online(s: &Settings) -> Vec<(String, String, bool)> {
|
||||
if s.relay_url.is_empty() {
|
||||
return vec![];
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -289,7 +289,7 @@ fn on_frame(head: Value, body: Vec<u8>, tx: &Sender<Vec<u8>>, calls: &Calls) {
|
|||
method: s("method"),
|
||||
path: s("path"),
|
||||
query: head["query"].as_object().map(|o| o.iter().map(|(k, v)| (k.clone(), v.as_str().unwrap_or("").to_string())).collect()).unwrap_or_default(),
|
||||
headers: [("x-anybot-user", s("user")), ("x-anybot-role", if head["role"].is_string() { s("role") } else { "owner".into() })].into_iter().map(|(k, v)| (k.to_string(), v)).collect(),
|
||||
headers: [("x-anybot-user", s("user")), ("x-anybot-role", if head["role"].is_string() { s("role") } else { "owner".into() }), ("x-anybot-from", s("from"))].into_iter().map(|(k, v)| (k.to_string(), v)).collect(),
|
||||
body,
|
||||
peer: "relay".into(),
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue