You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
227 lines
7.3 KiB
227 lines
7.3 KiB
|
1 week ago
|
#!/bin/bash
|
||
|
|
###############################################################################
|
||
|
|
# Generate a new remote agent instance from the template.
|
||
|
|
# - Interactive mode: no flags, prompts for values.
|
||
|
|
# - Non-interactive (ChatOps) mode: any flag (-a/-n/-p/-u/-k/-m/-t) enables it;
|
||
|
|
# all required params must be provided, otherwise exits with JSON error.
|
||
|
|
###############################################################################
|
||
|
|
set -e
|
||
|
|
|
||
|
|
WORKSPACE="${WORKSPACE:-/root/.openclaw/workspace}"
|
||
|
|
TEMPLATE_DIR="$WORKSPACE/remote-blueprints/template"
|
||
|
|
|
||
|
|
RED='\033[0;31m'
|
||
|
|
GREEN='\033[0;32m'
|
||
|
|
YELLOW='\033[1;33m'
|
||
|
|
BLUE='\033[0;34m'
|
||
|
|
NC='\033[0m'
|
||
|
|
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
||
|
|
log_ok() { echo -e "${GREEN}[OK]${NC} $1"; }
|
||
|
|
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||
|
|
log_err() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||
|
|
|
||
|
|
# ---------- parse flags (non-interactive mode detection) ----------
|
||
|
|
NONINTERACTIVE=false
|
||
|
|
AGENT_ID=""
|
||
|
|
AGENT_NAME=""
|
||
|
|
PROJECT_ID=""
|
||
|
|
LLM_BASE_URL=""
|
||
|
|
LLM_API_KEY=""
|
||
|
|
LLM_MODEL_ID=""
|
||
|
|
CONTROL_UI_TOKEN=""
|
||
|
|
|
||
|
|
while getopts "a:n:p:u:k:m:t:" opt; do
|
||
|
|
NONINTERACTIVE=true
|
||
|
|
case "$opt" in
|
||
|
|
a) AGENT_ID="$OPTARG" ;;
|
||
|
|
n) AGENT_NAME="$OPTARG" ;;
|
||
|
|
p) PROJECT_ID="$OPTARG" ;;
|
||
|
|
u) LLM_BASE_URL="$OPTARG" ;;
|
||
|
|
k) LLM_API_KEY="$OPTARG" ;;
|
||
|
|
m) LLM_MODEL_ID="$OPTARG" ;;
|
||
|
|
t) CONTROL_UI_TOKEN="$OPTARG" ;;
|
||
|
|
*) ;;
|
||
|
|
esac
|
||
|
|
done
|
||
|
|
shift $((OPTIND-1))
|
||
|
|
|
||
|
|
# ---------- interactive fallback (no flags at all) ----------
|
||
|
|
if [ "$NONINTERACTIVE" = false ]; then
|
||
|
|
echo -n "AGENT_ID (required): "
|
||
|
|
read -r AGENT_ID
|
||
|
|
if [ -z "$AGENT_ID" ]; then
|
||
|
|
log_err "AGENT_ID is required."
|
||
|
|
exit 1
|
||
|
|
fi
|
||
|
|
|
||
|
|
echo -n "AGENT_NAME (optional, default = AGENT_ID): "
|
||
|
|
read -r AGENT_NAME
|
||
|
|
echo -n "PROJECT_ID (optional, default = default): "
|
||
|
|
read -r PROJECT_ID
|
||
|
|
|
||
|
|
echo -n "LLM_BASE_URL (required): "
|
||
|
|
read -r LLM_BASE_URL
|
||
|
|
echo -n "LLM_API_KEY (required): "
|
||
|
|
read -r LLM_API_KEY
|
||
|
|
echo -n "LLM_MODEL_ID (required, e.g. qwen-max, gpt-4o): "
|
||
|
|
read -r LLM_MODEL_ID
|
||
|
|
|
||
|
|
echo -n "CONTROL_UI_TOKEN (optional, auto-generate if empty): "
|
||
|
|
read -r CONTROL_UI_TOKEN
|
||
|
|
else
|
||
|
|
# ---------- strict non-interactive mode ----------
|
||
|
|
missing=()
|
||
|
|
[ -z "$AGENT_ID" ] && missing+=("AGENT_ID")
|
||
|
|
[ -z "$LLM_BASE_URL" ] && missing+=("LLM_BASE_URL")
|
||
|
|
[ -z "$LLM_API_KEY" ] && missing+=("LLM_API_KEY")
|
||
|
|
[ -z "$LLM_MODEL_ID" ] && missing+=("LLM_MODEL_ID")
|
||
|
|
|
||
|
|
if [ ${#missing[@]} -ne 0 ]; then
|
||
|
|
# JSON error for ChatOps callers
|
||
|
|
printf '{"ok":false,"error":"missing_required_params","missing":[' >&2
|
||
|
|
for i in "${!missing[@]}"; do
|
||
|
|
[ "$i" -ne 0 ] && printf ',' >&2
|
||
|
|
printf '"%s"' "${missing[$i]}" >&2
|
||
|
|
done
|
||
|
|
printf ']}' >&2
|
||
|
|
echo >&2
|
||
|
|
exit 1
|
||
|
|
fi
|
||
|
|
fi
|
||
|
|
|
||
|
|
# Defaults for optional values
|
||
|
|
[ -z "$AGENT_NAME" ] && AGENT_NAME="$AGENT_ID"
|
||
|
|
[ -z "$PROJECT_ID" ] && PROJECT_ID="default"
|
||
|
|
|
||
|
|
# ---------- validate AGENT_ID ----------
|
||
|
|
if ! echo "$AGENT_ID" | grep -qE '^[a-zA-Z0-9_-]+$'; then
|
||
|
|
log_err "AGENT_ID must contain only letters, numbers, hyphens, and underscores."
|
||
|
|
exit 1
|
||
|
|
fi
|
||
|
|
|
||
|
|
# ---------- ensure template dir exists ----------
|
||
|
|
if [ ! -d "$TEMPLATE_DIR" ]; then
|
||
|
|
log_err "Template not found: $TEMPLATE_DIR"
|
||
|
|
exit 1
|
||
|
|
fi
|
||
|
|
|
||
|
|
# ---------- auto-generate CONTROL_UI_TOKEN if empty ----------
|
||
|
|
if [ -z "$CONTROL_UI_TOKEN" ]; then
|
||
|
|
if command -v openssl >/dev/null 2>&1; then
|
||
|
|
CONTROL_UI_TOKEN=$(openssl rand -hex 24)
|
||
|
|
log_info "Generated CONTROL_UI_TOKEN via openssl."
|
||
|
|
else
|
||
|
|
CONTROL_UI_TOKEN=$(head -c 32 /dev/urandom | base64 | tr -dc 'a-f0-9' | head -c 48)
|
||
|
|
log_warn "openssl not found; CONTROL_UI_TOKEN generated via /dev/urandom fallback."
|
||
|
|
fi
|
||
|
|
fi
|
||
|
|
|
||
|
|
INSTANCE_DIR="$WORKSPACE/remote-blueprints/$AGENT_ID"
|
||
|
|
if [ -d "$INSTANCE_DIR" ]; then
|
||
|
|
if [ "$NONINTERACTIVE" = true ]; then
|
||
|
|
log_err "Instance already exists: $INSTANCE_DIR"
|
||
|
|
exit 1
|
||
|
|
fi
|
||
|
|
log_warn "Instance already exists: $INSTANCE_DIR"
|
||
|
|
echo -n "Overwrite? (y/N): "
|
||
|
|
read -r confirm
|
||
|
|
if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then
|
||
|
|
log_info "Aborted."
|
||
|
|
exit 0
|
||
|
|
fi
|
||
|
|
rm -rf "$INSTANCE_DIR"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# ---------- copy template ----------
|
||
|
|
log_info "Copying template to $INSTANCE_DIR ..."
|
||
|
|
mkdir -p "$INSTANCE_DIR"
|
||
|
|
cp -r "$TEMPLATE_DIR"/* "$INSTANCE_DIR/"
|
||
|
|
[ -f "$TEMPLATE_DIR/.env.tpl" ] && cp "$TEMPLATE_DIR/.env.tpl" "$INSTANCE_DIR/.env.tpl"
|
||
|
|
log_ok "Copy done."
|
||
|
|
|
||
|
|
# ---------- render templates (safe sed with # delimiter) ----------
|
||
|
|
escape_sed_val() {
|
||
|
|
local v="$1"
|
||
|
|
v="${v//\\/\\\\}"
|
||
|
|
v="${v//&/\\&}"
|
||
|
|
v="${v//\//\\/}"
|
||
|
|
printf '%s' "$v"
|
||
|
|
}
|
||
|
|
|
||
|
|
AGENT_NAME_ESC=$(escape_sed_val "$AGENT_NAME")
|
||
|
|
PROJECT_ID_ESC=$(escape_sed_val "$PROJECT_ID")
|
||
|
|
LLM_BASE_URL_ESC=$(escape_sed_val "$LLM_BASE_URL")
|
||
|
|
LLM_API_KEY_ESC=$(escape_sed_val "$LLM_API_KEY")
|
||
|
|
LLM_MODEL_ID_ESC=$(escape_sed_val "$LLM_MODEL_ID")
|
||
|
|
CONTROL_UI_TOKEN_ESC=$(escape_sed_val "$CONTROL_UI_TOKEN")
|
||
|
|
|
||
|
|
# Render all *.tpl and .env.tpl files
|
||
|
|
while IFS= read -r -d '' f; do
|
||
|
|
log_info "Rendering $f ..."
|
||
|
|
sed -i "s#{{AGENT_ID}}#$AGENT_ID#g" "$f"
|
||
|
|
sed -i "s#{{AGENT_NAME}}#$AGENT_NAME_ESC#g" "$f"
|
||
|
|
sed -i "s#{{PROJECT_ID}}#$PROJECT_ID_ESC#g" "$f"
|
||
|
|
sed -i "s#{{LLM_BASE_URL}}#$LLM_BASE_URL_ESC#g" "$f"
|
||
|
|
sed -i "s#{{LLM_API_KEY}}#$LLM_API_KEY_ESC#g" "$f"
|
||
|
|
sed -i "s#{{LLM_MODEL_ID}}#$LLM_MODEL_ID_ESC#g" "$f"
|
||
|
|
sed -i "s#{{CONTROL_UI_TOKEN}}#$CONTROL_UI_TOKEN_ESC#g" "$f"
|
||
|
|
# Backward-compat: clean any legacy BAILIAN placeholders if present
|
||
|
|
sed -i "s#{{BAILIAN_API_KEY}}#$LLM_API_KEY_ESC#g" "$f" || true
|
||
|
|
|
||
|
|
done < <(find "$INSTANCE_DIR" -type f \( -name '*.tpl' -o -name '.env.tpl' \) -print0)
|
||
|
|
|
||
|
|
# ---------- rename agents/{{AGENT_ID}}.json.tpl -> agents/<AGENT_ID>.json.tpl (before generic .tpl removal) ----------
|
||
|
|
if [ -f "$INSTANCE_DIR/agents/{{AGENT_ID}}.json.tpl" ]; then
|
||
|
|
mv "$INSTANCE_DIR/agents/{{AGENT_ID}}.json.tpl" "$INSTANCE_DIR/agents/$AGENT_ID.json.tpl"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# ---------- rename all *.tpl to final filenames ----------
|
||
|
|
while IFS= read -r -d '' f; do
|
||
|
|
base="${f%.tpl}"
|
||
|
|
mv "$f" "$base"
|
||
|
|
log_info "Renamed to $base"
|
||
|
|
done < <(find "$INSTANCE_DIR" -type f -name '*.tpl' -print0)
|
||
|
|
|
||
|
|
# .env.tpl -> .env
|
||
|
|
if [ -f "$INSTANCE_DIR/.env.tpl" ]; then
|
||
|
|
mv "$INSTANCE_DIR/.env.tpl" "$INSTANCE_DIR/.env"
|
||
|
|
log_info "Renamed .env.tpl to .env"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# ---------- generate deploy_to_target.sh ----------
|
||
|
|
DEPLOY_SCRIPT="$INSTANCE_DIR/deploy_to_target.sh"
|
||
|
|
cat > "$DEPLOY_SCRIPT" << 'DEPLOYEOF'
|
||
|
|
#!/bin/bash
|
||
|
|
# Deploy this agent directory to a remote host. Usage: ./deploy_to_target.sh <TARGET_IP> [SSH_USER]
|
||
|
|
set -e
|
||
|
|
|
||
|
|
TARGET_IP="${1:?Usage: $0 <TARGET_IP> [SSH_USER]}"
|
||
|
|
SSH_USER="${2:-root}"
|
||
|
|
AGENT_ID="__AGENT_ID_PLACEHOLDER__"
|
||
|
|
REMOTE_PATH="/opt/openclaw-remote/$AGENT_ID"
|
||
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||
|
|
|
||
|
|
if [ ! -f "$SCRIPT_DIR/docker-compose.yml" ]; then
|
||
|
|
echo "[ERROR] docker-compose.yml not found in $SCRIPT_DIR"
|
||
|
|
exit 1
|
||
|
|
fi
|
||
|
|
|
||
|
|
echo "[INFO] Syncing $SCRIPT_DIR to ${SSH_USER}@${TARGET_IP}:$REMOTE_PATH ..."
|
||
|
|
ssh "${SSH_USER}@${TARGET_IP}" "mkdir -p $REMOTE_PATH"
|
||
|
|
(cd "$SCRIPT_DIR" && tar cf - .) | ssh "${SSH_USER}@${TARGET_IP}" "cd $REMOTE_PATH && tar xf -"
|
||
|
|
|
||
|
|
echo "[INFO] Remote: docker compose down && docker compose up -d --build ..."
|
||
|
|
ssh "${SSH_USER}@${TARGET_IP}" "cd $REMOTE_PATH && docker compose down 2>/dev/null || true && docker compose up -d --build"
|
||
|
|
|
||
|
|
echo "[OK] Deploy done."
|
||
|
|
DEPLOYEOF
|
||
|
|
|
||
|
|
# inject actual AGENT_ID into deploy script
|
||
|
|
sed -i "s#__AGENT_ID_PLACEHOLDER__#$AGENT_ID#g" "$DEPLOY_SCRIPT"
|
||
|
|
chmod +x "$DEPLOY_SCRIPT"
|
||
|
|
log_ok "Generated $DEPLOY_SCRIPT"
|
||
|
|
|
||
|
|
log_ok "Instance ready: $INSTANCE_DIR"
|
||
|
|
echo "[OK] CONTROL_UI_TOKEN: $CONTROL_UI_TOKEN"
|
||
|
|
echo "Next: cd $INSTANCE_DIR && ./deploy_to_target.sh <TARGET_IP> [SSH_USER]"
|