#!/bin/bash
generate_compose_file() {
ensure_envsubst
local compose_file="$CONFIG_BASE/docker-compose.yml"
cp "$TEMPLATES_DIR/docker-compose.header.tpl" "$compose_file"
local skills_merged_dir="$CONFIG_BASE/skills-merged"
[ -d "$skills_merged_dir" ] && find "$skills_merged_dir" -type d -exec chmod u+w {} + 2>/dev/null || true
rm -rf "$skills_merged_dir" 2>/dev/null || true
mkdir -p "$skills_merged_dir"
local image_skills_dir="$CONFIG_BASE/.image-skills-tmp"
rm -rf "$image_skills_dir" 2>/dev/null || true
mkdir -p "$image_skills_dir"
docker rm -f skills-extract-tmp 2>/dev/null || true
local _create_output
_create_output=$(docker create --name skills-extract-tmp "$IMAGE" 2>&1) || {
log_warn "无法从镜像创建临时容器(镜像: $IMAGE, 错误: $_create_output)"
}
if [ -n "$_create_output" ] && docker ps -a --filter name=skills-extract-tmp --format '{{.ID}}' 2>/dev/null | grep -q .; then
docker cp skills-extract-tmp:/home/node/.openclaw/skills-shared/. - 2>/dev/null | tar xf - -C "$image_skills_dir/" 2>/dev/null || true
docker rm -f skills-extract-tmp 2>/dev/null || true
fi
if [ -d "$image_skills_dir" ] && [ "$(ls -A "$image_skills_dir" 2>/dev/null)" ]; then
cp -r "$image_skills_dir/." "$skills_merged_dir/"
log_info "从镜像中提取 skills 到合并目录"
else
log_warn "镜像中未找到 skills-shared 目录,skills 将为空"
fi
rm -rf "$image_skills_dir" 2>/dev/null || true
if [ "$MOUNT_SKILLS" = "true" ]; then
local host_skills_dir
host_skills_dir="$(cd "$SCRIPT_DIR/../../openclaw/skills" 2>/dev/null && pwd || echo "")"
if [ -n "$host_skills_dir" ] && [ -d "$host_skills_dir" ]; then
for skill_dir in "$host_skills_dir"/*/; do
[ -d "$skill_dir" ] || continue
local skill_name
skill_name="$(basename "$skill_dir")"
rm -rf "$skills_merged_dir/$skill_name"
cp -r "$skill_dir" "$skills_merged_dir/$skill_name/"
log_info "合并宿主机技能: $skill_name"
done
else
log_warn "未找到宿主机 skills 目录,仅使用镜像内置技能"
fi
fi
find "$skills_merged_dir" -name "*.sh" -exec chmod +x {} + 2>/dev/null || true
find "$skills_merged_dir" -name "*.py" -exec chmod +x {} + 2>/dev/null || true
local skill_count
skill_count=$(ls -1 "$skills_merged_dir/" 2>/dev/null | wc -l | tr -d ' ')
if [ "$MOUNT_SKILLS" = "true" ]; then
log_ok "skills 合并完成(共 ${skill_count} 项技能),目录: $skills_merged_dir"
else
log_info "skills 提取完成(共 ${skill_count} 项镜像内置技能),目录: $skills_merged_dir"
fi
for i in $(seq "$START_INDEX" "$((START_INDEX + COUNT - 1))"); do
export INSTANCE_NUM="$i"
export INSTANCE_PREFIX
export IMAGE
export GW_PORT=$((BASE_PORT + (i - 1) * 4))
export SFTP_PORT=$((GW_PORT + 1))
export MDNS_PORT_HOST=$((GW_PORT + 2))
export MDNS_PORT
if [ "$OPENCLAW_TOKEN_PER_INSTANCE" = "true" ]; then
local token_file="$CONFIG_BASE/instance-$i/.gateway_token"
if [ -f "$token_file" ]; then
OPENCLAW_TOKEN=$(cat "$token_file")
else
OPENCLAW_TOKEN=$(openssl rand -hex 16)
log_warn "未找到实例 $i 的 Token 文件,重新生成"
fi
fi
export OPENCLAW_TOKEN
local config_dir="$CONFIG_BASE/instance-$i"
export CONFIG_DIR_ABS
if command -v realpath &> /dev/null; then
CONFIG_DIR_ABS="$(realpath "$config_dir" 2>/dev/null || echo "$config_dir")"
elif command -v readlink &> /dev/null; then
CONFIG_DIR_ABS="$(readlink -f "$config_dir" 2>/dev/null || echo "$config_dir")"
else
CONFIG_DIR_ABS="$(cd "$config_dir" 2>/dev/null && pwd || echo "$config_dir")"
fi
export SKILLS_MERGED_DIR_ABS
if command -v realpath &> /dev/null; then
SKILLS_MERGED_DIR_ABS="$(realpath "$skills_merged_dir" 2>/dev/null || echo "$skills_merged_dir")"
elif command -v readlink &> /dev/null; then
SKILLS_MERGED_DIR_ABS="$(readlink -f "$skills_merged_dir" 2>/dev/null || echo "$skills_merged_dir")"
else
SKILLS_MERGED_DIR_ABS="$(cd "$skills_merged_dir" 2>/dev/null && pwd || echo "$skills_merged_dir")"
fi
export INSTALL_HERMES
export GUARDIAN_PORT
local envsubst_vars='${INSTANCE_PREFIX} ${INSTANCE_NUM} ${IMAGE} ${GW_PORT} ${SFTP_PORT} ${MDNS_PORT_HOST} ${MDNS_PORT} ${CONFIG_DIR_ABS} ${OPENCLAW_TOKEN} ${INSTALL_HERMES} ${GUARDIAN_PORT} ${SKILLS_MERGED_DIR_ABS}'
envsubst "$envsubst_vars" \
< "$TEMPLATES_DIR/docker-compose.service.tpl" >> "$compose_file"
if sed --version &> /dev/null; then
SED_I=(sed -i)
else
SED_I=(sed -i '')
fi
if [ -z "$GUARDIAN_PORT" ]; then
"${SED_I[@]}" "/GUARDIAN_PORT/d" "$compose_file"
"${SED_I[@]}" '/"":""$/d' "$compose_file"
"${SED_I[@]}" '/- ":"$/d' "$compose_file"
fi
if [ "$SANDBOX_ENABLED" != "true" ]; then
"${SED_I[@]}" "/docker.sock/d" "$compose_file"
fi
done
if [[ "$(uname -s)" == MINGW* || "$(uname -s)" == MSYS* ]]; then
if command -v iconv &> /dev/null; then
iconv -f GBK -t UTF-8 "$compose_file" > "${compose_file}.tmp" 2>/dev/null && \
mv "${compose_file}.tmp" "$compose_file" || \
rm -f "${compose_file}.tmp"
fi
fi
{
echo ''
echo 'networks:'
echo ' default:'
echo ' driver: bridge'
echo ' ipam:'
echo ' config:'
echo " - subnet: ${SUBNET}"
} >> "$compose_file"
log_ok "docker-compose.yml 生成完成"
}
show_compose_hints() {
echo ""
echo "docker compose 命令参考"
echo "=============================================="
echo "启动集群:"
echo " ${DOCKER_COMPOSE_CMD} -f $CONFIG_BASE/docker-compose.yml up -d"
echo ""
echo "查看日志:"
echo " ${DOCKER_COMPOSE_CMD} -f $CONFIG_BASE/docker-compose.yml logs -f"
echo ""
echo "停止集群:"
echo " ${DOCKER_COMPOSE_CMD} -f $CONFIG_BASE/docker-compose.yml down"
echo "=============================================="
}
cluster_up() {
detect_docker_compose
show_compose_hints
echo ""
log_info "开始启动集群..."
if [ "$SKIP_DOWN" = "true" ]; then
log_info "扩容模式:跳过 down,已有实例不受影响"
else
log_info "执行 ${DOCKER_COMPOSE_CMD} -f $CONFIG_BASE/docker-compose.yml down 关闭已有集群并准备更新"
${DOCKER_COMPOSE_CMD} -f "$CONFIG_BASE/docker-compose.yml" down 2>/dev/null || true
sleep 5
fi
log_info "执行 ${DOCKER_COMPOSE_CMD} -f $CONFIG_BASE/docker-compose.yml up -d 集群启动中"
${DOCKER_COMPOSE_CMD} -f "$CONFIG_BASE/docker-compose.yml" up -d
echo "$IMAGE" > "$CONFIG_BASE/.current_version"
for i in $(seq "$START_INDEX" "$((START_INDEX + COUNT - 1))"); do
local hermes_cfg_dir="$CONFIG_BASE/instance-$i/.hermes"
if [ -d "$hermes_cfg_dir" ]; then
local cfg_ok=true
[ -f "$hermes_cfg_dir/config.yaml" ] || { log_warn "实例 $i hermes config.yaml 缺失"; cfg_ok=false; }
[ -f "$hermes_cfg_dir/.env" ] || { log_warn "实例 $i hermes .env 缺失"; cfg_ok=false; }
if [ "$cfg_ok" = "true" ]; then
log_info "实例 $i hermes 配置已就绪(bind mount: $hermes_cfg_dir)"
fi
else
log_warn "实例 $i hermes 配置目录不存在: $hermes_cfg_dir"
fi
done
log_ok "集群启动完成"
}
cluster_down() {
detect_docker_compose
log_info "正在停止集群..."
${DOCKER_COMPOSE_CMD} -f "$CONFIG_BASE/docker-compose.yml" down 2>/dev/null || true
log_ok "集群已停止"
}