2026-03-12 12:56:59 +00:00
|
|
|
#!/bin/bash
|
|
|
|
|
#
|
|
|
|
|
# Setup AIUI + Claude API proxy + FileBrowser on any Archipelago server
|
|
|
|
|
#
|
|
|
|
|
# Usage:
|
|
|
|
|
# ./scripts/setup-aiui-server.sh <host>
|
|
|
|
|
# ./scripts/setup-aiui-server.sh archipelago@192.168.1.198
|
|
|
|
|
# ./scripts/setup-aiui-server.sh archipelago@192.168.1.228
|
|
|
|
|
#
|
|
|
|
|
# What it does:
|
|
|
|
|
# 1. Deploys AIUI files (from local build)
|
|
|
|
|
# 2. Configures nginx Claude API proxy (direct to Anthropic with API key)
|
|
|
|
|
# 3. Fixes FileBrowser container (removes read-only root if needed)
|
|
|
|
|
# 4. Reloads nginx
|
|
|
|
|
#
|
|
|
|
|
# Prerequisites:
|
|
|
|
|
# - AIUI must be built locally first: cd AIUI/packages/app && VITE_BASE_PATH=/aiui/ npx vite build
|
|
|
|
|
# - SSH key access to target server
|
|
|
|
|
|
|
|
|
|
set -e
|
|
|
|
|
|
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
|
|
|
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
|
|
|
|
SSH_KEY="${ARCHIPELAGO_SSH_KEY:-$HOME/.ssh/archipelago-deploy}"
|
|
|
|
|
SSH_OPTS="-o StrictHostKeyChecking=no -i $SSH_KEY"
|
|
|
|
|
|
|
|
|
|
# Anthropic API key — used by all servers for AIUI Claude chat
|
|
|
|
|
ANTHROPIC_API_KEY="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"
|
|
|
|
|
|
|
|
|
|
TARGET_HOST="$1"
|
|
|
|
|
if [ -z "$TARGET_HOST" ]; then
|
|
|
|
|
echo "Usage: $0 <user@host>"
|
|
|
|
|
echo " e.g. $0 archipelago@192.168.1.198"
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
AIUI_DIST="$PROJECT_DIR/../AIUI/packages/app/dist"
|
|
|
|
|
if [ ! -f "$AIUI_DIST/index.html" ]; then
|
|
|
|
|
echo "ERROR: AIUI build not found at $AIUI_DIST"
|
|
|
|
|
echo "Build it first: cd ../AIUI/packages/app && VITE_BASE_PATH=/aiui/ npx vite build"
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
timestamp() { echo "[$(date +%H:%M:%S)]"; }
|
|
|
|
|
|
|
|
|
|
echo "╔════════════════════════════════════════════════════════════╗"
|
|
|
|
|
echo "║ Archipelago AIUI + Claude API Setup ║"
|
|
|
|
|
echo "║ Target: $TARGET_HOST"
|
|
|
|
|
echo "╚════════════════════════════════════════════════════════════╝"
|
|
|
|
|
|
|
|
|
|
# --- Step 1: Deploy AIUI files ---
|
|
|
|
|
echo ""
|
|
|
|
|
echo "$(timestamp) 📦 Deploying AIUI files..."
|
|
|
|
|
|
|
|
|
|
# Check if rsync is available on remote
|
|
|
|
|
if ssh $SSH_OPTS "$TARGET_HOST" "which rsync" &>/dev/null; then
|
|
|
|
|
rsync -avz --delete -e "ssh $SSH_OPTS" "$AIUI_DIST/" "$TARGET_HOST:/opt/archipelago/web-ui/aiui/" 2>&1 | tail -3
|
|
|
|
|
else
|
|
|
|
|
echo " rsync not available, using tar+scp..."
|
|
|
|
|
TMPTAR=$(mktemp /tmp/aiui-dist-XXXXX.tar.gz)
|
|
|
|
|
(cd "$AIUI_DIST" && tar czf "$TMPTAR" .)
|
|
|
|
|
scp $SSH_OPTS "$TMPTAR" "$TARGET_HOST:/tmp/aiui-dist.tar.gz"
|
|
|
|
|
ssh $SSH_OPTS "$TARGET_HOST" "sudo mkdir -p /opt/archipelago/web-ui/aiui && cd /opt/archipelago/web-ui/aiui && sudo tar xzf /tmp/aiui-dist.tar.gz --overwrite"
|
|
|
|
|
rm -f "$TMPTAR"
|
|
|
|
|
fi
|
|
|
|
|
echo " AIUI deployed."
|
|
|
|
|
|
|
|
|
|
# --- Step 2: Configure nginx Claude API proxy ---
|
|
|
|
|
echo ""
|
|
|
|
|
echo "$(timestamp) 🔧 Configuring nginx Claude API proxy..."
|
|
|
|
|
|
|
|
|
|
# Create a Python script to patch nginx config
|
|
|
|
|
cat << 'PYSCRIPT' > /tmp/patch-nginx-claude.py
|
|
|
|
|
import sys
|
|
|
|
|
import re
|
|
|
|
|
|
|
|
|
|
API_KEY = sys.argv[1]
|
|
|
|
|
|
|
|
|
|
with open("/etc/nginx/sites-available/archipelago") as f:
|
|
|
|
|
content = f.read()
|
|
|
|
|
|
|
|
|
|
# The new Claude API proxy block
|
|
|
|
|
new_block = '''location /aiui/api/claude/ {
|
|
|
|
|
if ($cookie_session = "") {
|
|
|
|
|
return 401 '{"error":"Unauthorized"}';
|
|
|
|
|
}
|
|
|
|
|
proxy_pass https://api.anthropic.com/;
|
|
|
|
|
proxy_http_version 1.1;
|
|
|
|
|
proxy_set_header Host api.anthropic.com;
|
|
|
|
|
proxy_set_header x-api-key "''' + API_KEY + '''";
|
|
|
|
|
proxy_set_header anthropic-version "2023-06-01";
|
|
|
|
|
proxy_set_header anthropic-dangerous-direct-browser-access "true";
|
|
|
|
|
proxy_ssl_server_name on;
|
|
|
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
|
|
|
proxy_buffering off;
|
|
|
|
|
proxy_cache off;
|
|
|
|
|
proxy_connect_timeout 120s;
|
|
|
|
|
proxy_read_timeout 300s;
|
|
|
|
|
proxy_send_timeout 120s;
|
|
|
|
|
}'''
|
|
|
|
|
|
|
|
|
|
# Replace existing Claude API proxy blocks (handles both old proxy and direct patterns)
|
|
|
|
|
pattern = r'location /aiui/api/claude/ \{[^}]*(?:\{[^}]*\}[^}]*)*\}'
|
|
|
|
|
content = re.sub(pattern, new_block, content)
|
|
|
|
|
|
|
|
|
|
with open("/etc/nginx/sites-available/archipelago", "w") as f:
|
|
|
|
|
f.write(content)
|
|
|
|
|
|
|
|
|
|
# Verify
|
|
|
|
|
count = content.count("api.anthropic.com")
|
|
|
|
|
print(f" Patched {count // 2} Claude API proxy blocks (HTTP + HTTPS)")
|
|
|
|
|
PYSCRIPT
|
|
|
|
|
|
|
|
|
|
scp $SSH_OPTS /tmp/patch-nginx-claude.py "$TARGET_HOST:/tmp/patch-nginx-claude.py"
|
|
|
|
|
ssh $SSH_OPTS "$TARGET_HOST" "sudo python3 /tmp/patch-nginx-claude.py '$ANTHROPIC_API_KEY'"
|
|
|
|
|
|
|
|
|
|
# Test and reload nginx
|
|
|
|
|
echo " Testing nginx config..."
|
|
|
|
|
ssh $SSH_OPTS "$TARGET_HOST" "sudo nginx -t 2>&1 && sudo systemctl reload nginx && echo ' Nginx reloaded OK'" || {
|
|
|
|
|
echo " ERROR: nginx config test failed!"
|
|
|
|
|
exit 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# --- Step 3: Fix FileBrowser container ---
|
|
|
|
|
echo ""
|
|
|
|
|
echo "$(timestamp) 📁 Checking FileBrowser..."
|
|
|
|
|
|
2026-03-21 01:11:05 +00:00
|
|
|
FB_STATUS=$(ssh $SSH_OPTS "$TARGET_HOST" "podman inspect filebrowser 2>/dev/null | grep -oP '\"ReadonlyRootfs\":\s*\K\w+'" 2>/dev/null || echo "not_found")
|
2026-03-12 12:56:59 +00:00
|
|
|
|
|
|
|
|
if [ "$FB_STATUS" = "true" ]; then
|
|
|
|
|
echo " FileBrowser has read-only root — recreating..."
|
|
|
|
|
ssh $SSH_OPTS "$TARGET_HOST" "
|
2026-03-21 01:11:05 +00:00
|
|
|
podman stop filebrowser 2>/dev/null
|
|
|
|
|
podman rm filebrowser 2>/dev/null
|
2026-03-12 12:56:59 +00:00
|
|
|
sudo mkdir -p /var/lib/archipelago/filebrowser
|
2026-03-21 01:11:05 +00:00
|
|
|
podman run -d --name filebrowser --restart=always \
|
2026-03-12 12:56:59 +00:00
|
|
|
-p 8083:80 \
|
|
|
|
|
-v /var/lib/archipelago/filebrowser:/srv \
|
|
|
|
|
filebrowser/filebrowser:v2.27.0
|
|
|
|
|
" 2>&1 | tail -2
|
|
|
|
|
echo " FileBrowser recreated."
|
|
|
|
|
elif [ "$FB_STATUS" = "not_found" ]; then
|
|
|
|
|
echo " FileBrowser not found — creating..."
|
|
|
|
|
ssh $SSH_OPTS "$TARGET_HOST" "
|
|
|
|
|
sudo mkdir -p /var/lib/archipelago/filebrowser
|
2026-03-21 01:11:05 +00:00
|
|
|
podman run -d --name filebrowser --restart=always \
|
2026-03-12 12:56:59 +00:00
|
|
|
-p 8083:80 \
|
|
|
|
|
-v /var/lib/archipelago/filebrowser:/srv \
|
|
|
|
|
filebrowser/filebrowser:v2.27.0
|
|
|
|
|
" 2>&1 | tail -2
|
|
|
|
|
echo " FileBrowser created."
|
|
|
|
|
else
|
|
|
|
|
echo " FileBrowser OK (ReadonlyRootfs: $FB_STATUS)"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# --- Step 4: Verify ---
|
|
|
|
|
echo ""
|
|
|
|
|
echo "$(timestamp) ✅ Verification..."
|
|
|
|
|
ssh $SSH_OPTS "$TARGET_HOST" "
|
|
|
|
|
echo \" AIUI index: \$(ls -la /opt/archipelago/web-ui/aiui/index.html 2>/dev/null | awk '{print \$6,\$7,\$8}')\"
|
2026-03-21 01:11:05 +00:00
|
|
|
echo \" FileBrowser: \$(podman ps --format '{{.Names}} {{.Status}}' | grep filebrowser)\"
|
2026-03-12 12:56:59 +00:00
|
|
|
echo \" Nginx: \$(systemctl is-active nginx)\"
|
|
|
|
|
echo \" Backend: \$(systemctl is-active archipelago)\"
|
|
|
|
|
echo \" Claude API test: \$(curl -s -o /dev/null -w '%{http_code}' -X POST http://localhost/aiui/api/claude/v1/messages -H 'Content-Type: application/json' -H 'Cookie: session=test' -d '{\"model\":\"claude-sonnet-4-20250514\",\"max_tokens\":5,\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}]}')\"
|
|
|
|
|
"
|
|
|
|
|
|
|
|
|
|
echo ""
|
|
|
|
|
echo "$(timestamp) Done! Server configured."
|
|
|
|
|
echo " Access: http://$(echo $TARGET_HOST | cut -d@ -f2)"
|