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

#!/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]"