diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..496d65a --- /dev/null +++ b/.gitea/workflows/release.yml @@ -0,0 +1,45 @@ +# Push a version tag (git tag v1.2.0 && git push origin v1.2.0) and every build lands on that +# tag's release: macOS (the app), Ubuntu (with a window), Linux without a screen, and the server. +# +# Runners (see deploy/ci/README.md): +# anybot-linux a Linux computer with Docker; jobs run in anybot-builder:22.04 +# macos-arm64 a Mac (runs directly on it) +name: Release +on: + push: + tags: ['v*'] + +env: + RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN || secrets.GITHUB_TOKEN }} + +jobs: + linux: + runs-on: anybot-linux + env: + # Kept between releases (a Docker volume on the runner): only what changed is compiled. + CARGO_TARGET_DIR: /ci-target + steps: + - uses: actions/checkout@v4 + - name: Build + run: | + cargo build --release --bin anybot + cd relay && cargo build --release + - name: Test + run: cargo test --release --bin anybot -- --skip live 2>&1 | tail -3 + - name: Package + run: sh deploy/ci/package-linux.sh "${GITHUB_REF_NAME#v}" + - name: Upload + run: sh deploy/ci/upload-release.sh "$GITHUB_REF_NAME" dist/*.tar.gz + + macos: + runs-on: macos-arm64 + env: + CARGO_TARGET_DIR: ${{ github.workspace }}/../anybot-target + steps: + - uses: actions/checkout@v4 + - name: Build + run: cargo build --release --bin anybot + - name: Package + run: sh deploy/mac/bundle.sh "${GITHUB_REF_NAME#v}" + - name: Upload + run: sh deploy/ci/upload-release.sh "$GITHUB_REF_NAME" dist/*.zip diff --git a/.gitignore b/.gitignore index 0dc47b9..a9fc12b 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,6 @@ # macOS .DS_Store + +# Release packages (deploy/ci, deploy/mac/bundle.sh) +/dist/ diff --git a/deploy/ci/Dockerfile.linux b/deploy/ci/Dockerfile.linux new file mode 100644 index 0000000..b76f4d4 --- /dev/null +++ b/deploy/ci/Dockerfile.linux @@ -0,0 +1,12 @@ +# The Linux build environment for the release workflow (.gitea/workflows/release.yml). +# Ubuntu 22.04 (glibc 2.35), so the binaries also run on newer Ubuntu, Debian 12 and the like. +# Built once on the runner's computer: docker build -t anybot-builder:22.04 -f deploy/ci/Dockerfile.linux deploy/ci +FROM ubuntu:22.04 +ENV DEBIAN_FRONTEND=noninteractive +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates curl git build-essential pkg-config cmake clang libclang-dev python3 nodejs xz-utils zip \ + libasound2-dev libxkbcommon-dev libwayland-dev libx11-dev libxtst-dev libdbus-1-dev \ + libxcb1-dev libxcb-randr0-dev libxcb-shm0-dev libxdo-dev libpipewire-0.3-dev libgbm-dev \ + && rm -rf /var/lib/apt/lists/* +ENV RUSTUP_HOME=/usr/local/rustup CARGO_HOME=/usr/local/cargo PATH=/usr/local/cargo/bin:$PATH +RUN curl -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.98.1 && chmod -R a+w $RUSTUP_HOME $CARGO_HOME diff --git a/deploy/ci/Dockerfile.server-anybot b/deploy/ci/Dockerfile.server-anybot new file mode 100644 index 0000000..5e1d6c9 --- /dev/null +++ b/deploy/ci/Dockerfile.server-anybot @@ -0,0 +1,8 @@ +# AnyBot (the brain, no window) from the release's binary. +FROM debian:bookworm-slim +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl git ripgrep poppler-utils docker.io openssh-client libasound2 \ + && rm -rf /var/lib/apt/lists/* +COPY bin/anybot /usr/local/bin/anybot +ENV ANYBOT_DATA_DIR=/data +VOLUME /data +CMD ["anybot", "--serve"] diff --git a/deploy/ci/Dockerfile.server-relay b/deploy/ci/Dockerfile.server-relay new file mode 100644 index 0000000..34d1bab --- /dev/null +++ b/deploy/ci/Dockerfile.server-relay @@ -0,0 +1,8 @@ +# The account server from the release's binary (no compiling on the server). +FROM debian:bookworm-slim +RUN useradd -m relay && mkdir /data && chown relay /data +COPY bin/anybot-relay /usr/local/bin/anybot-relay +USER relay +VOLUME /data +EXPOSE 8080 +CMD ["anybot-relay", "--port", "8080", "--data", "/data"] diff --git a/deploy/ci/linux-install.sh b/deploy/ci/linux-install.sh new file mode 100755 index 0000000..2676935 --- /dev/null +++ b/deploy/ci/linux-install.sh @@ -0,0 +1,23 @@ +#!/bin/sh +# Install AnyBot from this package for the current user (no root needed). +# ./install.sh the app (menu entry and icon too when this computer has a desktop) +# Then: open AnyBot from the menu, or on a computer without a screen: anybot setup +set -e +cd "$(dirname "$0")" +BIN="$HOME/.local/bin" +mkdir -p "$BIN" +install -m 755 anybot "$BIN/anybot" +if [ -f anybot.desktop ]; then + APPS="$HOME/.local/share/applications"; ICONS="$HOME/.local/share/icons/hicolor" + mkdir -p "$APPS" "$ICONS/scalable/apps" "$ICONS/256x256/apps" + install -m 644 anybot.desktop "$APPS/anybot.desktop" + install -m 644 anybot.svg "$ICONS/scalable/apps/anybot.svg" + "$BIN/anybot" --write-icon "$ICONS/256x256/apps/anybot.png" 256 2>/dev/null || true + command -v update-desktop-database >/dev/null && update-desktop-database "$APPS" 2>/dev/null || true +fi +echo "AnyBot 已安裝在 $BIN/anybot" +case ":$PATH:" in *":$BIN:"*) ;; *) echo "(把 $BIN 加到 PATH,才能直接打 anybot)" ;; esac +if ! ldconfig -p 2>/dev/null | grep -q libasound.so.2; then + echo "還需要 libasound2:sudo apt-get install -y libasound2(或 libasound2t64)" +fi +echo "接到你的大腦:在手機或電腦的 AnyBot 按「加一台電腦」,把那一行貼到這裡執行。" diff --git a/deploy/ci/package-linux.sh b/deploy/ci/package-linux.sh new file mode 100755 index 0000000..6288fe1 --- /dev/null +++ b/deploy/ci/package-linux.sh @@ -0,0 +1,44 @@ +#!/bin/sh +# Package the Linux builds for a release (run from app/ after the release builds): +# dist/AnyBot--ubuntu-x86_64.tar.gz the app with its window (menu entry, icon) +# dist/AnyBot--headless-x86_64.tar.gz for a computer without a screen (hands, or a brain) +# dist/AnyBot--server-x86_64.tar.gz account server + brain on one server (Docker) +set -e +VERSION=${1:?version} +ARCH=$(uname -m) +OUT=dist +rm -rf "$OUT" && mkdir -p "$OUT" +BIN=${CARGO_TARGET_DIR:-target}/release/anybot +RELAY=${CARGO_TARGET_DIR:-relay/target}/release/anybot-relay +strip "$BIN" "$RELAY" 2>/dev/null || true + +# Ubuntu (with a window) +D="$OUT/AnyBot-$VERSION-ubuntu-$ARCH" +mkdir -p "$D" +cp "$BIN" deploy/linux/anybot.desktop icons/anybot.svg "$D/" +cp deploy/ci/linux-install.sh "$D/install.sh" +printf 'AnyBot %s(Ubuntu 22.04 以後、Debian 12 以後)\n\n ./install.sh 安裝,之後從選單打開 AnyBot\n' "$VERSION" > "$D/讀我.txt" +tar -C "$OUT" -czf "$D.tar.gz" "$(basename "$D")" + +# Without a screen (hands, or a brain) +D="$OUT/AnyBot-$VERSION-headless-$ARCH" +mkdir -p "$D" +cp "$BIN" "$D/" +cp deploy/ci/linux-install.sh "$D/install.sh" +cp deploy/linux/hands.sh "$D/hands.sh" +printf 'AnyBot %s:沒有螢幕的 Linux\n\n ./install.sh\n 然後在手機或電腦的 AnyBot 按「加一台電腦」,把那一行貼到這台執行。\n 想讓 Bot 看這台的畫面(虛擬螢幕):先執行 ./hands.sh --deps\n' "$VERSION" > "$D/讀我.txt" +tar -C "$OUT" -czf "$D.tar.gz" "$(basename "$D")" + +# Server: account server + brain, from these binaries (no compiling on the server) +D="$OUT/AnyBot-$VERSION-server-$ARCH" +mkdir -p "$D/bin" "$D/deploy/server" "$D/relay" +cp "$BIN" "$RELAY" "$D/bin/" +cp deploy/server/setup.sh deploy/server/.env.example "$D/deploy/server/" +cp deploy/ci/server-compose.yml "$D/deploy/server/docker-compose.yml" +cp deploy/ci/Dockerfile.server-relay deploy/ci/Dockerfile.server-anybot "$D/deploy/server/" +cp relay/Caddyfile "$D/relay/Caddyfile" +printf 'AnyBot %s 伺服器(帳號伺服器 + 大腦 + HTTPS,用 Docker)\n\n cd deploy/server && ./setup.sh\n' "$VERSION" > "$D/讀我.txt" +tar -C "$OUT" -czf "$D.tar.gz" "$(basename "$D")" + +find "$OUT" -maxdepth 1 -type d -name 'AnyBot-*' -exec rm -rf {} + +ls -la "$OUT" diff --git a/deploy/ci/runner-linux.yaml b/deploy/ci/runner-linux.yaml new file mode 100644 index 0000000..9d06a2b --- /dev/null +++ b/deploy/ci/runner-linux.yaml @@ -0,0 +1,19 @@ +# gitea-runner settings for the Linux build computer (~/.config/gitea-runner/config.yaml). +log: + level: info +runner: + file: /home/daniel/.config/gitea-runner/.runner + capacity: 1 + timeout: 3h + labels: + - "anybot-linux:docker://anybot-builder:22.04" +cache: + enabled: true +container: + # anybot-builder is built on this computer, not pulled. + force_pull: false + # Downloads and compiled pieces kept between releases (much faster second builds). + valid_volumes: + - anybot-cargo-registry + - anybot-ci-target + options: "-v anybot-cargo-registry:/usr/local/cargo/registry -v anybot-ci-target:/ci-target" diff --git a/deploy/ci/server-compose.yml b/deploy/ci/server-compose.yml new file mode 100644 index 0000000..c219f4c --- /dev/null +++ b/deploy/ci/server-compose.yml @@ -0,0 +1,63 @@ +# The server from a release package (prebuilt binaries in ../../bin). Made from deploy/server/docker-compose.yml. +# Everything on one small server: the account server (relay), HTTPS (Caddy), and AnyBot itself. +# 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. +# AI keys: set them in .env, or set ANYBOT_RESTORE_FROM_ACCOUNT=1 to start from the newest backup +# your Mac made (bots, chats, routines and the keys you set there). +# +# 大腦: this AnyBot is the brain of the account. It keeps the bots, chats, memory and routines, +# 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: run `anybot setup` there and pick this brain. +services: + relay: + build: + context: ../.. + dockerfile: deploy/server/Dockerfile.server-relay + restart: unless-stopped + environment: + # Your public address (for the links of sites the bots publish), e.g. https://anybot.example.com + ANYBOT_RELAY_PUBLIC_URL: "${ANYBOT_RELAY_PUBLIC_URL:-}" + ANYBOT_RELAY_INVITE: "${ANYBOT_RELAY_INVITE:-}" + volumes: + - relay-data:/data + caddy: + image: caddy:2 + restart: unless-stopped + ports: ["80:80", "443:443"] + volumes: + # Written by setup.sh (your domain); ../../relay/Caddyfile is the template. + - ./Caddyfile:/etc/caddy/Caddyfile:ro + - caddy-data:/data + depends_on: [relay] + anybot: + build: + context: ../.. + dockerfile: deploy/server/Dockerfile.server-anybot + restart: unless-stopped + depends_on: [relay] + environment: + 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:-}" + GEMINI_API_KEY: "${GEMINI_API_KEY:-}" + ANYBOT_RESTORE_FROM_ACCOUNT: "${ANYBOT_RESTORE_FROM_ACCOUNT:-}" + volumes: + - anybot-data:/data + # The bot's own computer runs as a container next to this one. + - /var/run/docker.sock:/var/run/docker.sock +volumes: + relay-data: + caddy-data: + anybot-data: diff --git a/deploy/ci/upload-release.sh b/deploy/ci/upload-release.sh new file mode 100755 index 0000000..4a69bcb --- /dev/null +++ b/deploy/ci/upload-release.sh @@ -0,0 +1,25 @@ +#!/bin/sh +# Attach files to this tag's release on Gitea (made if it isn't there yet). +# deploy/ci/upload-release.sh … +# Uses GITHUB_SERVER_URL, GITHUB_REPOSITORY and RELEASE_TOKEN (set by the workflow). +set -e +TAG=${1:?tag}; shift +API="$GITHUB_SERVER_URL/api/v1/repos/$GITHUB_REPOSITORY" +AUTH="Authorization: token $RELEASE_TOKEN" +json() { python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('$1','') if isinstance(d,dict) else '')"; } +ID=$(curl -fsS -H "$AUTH" "$API/releases/tags/$TAG" 2>/dev/null | json id || true) +if [ -z "$ID" ]; then + # Two jobs may get here at once: whoever is second finds the first one's release. + ID=$(curl -fsS -H "$AUTH" -H "Content-Type: application/json" -X POST "$API/releases" \ + -d "{\"tag_name\":\"$TAG\",\"name\":\"AnyBot $TAG\",\"body\":\"自動建置:macOS、Ubuntu、沒有螢幕的 Linux、伺服器。\"}" | json id) \ + || ID=$(curl -fsS -H "$AUTH" "$API/releases/tags/$TAG" | json id) +fi +[ -n "$ID" ] || { echo "找不到也建不了 $TAG 的 release" >&2; exit 1; } +for f in "$@"; do + name=$(basename "$f") + # Built again: replace the old file. + old=$(curl -fsS -H "$AUTH" "$API/releases/$ID/assets" | python3 -c "import json,sys; print(next((str(a['id']) for a in json.load(sys.stdin) if a['name']=='$name'),''))") + [ -n "$old" ] && curl -fsS -H "$AUTH" -X DELETE "$API/releases/$ID/assets/$old" >/dev/null + curl -fsS -H "$AUTH" -F "attachment=@$f" "$API/releases/$ID/assets?name=$name" >/dev/null + echo "已上傳 $name" +done diff --git a/deploy/mac/bundle.sh b/deploy/mac/bundle.sh new file mode 100755 index 0000000..62d90ee --- /dev/null +++ b/deploy/mac/bundle.sh @@ -0,0 +1,44 @@ +#!/bin/sh +# Make AnyBot.app from the release build and zip it (run from app/ on a Mac, after +# `cargo build --release`): deploy/mac/bundle.sh 1.2.0 → dist/AnyBot-1.2.0-macos-.zip +# Signed ad hoc (no Apple developer account): the first time, right-click › 打開. +set -e +VERSION=${1:?version} +BIN=${CARGO_TARGET_DIR:-target}/release/anybot +ARCH=$(uname -m) +OUT=dist +APP="$OUT/AnyBot.app" +rm -rf "$APP" && mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources" +cp "$BIN" "$APP/Contents/MacOS/AnyBot" + +# The icon: every size the system asks for. +SET=$(mktemp -d)/AnyBot.iconset +mkdir -p "$SET" +for s in 16 32 128 256 512; do + "$BIN" --write-icon "$SET/icon_${s}x${s}.png" $s + "$BIN" --write-icon "$SET/icon_${s}x${s}@2x.png" $((s * 2)) +done +iconutil -c icns "$SET" -o "$APP/Contents/Resources/AnyBot.icns" + +cat > "$APP/Contents/Info.plist" < + + + CFBundleNameAnyBot + CFBundleDisplayNameAnyBot + CFBundleIdentifiernet.30cm.anybot + CFBundleExecutableAnyBot + CFBundleIconFileAnyBot + CFBundlePackageTypeAPPL + CFBundleShortVersionString$VERSION + CFBundleVersion$VERSION + LSMinimumSystemVersion12.0 + NSHighResolutionCapable + NSMicrophoneUsageDescription語音聊天時,聽你說話。 + NSAppleEventsUsageDescription讓 Bot 在你允許時操作其他 App。 + +PLIST +codesign --force --deep -s - "$APP" +ditto -c -k --norsrc --keepParent "$APP" "$OUT/AnyBot-$VERSION-macos-$ARCH.zip" +rm -rf "$APP" +ls -la "$OUT" diff --git a/relay/Dockerfile b/relay/Dockerfile index e3eeccf..cb48673 100644 --- a/relay/Dockerfile +++ b/relay/Dockerfile @@ -3,6 +3,7 @@ FROM rust:1-bookworm AS build WORKDIR /src COPY src/httpd.rs src/httpd.rs +COPY src/wire.rs src/wire.rs COPY mobile mobile COPY relay relay RUN cd relay && cargo build --release