该脚本在Ubuntu中部署,支持Windows访问web控制台!
亲测好用,免去很多配置步骤,只需要填写一些基本的参数即可,但是没有Skill的安装步骤,需要自己去安装一下
在 OpenClaw 的日常运维与集群部署中,随着节点数量的增加,手动维护 JSON 配置文件、管理端口映射以及执行多节点启停等操作变得相对繁琐。为提升部署效率并规范化节点管理,本文分享一个基于 Bash 开发的 OpenClaw 多节点自动化管理脚本(OpenClaw Manager,简称 OCM)。
该脚本提供了交互式的终端菜单,集成了环境初始化、多模型厂商配置、智能端口分配及节点生命周期管理等功能,且通过CMD隧道,实现Windows访问
核心功能概述
-
自动化环境准备 脚本启动时将自动检测
jq、Node.js、OpenClaw及psmisc等底层依赖。若检测到组件缺失,将自动配置国内镜像源并静默完成安装。 -
配置状态持久化 内置输入缓存机制,可自动记录历史使用的 API Key 与模型标识。用户在进行二次部署或新增节点时,可直接通过数字键快速复用历史配置,提高录入效率。
-
多厂商协议自动适配 内置硅基流动、Claude、OpenAI、Gemini、DeepSeek 等 12 家主流模型 API 供应商。用户选择对应的服务商后,系统会自动映射并配置对应的底层通信协议。
-
智能端口防冲突机制 采用全局端口分配池与实时端口探测技术。在批量拉起多个节点时,能够自动规避已被占用的端口,有序分配连续的可用端口,解决并发启动导致的端口冲突问题。
-
节点生命周期闭环管理 支持通过控制台对已部署的节点进行独立管理,包含:节点参数修改(端口、厂商、模型等)、热重启、单节点启停控制以及目录级安全销毁。

通过系统给出的命令进行CMD隧道建立



源码与部署指引
请在基于 Debian/Ubuntu 的终端中执行以下命令,生成并运行控制台:
cat << 'EOF' > ~/openclaw_deploy.sh
#!/bin/bash
# ╔══════════════════════════════════════════════════════════════════╗
# ║ OpenClaw Manager (OCM) v2.8 · 完美并发端口分配与全大厂阵列版 ║
# ╚══════════════════════════════════════════════════════════════════╝
set -uo pipefail
# ─── 颜色与变量 ──────────────────────────────────────────────────
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; CYAN='\033[0;36m'; MAGENTA='\033[0;35m'; BOLD='\033[1m'; DIM='\033[2m'; RESET='\033[0m'
BASE_DIR="$HOME/.openclaw_nodes"
KEYS_FILE="$HOME/.oc_keys.txt"
MODELS_FILE="$HOME/.oc_models.txt"
ALLOCATED_PORTS=()
# 🌟 多厂商API (自动匹配专属协议)
TMPL_NAMES=(
"硅基流动 SiliconFlow"
"Anthropic Claude 官方"
"OpenAI GPT 官方"
"Google Gemini 官方"
"DeepSeek 官方"
"阿里云百炼 DashScope"
"智谱清言 Zhipu"
"月之暗面 Moonshot"
"字节豆包 Ark"
"xAI Grok"
"Groq 高速"
"自定义 URL"
)
TMPL_URLS=(
"https://api.siliconflow.cn/v1"
"https://api.anthropic.com/v1"
"https://api.openai.com/v1"
"https://generativelanguage.googleapis.com/v1beta"
"https://api.deepseek.com/v1"
"https://dashscope.aliyuncs.com/compatible-mode/v1"
"https://open.bigmodel.cn/api/paas/v4"
"https://api.moonshot.cn/v1"
"https://ark.cn-beijing.volces.com/api/v3"
"https://api.x.ai/v1"
"https://api.groq.com/openai/v1"
"custom"
)
log_info() { echo -e "${GREEN}[INFO]${RESET} $*"; }
log_warn() { echo -e "${YELLOW}[WARN]${RESET} $*"; }
log_error() { echo -e "${RED}[ERROR] $*${RESET}"; }
log_step() { echo -e "\n${BOLD}${MAGENTA}━━━ $* ━━━${RESET}"; }
log_success() { echo -e "${GREEN}${BOLD}✅ $*${RESET}"; }
pause() { read -n 1 -s -r -p $'\n\033[1;33m按任意键继续...\033[0m'; echo ""; }
safe_run() { local desc="$1"; shift; if ! "$@"; then log_error "$desc 失败"; exit 1; fi }
has_cmd() { command -v "$1" &>/dev/null; }
read_lines() { local f="$1"; local -n arr="$2"; arr=(); if [[ -f "$f" ]]; then while read -r line; do [[ -n "$line" ]] && arr+=("$line"); done < "$f"; fi; }
save_line() {
local f="$1"; local val="$2"
grep -Fxq "$val" "$f" 2>/dev/null || echo "$val" >> "$f"
tail -n 8 "$f" > "$f.tmp" && mv "$f.tmp" "$f"
}
menu_select() {
local prompt="$1"; shift; local options=("$@") choice
echo -e "${CYAN} $prompt${RESET}" >&2
for i in "${!options[@]}"; do
if [[ $((i+1)) -lt 10 ]]; then echo -e " ${BOLD}$((i+1))${RESET}) ${options[$i]}" >&2
else echo -e " ${BOLD}$((i+1))${RESET}) ${options[$i]}" >&2; fi
done
while true; do
echo -ne " ${CYAN}请输入数字并回车 (1-${#options[@]}): ${RESET}" >&2
read -r choice
if [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= ${#options[@]} )); then
echo "$choice"; return
fi
done
}
read_default() {
local prompt="$1" default="$2" varname="$3" _input
echo -ne "${CYAN} ${prompt}${RESET} [默认: ${YELLOW}${default}${RESET}]: " >&2
read -r _input
printf -v "$varname" '%s' "${_input:-$default}"
}
# 🌟 核心修复 1:纯计算端口,不在子 Shell 中污染全局数组
get_avail_port() {
local p=18810
while ss -tuln | grep -q ":$p " || [[ " ${ALLOCATED_PORTS[*]:-} " =~ " ${p} " ]]; do ((p+=10)); done
echo "$p"
}
kill_gateways() {
ps aux | grep "[o]penclaw gateway" | awk '{print $2}' | xargs -r kill -9 2>/dev/null || true
}
check_and_install_env() {
log_step "基础环境智能自检"
local need_install=false
if ! has_cmd jq || ! has_cmd node || ! has_cmd openclaw || ! has_cmd fuser; then need_install=true; fi
if $need_install; then
log_warn "检测到缺少必要组件,开始自动安装部署环境..."
safe_run "apt update" sudo apt-get update -q
safe_run "安装基础依赖" sudo apt-get install -y -q curl git jq openssh-server ca-certificates python3 psmisc
if ! has_cmd node; then
log_info "正在安装 Node.js..."
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - && sudo apt-get install -y nodejs
fi
if ! has_cmd openclaw; then
log_info "正在安装 OpenClaw 引擎..."
npm config set registry https://registry.npmmirror.com/
safe_run "安装 OpenClaw" sudo npm install -g openclaw@latest
fi
log_success "环境安装完毕!"
else
log_success "环境检测通过,所有底层组件均已就绪。"
fi
}
show_status() {
clear; echo -e "${BOLD}${MAGENTA}╔════════════════ 当前节点运行状态 ════════════════╗${RESET}"
local count=0
if has_cmd jq; then
for nd in "$BASE_DIR"/*; do
if [[ -d "$nd" && -f "$nd/.openclaw/openclaw.json" ]]; then
((count++))
local cfg="$nd/.openclaw/openclaw.json"
local name=$(basename "$nd")
local port=$(jq -r '.gateway.port // "未知"' "$cfg" 2>/dev/null)
local prov=$(jq -r '.models.providers | keys[0] // "未知"' "$cfg" 2>/dev/null)
local model=$(jq -r ".models.providers[\"$prov\"].models[0].name // \"未知\"" "$cfg" 2>/dev/null)
local status="${RED}已停止 ⭕${RESET}"
if ss -tuln | grep -q ":$port "; then status="${GREEN}运行中 🟢${RESET}"; fi
echo -e " ${BOLD}$name${RESET} | 端口: ${CYAN}$port${RESET} | 厂商: ${YELLOW}$prov${RESET} | 模型: $model | 状态: $status"
fi
done
fi
[[ $count -eq 0 ]] && echo -e " ${DIM}暂无已部署的节点。${RESET}"
echo -e "${BOLD}${MAGENTA}╚══════════════════════════════════════════════════╝${RESET}\n"
}
print_all_tunnels() {
local LAN_IP=$(hostname -I | awk '{print $1}')
local target_ports=(); local node_names=(); local node_links=()
for nd in "$BASE_DIR"/*; do
if [[ -d "$nd" && -f "$nd/.openclaw/openclaw.json" ]]; then
local port=$(jq -r '.gateway.port' "$nd/.openclaw/openclaw.json" 2>/dev/null)
local tok=$(jq -r '.gateway.auth.token' "$nd/.openclaw/openclaw.json" 2>/dev/null)
local name=$(basename "$nd")
if [[ -n "$port" ]]; then
target_ports+=("$port"); node_names+=("$name")
node_links+=(" $name → ${GREEN}http://127.0.0.1:$port/?token=$tok${RESET}")
fi
fi
done
[[ ${#target_ports[@]} -eq 0 ]] && return
echo -e "\n${BOLD}${GREEN}╔══════════════════════════════════════════════════════╗${RESET}"
echo -e "${BOLD}${GREEN}║ 🏁 节点访问与隧道指引 ║${RESET}"
echo -e "${BOLD}${GREEN}╚══════════════════════════════════════════════════════╝${RESET}"
echo -e "\n${BOLD}${YELLOW}【1】Windows CMD 隧道命令 (两种方式任选其一):${RESET}"
echo -e " ${RED}⚠️ 注意:输入密码连通后,请将 CMD 窗口最小化,切勿随意关闭!${RESET}"
echo -e "\n ${CYAN}▶ 方式 A:单窗口全开 (推荐,只需开 1 个 CMD)${RESET}"
local all_cmd="ssh"
for p in "${target_ports[@]}"; do all_cmd+=" -L $p:127.0.0.1:$p"; done
echo -e " ${BOLD}${all_cmd} root@$LAN_IP${RESET}"
echo -e "\n ${CYAN}▶ 方式 B:多窗口独立 (共 ${#target_ports[@]} 个节点,可分别在不同的 CMD 中运行)${RESET}"
for ((i=0; i<${#target_ports[@]}; i++)); do
echo -e " ${node_names[$i]}: ${BOLD}ssh -L ${target_ports[$i]}:127.0.0.1:${target_ports[$i]} root@$LAN_IP${RESET}"
done
echo -e "\n${BOLD}${YELLOW}【2】浏览器免密直达链接:${RESET}"
for link in "${node_links[@]}"; do echo -e "$link"; done
echo -e "${BOLD}${GREEN}══════════════════════════════════════════════════════${RESET}\n"
}
menu_view_info() {
clear; echo -e "${BOLD}${MAGENTA}╔════════════════ 节点详细信息检视 ════════════════╗${RESET}"
local count=0
for nd in "$BASE_DIR"/*; do
if [[ -d "$nd" && -f "$nd/.openclaw/openclaw.json" ]]; then
((count++))
local cfg="$nd/.openclaw/openclaw.json"
local name=$(basename "$nd")
local port=$(jq -r '.gateway.port // "未知"' "$cfg" 2>/dev/null)
local tok=$(jq -r '.gateway.auth.token // "未生成"' "$cfg" 2>/dev/null)
local prov=$(jq -r '.models.providers | keys[0] // "未知"' "$cfg" 2>/dev/null)
local model=$(jq -r ".models.providers[\"$prov\"].models[0].name // \"未知\"" "$cfg" 2>/dev/null)
local key=$(jq -r ".models.providers[\"$prov\"].apiKey // \"未知\"" "$cfg" 2>/dev/null)
local display_key="未知"
if [[ "$key" != "未知" && ${#key} -gt 10 ]]; then display_key="${key:0:5}******${key: -4}"; elif [[ "$key" != "未知" ]]; then display_key="******"; fi
echo -e "\n${BOLD}${BLUE}▶ $name${RESET}"
echo -e " - 端口: ${CYAN}$port${RESET} | 厂商: ${YELLOW}$prov${RESET} | 模型: $model"
echo -e " - 密钥: ${DIM}$display_key${RESET}"
echo -e " - Token: ${YELLOW}$tok${RESET}"
fi
done
[[ $count -eq 0 ]] && echo -e " ${DIM}暂无已部署的节点。${RESET}"
echo -e "\n${BOLD}${MAGENTA}╚══════════════════════════════════════════════════╝${RESET}"
print_all_tunnels
pause
}
generate_provider_json() {
local p_url="$1" p_key="$2" p_model="$3"
local p_id="provider"; local p_api="openai-completions"
if [[ "$p_url" == *"siliconflow"* ]]; then p_id="siliconflow"
elif [[ "$p_url" == *"anthropic"* ]]; then p_id="anthropic"; p_api="anthropic-messages"
elif [[ "$p_url" == *"openai"* ]]; then p_id="openai"
elif [[ "$p_url" == *"googleapis"* ]]; then p_id="google"; p_api="google-generative-ai"
elif [[ "$p_url" == *"dashscope"* ]]; then p_id="dashscope"
elif [[ "$p_url" == *"deepseek"* ]]; then p_id="deepseek"
elif [[ "$p_url" == *"bigmodel"* ]]; then p_id="zhipu"
elif [[ "$p_url" == *"moonshot"* ]]; then p_id="moonshot"
elif [[ "$p_url" == *"volces"* ]]; then p_id="ark"
elif [[ "$p_url" == *"x.ai"* ]]; then p_id="xai"
elif [[ "$p_url" == *"groq"* ]]; then p_id="groq"
fi
echo $(jq -n --arg pid "$p_id" --arg api "$p_api" --arg apiKey "$p_key" --arg baseUrl "$p_url" \
--arg mid "$p_model" '{($pid): {api:$api, apiKey:$apiKey, auth:"api-key", baseUrl:$baseUrl, models:[{id:$mid, name:$mid, api:$api, maxTokens:8192, input:["text"]}]}}')
}
collect_multi_nodes() {
local total=$1
MULTI_CONFIGS=()
local p_urls=(); local p_keys=(); local p_models=()
log_step "步骤 1/3:配置【厂商 Base URL】"
for (( i=1; i<=total; i++ )); do
echo -e "\n${BOLD}${BLUE}▶ 正在为 Node_$i 选择厂商${RESET}"
local u_idx=$(menu_select "选择厂商" "${TMPL_NAMES[@]}")
local prov_url="${TMPL_URLS[$((u_idx-1))]}"
if [[ "$prov_url" == "custom" ]]; then echo -ne " 输入自定义 URL: "; read -r prov_url; fi
p_urls+=("$prov_url")
done
log_step "步骤 2/3:配置【API Key】"
read_lines "$KEYS_FILE" my_keys
local k_opts=("🆕 手动输入新 API Key")
for k in "${my_keys[@]}"; do if [[ ${#k} -gt 10 ]]; then k_opts+=("🗂️ 历史: ${k:0:5}******${k: -4}"); else k_opts+=("🗂️ 历史: ******"); fi; done
for (( i=1; i<=total; i++ )); do
echo -e "\n${BOLD}${BLUE}▶ 正在为 Node_$i 配置 API Key${RESET}"
local k_idx=$(menu_select "选择或输入 API Key" "${k_opts[@]}")
local prov_key=""
if [[ $k_idx -eq 1 ]]; then
echo -ne " 请输入新 API Key (不回显): "; read -rs prov_key; echo ""
save_line "$KEYS_FILE" "$prov_key"
k_opts+=("🗂️ 历史: ${prov_key:0:5}******${prov_key: -4}")
my_keys+=("$prov_key")
else
prov_key="${my_keys[$((k_idx-2))]}"
fi
p_keys+=("$prov_key")
done
log_step "步骤 3/3:配置【模型 ID】"
read_lines "$MODELS_FILE" my_models
local m_opts=("🆕 手动输入新 Model ID")
for m in "${my_models[@]}"; do m_opts+=("🗂️ 历史: $m"); done
for (( i=1; i<=total; i++ )); do
echo -e "\n${BOLD}${BLUE}▶ 正在为 Node_$i 配置模型 ID${RESET}"
local m_idx=$(menu_select "选择或输入模型 ID" "${m_opts[@]}")
local prov_model=""
if [[ $m_idx -eq 1 ]]; then
echo -ne " 请输入模型 ID: "; read -r prov_model
save_line "$MODELS_FILE" "$prov_model"
m_opts+=("🗂️ 历史: $prov_model")
my_models+=("$prov_model")
else
prov_model="${my_models[$((m_idx-2))]}"
fi
p_models+=("$prov_model")
done
for (( i=1; i<=total; i++ )); do
MULTI_CONFIGS+=("$(generate_provider_json "${p_urls[$((i-1))]}" "${p_keys[$((i-1))]}" "${p_models[$((i-1))]}")")
done
}
start_and_mount() {
local nd="$1" port="$2" prov_json="$3"
log_info "正在启动 $(basename $nd) (绑定端口 $port)..."
export OPENCLAW_HOME="$nd"
openclaw config set gateway.port "$port" >/dev/null 2>&1
openclaw config set gateway.mode local >/dev/null 2>&1
openclaw config set gateway.auth false >/dev/null 2>&1
nohup openclaw gateway > "$nd/node.log" 2>&1 &
local waited=0 cfg="$nd/.openclaw/openclaw.json"
while [[ ! -f "$cfg" ]] || [[ -z "$(jq -r '.gateway.auth.token // empty' "$cfg" 2>/dev/null)" ]]; do
sleep 1; ((waited++)); echo -n "."; [[ $waited -gt 25 ]] && { log_error "\n$(basename $nd) Token 生成超时,请查看日志排错!"; return; }
done
# 🌟 核心修复 2:进度条完成后换行,解决日志挤压问题
echo ""
local tmp="${cfg}.tmp"
jq --argjson p "$prov_json" '.gateway.controlUi.allowedOrigins = ["*"] | .models.providers = $p' "$cfg" > "$tmp" 2>/dev/null && mv "$tmp" "$cfg"
fuser -k -9 "$port/tcp" >/dev/null 2>&1 || true; sleep 1
nohup openclaw gateway >> "$nd/node.log" 2>&1 &
}
# ─── 核心功能:新环境部署 ─────────────────────────
menu_new_deploy() {
local existing_nodes=0
for nd in "$BASE_DIR"/*; do [[ -d "$nd" && -f "$nd/.openclaw/openclaw.json" ]] && ((existing_nodes++)); done
if [[ $existing_nodes -gt 0 ]]; then
log_warn "检测到当前已存在 $existing_nodes 个部署好的节点环境。"
echo -ne " 确认要进行【新环境部署】吗?(y/N): "
read -n 1 -s ans1; echo "$ans1"
[[ ! "$ans1" =~ ^[Yy]$ ]] && return
echo -ne " 是否需要【彻底清空】全部旧环境数据及节点?(y/N): "
read -n 1 -s ans2; echo "$ans2"
kill_gateways
if [[ "$ans2" =~ ^[Yy]$ ]]; then
log_info "正在彻底抹除旧环境目录..."
rm -rf "$BASE_DIR"
else
log_info "保留旧环境目录,将在原有基础上进行覆盖/新增..."
fi
else
echo -ne " 确认开始【新环境部署】吗?(y/N): "
read -n 1 -s ans1; echo "$ans1"
[[ ! "$ans1" =~ ^[Yy]$ ]] && return
kill_gateways
fi
check_and_install_env
echo -ne "\n 请输入本次需要部署的节点总数 (直接键入数字并回车): "
read -r TOTAL
if [[ ! "$TOTAL" =~ ^[1-9][0-9]*$ ]]; then log_error "输入无效"; pause; return; fi
ALLOCATED_PORTS=()
collect_multi_nodes "$TOTAL"
log_step "步骤 4/4:点火与配置注入"
local ports=()
for (( i=1; i<=TOTAL; i++ )); do
local p=$(get_avail_port)
# 🌟 核心修复 3:在外层(主 Shell)显式记录已分配端口
ALLOCATED_PORTS+=("$p")
ports+=("$p")
local nd_name="node_$i"; mkdir -p "$BASE_DIR/$nd_name"
start_and_mount "$BASE_DIR/$nd_name" "$p" "${MULTI_CONFIGS[$((i-1))]}"
done
print_all_tunnels; pause
}
# ─── 其他节点管理功能 ───────────────────────────────────────────
menu_add_node() {
check_and_install_env
local next_idx=1
while [[ -d "$BASE_DIR/node_$next_idx" ]]; do ((next_idx++)); done
local nd_name="node_$next_idx"
ALLOCATED_PORTS=()
local port=$(get_avail_port)
ALLOCATED_PORTS+=("$port")
echo -e "\n${BOLD}${BLUE}┌─ 新增节点 ──────────────────────────────────────┐${RESET}"
read_default "自定义节点名称" "$nd_name" final_name
collect_multi_nodes 1
echo -e "${BOLD}${BLUE}└─────────────────────────────────────────────────┘${RESET}"
mkdir -p "$BASE_DIR/$final_name"
start_and_mount "$BASE_DIR/$final_name" "$port" "${MULTI_CONFIGS[0]}"
log_success "节点 $final_name 启动成功!"
print_all_tunnels; pause
}
menu_edit_node() {
local nodes=(); local opts=()
for nd in "$BASE_DIR"/*; do
if [[ -d "$nd" && -f "$nd/.openclaw/openclaw.json" ]]; then nodes+=("$nd"); opts+=("修改 $(basename "$nd")"); fi
done
if [[ ${#nodes[@]} -eq 0 ]]; then log_warn "没有可编辑的节点。"; pause; return; fi
opts+=("返回上一级")
local choice=$(menu_select "选择要编辑的节点" "${opts[@]}")
if [[ $choice -le ${#nodes[@]} ]]; then
local target_dir="${nodes[$((choice-1))]}"
local old_name=$(basename "$target_dir")
local old_port=$(jq -r '.gateway.port // ""' "$target_dir/.openclaw/openclaw.json" 2>/dev/null)
log_info "正在停止 $old_name 以便修改配置..."
fuser -k -9 "$old_port/tcp" >/dev/null 2>&1 || true
echo -e "\n${BOLD}${BLUE}┌─ 编辑节点参数 ──────────────────────────────────────┐${RESET}"
read_default "节点名称 (目录名)" "$old_name" new_name
local new_dir="$BASE_DIR/$new_name"
if [[ "$new_name" != "$old_name" ]]; then
if [[ -d "$new_dir" ]]; then log_error "目标名称 $new_name 已存在!取消重命名。"; new_name="$old_name"; new_dir="$target_dir"
else mv "$target_dir" "$new_dir"; fi
fi
ALLOCATED_PORTS=()
collect_multi_nodes 1
echo -e "${BOLD}${BLUE}└─────────────────────────────────────────────────────┘${RESET}"
start_and_mount "$new_dir" "$old_port" "${MULTI_CONFIGS[0]}"
log_success "节点 $old_name 已成功更新!面板及命令已自动刷新。"
print_all_tunnels; pause
fi
}
menu_toggle_node() {
local dirs=(); local ports=(); local opts=(); local statuses=()
for nd in "$BASE_DIR"/*; do
if [[ -d "$nd" && -f "$nd/.openclaw/openclaw.json" ]]; then
local port=$(jq -r '.gateway.port' "$nd/.openclaw/openclaw.json" 2>/dev/null)
local name=$(basename "$nd")
dirs+=("$nd"); ports+=("$port")
if ss -tuln | grep -q ":$port "; then
opts+=("$name (端口 $port) - 🟢 运行中 -> ${RED}点击关闭${RESET}")
statuses+=("running")
else
opts+=("$name (端口 $port) - ⭕ 已停止 -> ${GREEN}点击启动${RESET}")
statuses+=("stopped")
fi
fi
done
if [[ ${#dirs[@]} -eq 0 ]]; then log_warn "当前没有已配置的节点。"; pause; return; fi
opts+=("返回上一级")
local choice=$(menu_select "选择要操作的节点" "${opts[@]}")
if [[ $choice -le ${#dirs[@]} ]]; then
local idx=$((choice-1)); local t_dir="${dirs[$idx]}"; local t_port="${ports[$idx]}"; local t_name=$(basename "$t_dir"); local t_status="${statuses[$idx]}"
if [[ "$t_status" == "running" ]]; then
log_info "正在停止 $t_name (强杀端口 $t_port)..."
fuser -k -9 "$t_port/tcp" >/dev/null 2>&1 || true
log_success "$t_name 已关闭。"
else
log_info "正在唤醒 $t_name (绑定端口 $t_port)..."
export OPENCLAW_HOME="$t_dir"
nohup openclaw gateway >> "$t_dir/node.log" 2>&1 &
sleep 2
if ss -tuln | grep -q ":$t_port "; then log_success "$t_name 已成功启动!"
else log_error "$t_name 启动失败,请查看日志。"
fi
fi
pause
fi
}
menu_delete_node() {
local dirs=(); local opts=()
for nd in "$BASE_DIR"/*; do
if [[ -d "$nd" && -f "$nd/.openclaw/openclaw.json" ]]; then
dirs+=("$nd"); opts+=("销毁 $(basename "$nd")")
fi
done
if [[ ${#dirs[@]} -eq 0 ]]; then log_warn "当前没有可删除的节点。"; pause; return; fi
opts+=("返回上一级")
local choice=$(menu_select "选择要彻底删除的节点" "${opts[@]}")
if [[ $choice -le ${#dirs[@]} ]]; then
local t_dir="${dirs[$((choice-1))]}"; local t_name=$(basename "$t_dir")
local t_port=$(jq -r '.gateway.port' "$t_dir/.openclaw/openclaw.json" 2>/dev/null)
echo -ne "${RED}⚠️ 警告:确定要彻底删除 $t_name 及所有配置吗?(y/N): ${RESET}"
read -n 1 -s ans; echo "$ans"
if [[ "$ans" =~ ^[Yy]$ ]]; then
log_info "正在清理进程与目录..."
fuser -k -9 "$t_port/tcp" >/dev/null 2>&1 || true
rm -rf "$t_dir"
log_success "$t_name 已被彻底删除!"
else
log_info "已取消删除。"
fi
pause
fi
}
menu_start_existing() {
log_info "正在唤醒所有已停止的节点..."
local started=0
for nd in "$BASE_DIR"/*; do
if [[ -d "$nd" && -f "$nd/.openclaw/openclaw.json" ]]; then
local port=$(jq -r '.gateway.port' "$nd/.openclaw/openclaw.json" 2>/dev/null)
if ! ss -tuln | grep -q ":$port "; then
export OPENCLAW_HOME="$nd"
nohup openclaw gateway >> "$nd/node.log" 2>&1 &
((started++))
fi
fi
done
[[ $started -eq 0 ]] && log_info "所有节点都在运行中,无需唤醒。" || log_success "共唤醒 $started 个节点。"
pause
}
menu_view_logs() {
local dirs=(); local opts=()
for nd in "$BASE_DIR"/*; do
if [[ -d "$nd" ]]; then dirs+=("$nd"); opts+=("查看 $(basename "$nd") 实时日志"); fi
done
if [[ ${#dirs[@]} -eq 0 ]]; then log_warn "当前没有节点。"; pause; return; fi
opts+=("返回上一级")
local choice=$(menu_select "选择要查看日志的节点" "${opts[@]}")
if [[ $choice -le ${#dirs[@]} ]]; then
local target_log="${dirs[$((choice-1))]}/node.log"
clear
echo -e "${BOLD}${CYAN}实时日志流 ( $(basename "${dirs[$((choice-1))]}") ) ${RESET}"
echo -e "${DIM}提示:按 Ctrl+C 退出日志查看,不会影响节点运行。${RESET}"
echo -e "──────────────────────────────────────────────────────────"
trap '' INT
tail -f -n 30 "$target_log" || echo "日志文件为空或不存在。"
trap - INT
echo -e "\n${GREEN}已退出日志面板。${RESET}"
pause
fi
}
# ─── 主路由 ──────────────────────────────────────────────────────
while true; do
show_status
echo -e "${CYAN}请直接按数字键选择操作:${RESET}"
echo " 1) 🆕 新环境部署 (智能环境检测/批量建站)"
echo " 2) 🚀 唤醒所有 (一键启动所有已停止节点)"
echo " 3) ⚙️ 节点管理 (新增 / 修改 / 启停 / 删除)"
echo " 4) 📄 查看详细信息与链接"
echo " 5) 📜 实时查看节点日志"
echo " 0) ❌ 退出"
read -n 1 -s main_choice; echo ""
case "$main_choice" in
1) menu_new_deploy ;;
2) menu_start_existing ;;
3)
while true; do
show_status
echo -e "${CYAN}节点管理菜单 (直接按数字键):${RESET}"
echo " 1) ➕ 新增一个节点"
echo " 2) ✏️ 修改已有节点"
echo " 3) 🟢/🛑 启停指定节点"
echo " 4) 🗑️ 彻底删除节点"
echo " 0) ↩️ 返回主菜单"
read -n 1 -s sub_choice; echo ""
case "$sub_choice" in
1) menu_add_node ;;
2) menu_edit_node ;;
3) menu_toggle_node ;;
4) menu_delete_node ;;
0) break ;;
esac
done
;;
4) menu_view_info ;;
5) menu_view_logs ;;
0) echo "再见!"; exit 0 ;;
esac
done
EOF
调整权限并启动
chmod +x ~/openclaw_deploy.sh
~/openclaw_deploy.sh
直接复制代码到命令行中进行回车再添加权限启动即可