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.
226 lines
7.3 KiB
226 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]"
|
|
|