#!/bin/bash
set -e
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_error() { echo -e "${RED}[ERROR]${NC} $1"; }
VERSION="2026.5.22"
REGISTRY="localhost"
OFFLINE=false
SKIP_BASE=false
SKIP_APP=false
SKIP_OVERLAY=false
SKIP_PLUGINS=false
CHECK_PLUGINS=false
OPENCLAW_SRC="${OPENCLAW_SRC:-./.openclaw}"
HERMES_SRC="${HERMES_SRC:-./.hermes}"
PLUGINS_SRC="${PLUGINS_SRC:-./.plugins}"
OPENCLAW_GIT_URL="${OPENCLAW_GIT_URL:-https://github.com/openclaw/openclaw.git}"
HERMES_GIT_URL="${HERMES_GIT_URL:-https://github.com/NousResearch/hermes-agent.git}"
DOCKER_REGISTRY_NPM=""
while [[ $# -gt 0 ]]; do
case $1 in
--offline) OFFLINE=true; shift ;;
--no-cache) DOCKER_BUILD_OPTS="${DOCKER_BUILD_OPTS} --no-cache"; shift ;;
--version) VERSION="$2"; shift 2 ;;
--registry) REGISTRY="$2"; shift 2 ;;
--skip-base) SKIP_BASE=true; shift ;;
--skip-app) SKIP_APP=true; shift ;;
--skip-overlay) SKIP_OVERLAY=true; shift ;;
--skip-plugins) SKIP_PLUGINS=true; shift ;;
--check-plugins) CHECK_PLUGINS=true; shift ;;
--openclaw-src) OPENCLAW_SRC="$2"; shift 2 ;;
--hermes-src) HERMES_SRC="$2"; shift 2 ;;
--plugins-src) PLUGINS_SRC="$2"; shift 2 ;;
--npmmirror) DOCKER_REGISTRY_NPM="$2"; shift 2 ;;
-h|--help)
echo "OpenClaw 三层架构构建脚本(支持离线模式)"
echo ""
echo "用法: $0 [选项]"
echo ""
echo "选项:"
echo " --offline 仅使用本地源码,不尝试网络克隆"
echo " --version VERSION 指定版本(默认 2026.5.22)"
echo " --registry REGISTRY 镜像仓库前缀(默认 localhost)"
echo " --skip-base 跳过 Layer 1 构建"
echo " --skip-app 跳过 Layer 2 构建"
echo " --skip-overlay 跳过 Layer 3 构建"
echo " --skip-plugins 跳过插件准备"
echo " --check-plugins 检查插件集成状态(不执行构建)"
echo " --openclaw-src PATH openclaw 源码目录(默认 ./.openclaw)"
echo " --hermes-src PATH Hermes Agent 源码目录(默认 ./.hermes)"
echo " --plugins-src PATH 自定义插件目录(默认 ./.plugins)"
echo " --npmmirror URL npm 镜像地址"
echo ""
echo "环境变量:"
echo " OPENCLAW_SRC openclaw 源码目录(默认 ./.openclaw)"
echo " HERMES_SRC Hermes Agent 源码目录(默认 ./.hermes)"
echo " PLUGINS_SRC 自定义插件目录(默认 ./.plugins)"
echo " OPENCLAW_GIT_URL OpenClaw git 仓库地址"
echo " HERMES_GIT_URL Hermes git 仓库地址"
echo " DOCKER_BUILD_OPTS 额外传给 docker build 的参数"
echo ""
echo "源码路径(docker/ 目录下的 . 前缀目录):"
echo " - ./.openclaw → OpenClaw 源码(app 层构建上下文)"
echo " - ./.hermes → Hermes Agent 源码"
echo " - ./.plugins → 自定义插件目录"
echo " - ../skills/ → 自定义技能目录(overlay 层使用)"
echo ""
echo "离线构建示例:"
echo " ./build-openclaw.sh --offline --version 2026.5.22"
echo ""
echo "插件检查示例:"
echo " ./build-openclaw.sh --check-plugins"
exit 0
;;
-*)
log_error "未知选项: $1"
exit 1
;;
esac
done
BASE_IMAGE="${REGISTRY}/openclaw:base-${VERSION}"
APP_IMAGE="${REGISTRY}/openclaw:app-${VERSION}"
FINAL_IMAGE="${REGISTRY}/openclaw:${VERSION}"
log_info "构建配置:"
echo " 版本: ${VERSION}"
echo " 镜像仓库: ${REGISTRY}"
echo " Base 镜像: ${BASE_IMAGE}"
echo " App 镜像: ${APP_IMAGE}"
echo " Final 镜像: ${FINAL_IMAGE}"
echo " 离线模式: ${OFFLINE}"
echo " OpenClaw 源码: ${OPENCLAW_SRC}"
echo " Hermes 源码: ${HERMES_SRC}"
echo " 插件目录: ${PLUGINS_SRC}"
echo ""
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
check_docker() {
if ! command -v docker &> /dev/null; then
log_error "Docker 未安装"
exit 1
fi
if ! docker info &> /dev/null; then
log_error "Docker 未运行或无权限"
exit 1
fi
log_ok "Docker 检查通过"
}
ensure_openclaw_src() {
if [[ -d "${OPENCLAW_SRC}" ]]; then
log_ok "OpenClaw 源码已存在: ${OPENCLAW_SRC}"
return
fi
if [[ "$OFFLINE" == "true" ]]; then
log_error "离线模式下无法克隆 OpenClaw 源码: ${OPENCLAW_SRC} 不存在"
log_error "请手动将 OpenClaw 源码放置到 ${OPENCLAW_SRC},或取消 --offline"
exit 1
fi
log_info "克隆 OpenClaw 源码到 ${OPENCLAW_SRC} ..."
git clone --depth 1 "${OPENCLAW_GIT_URL}" "${OPENCLAW_SRC}"
log_ok "OpenClaw 源码克隆完成: ${OPENCLAW_SRC}"
}
ensure_hermes_src() {
if [[ -d "${HERMES_SRC}" ]]; then
log_ok "Hermes 源码已存在: ${HERMES_SRC}"
return
fi
if [[ "$OFFLINE" == "true" ]]; then
log_warn "离线模式下无法克隆 Hermes 源码: ${HERMES_SRC} 不存在,跳过"
return
fi
log_info "克隆 Hermes 源码到 ${HERMES_SRC} ..."
git clone --depth 1 "${HERMES_GIT_URL}" "${HERMES_SRC}"
log_ok "Hermes 源码克隆完成: ${HERMES_SRC}"
}
ensure_plugins_src() {
if [[ -d "${PLUGINS_SRC}" ]]; then
log_ok "插件目录已存在: ${PLUGINS_SRC}"
return
fi
local src_plugins="${SCRIPT_DIR}/../plugins"
if [[ -d "${src_plugins}" ]]; then
log_info "从 ${src_plugins} 复制插件到 ${PLUGINS_SRC} ..."
cp -r "${src_plugins}" "${PLUGINS_SRC}"
log_ok "插件复制完成: ${PLUGINS_SRC}"
return
fi
log_warn "插件目录不存在: ${PLUGINS_SRC},且无本地源可复制(${src_plugins} 也不存在)"
}
ensure_bun() {
if [[ ":$PATH:" != *":$HOME/.bun/bin:"* ]]; then
export PATH="$HOME/.bun/bin:$PATH"
fi
if command -v bun &> /dev/null; then
return
fi
log_info "安装 bun..."
curl -fsSL https://bun.sh/install | bash -s -- >&2
}
cleanup_old_plugins() {
if [[ -d "claude-mem-dist" ]]; then
log_info "移除旧的 claude-mem-dist 目录(已不再使用 claude-mem 插件)"
rm -rf claude-mem-dist
fi
if [[ -d "claude-code-best-dist" ]]; then
log_info "移除旧的 claude-code-best-dist 目录(已不再使用 ccb 插件)"
rm -rf claude-code-best-dist
fi
}
prepare_openclaw_monorepo() {
local src_dir="${OPENCLAW_SRC}"
if [[ ! -d "${src_dir}" ]]; then
log_error "openclaw 源码目录不存在: ${src_dir}"
exit 1
fi
if [[ -f "${src_dir}/packages/plugin-sdk/dist/src/plugin-sdk/index.d.ts" ]]; then
log_info "openclaw monorepo 已构建,跳过"
return
fi
log_info "=== 构建 openclaw monorepo ==="
cd "${src_dir}"
pnpm install --frozen-lockfile 2>&1 | tail -10
pnpm build 2>&1 | tail -15
cd "${SCRIPT_DIR}"
log_ok "openclaw monorepo 构建完成"
}
build_app_image() {
if [[ "$SKIP_APP" == "true" ]]; then
log_warn "跳过 Layer 2 构建(--skip-app)"
return
fi
if ! docker image inspect "${BASE_IMAGE}" &> /dev/null; then
log_error "Layer 1 镜像不存在: ${BASE_IMAGE}"
exit 1
fi
if [[ ! -d "${OPENCLAW_SRC}" ]]; then
log_error "OpenClaw 源码目录不存在: ${OPENCLAW_SRC}"
log_error "请先运行 ensure_openclaw_src 或手动将源码放置到 ${OPENCLAW_SRC}"
exit 1
fi
log_info "=== 构建 Layer 2: openclaw-app ==="
log_info "构建上下文: ${OPENCLAW_SRC}"
local npmmirror="${DOCKER_REGISTRY_NPM:-https://registry.npmmirror.com}"
local app_build_dir="${SCRIPT_DIR}/.app-build-tmp"
rm -rf "${app_build_dir}"
mkdir -p "${app_build_dir}"
rsync -a --exclude='node_modules' "${OPENCLAW_SRC}/" "${app_build_dir}/"
log_info "源码已复制到临时构建目录(排除 node_modules): ${app_build_dir}"
local arch_placeholder_name="matrix-sdk-crypto.linux-$(node -p 'process.arch')-gnu.node.placeholder"
touch "${app_build_dir}/${arch_placeholder_name}"
local crypto_pkg="${OPENCLAW_SRC}/node_modules/@matrix-org/matrix-sdk-crypto-nodejs"
if [[ -d "${crypto_pkg}" ]] && [[ -f "${crypto_pkg}/package.json" ]]; then
local crypto_version=$(node -p "require('${crypto_pkg}/package.json').version" 2>/dev/null || echo "")
if [[ -n "${crypto_version}" ]]; then
local arch_suffix
arch_suffix="linux-$(node -p 'process.arch')-gnu"
local crypto_file="${app_build_dir}/matrix-sdk-crypto.${arch_suffix}.node"
if [[ ! -f "${crypto_file}" ]]; then
local crypto_url="https://github.com/matrix-org/matrix-rust-sdk-crypto-nodejs/releases/download/v${crypto_version}/matrix-sdk-crypto.${arch_suffix}.node"
log_info "预下载 matrix-sdk-crypto v${crypto_version} (${arch_suffix}) ..."
curl -fsSL -o "${crypto_file}" "${crypto_url}" 2>/dev/null || {
log_warn "预下载失败(Matrix 聊天功能将不可用)"
rm -f "${crypto_file}"
}
fi
fi
else
log_warn "宿主机无 matrix-sdk-crypto-nodejs 包,跳过预下载"
fi
docker build \
-t "${APP_IMAGE}" \
-f Dockerfile.openclaw-app \
--build-arg OPENCLAW_BASE_IMAGE=${BASE_IMAGE} \
--build-arg OPENCLAW_VERSION=${VERSION} \
--build-arg NPM_REGISTRY=${npmmirror} \
${DOCKER_BUILD_OPTS} \
"${app_build_dir}"
rm -rf "${app_build_dir}"
log_ok "Layer 2 构建完成: ${APP_IMAGE}"
}
prepare_subagent_coordinator() {
local dist_dst="${SCRIPT_DIR}/subagent-coordinator-dist"
if [[ -d "${dist_dst}" ]]; then
log_info "subagent-coordinator-dist 已存在,跳过"
return
fi
log_info "=== 准备 subagent-coordinator 插件 ==="
local plugin_src="${PLUGINS_SRC}/subagent-coordinator"
if [[ ! -d "${plugin_src}" ]]; then
plugin_src="${SCRIPT_DIR}/../plugins/subagent-coordinator"
fi
if [[ ! -d "${plugin_src}" ]]; then
log_error "subagent-coordinator 插件源码不存在(尝试过 ${PLUGINS_SRC}/subagent-coordinator 和 ${SCRIPT_DIR}/../plugins/subagent-coordinator)"
exit 1
fi
local install_script="${plugin_src}/install_to_image.sh"
if [[ ! -f "$install_script" ]]; then
log_error "install_to_image.sh 不存在: $install_script"
exit 1
fi
local abs_plugin_sdk_src
abs_plugin_sdk_src="$(cd "${SCRIPT_DIR}" && cd "${OPENCLAW_SRC}" 2>/dev/null && pwd)/packages/plugin-sdk" || abs_plugin_sdk_src="${OPENCLAW_SRC}/packages/plugin-sdk"
PLUGIN_SDK_SRC="${abs_plugin_sdk_src}" \
OPENCLAW_VERSION="${VERSION}" \
OPENCLAW_SRC="${OPENCLAW_SRC}" \
bash "$install_script" || {
log_error "subagent-coordinator 插件准备失败"
exit 1
}
cp -r "${plugin_src}/dist/." "${dist_dst}/"
log_ok "subagent-coordinator 准备完成"
}
build_base_image() {
if [[ "$SKIP_BASE" == "true" ]]; then
log_warn "跳过 Layer 1 构建(--skip-base)"
return
fi
log_info "=== 构建 Layer 1: openclaw-base ==="
docker build \
-t "${BASE_IMAGE}" \
-f Dockerfile.openclaw-base \
${DOCKER_BUILD_OPTS} \
.
log_ok "Layer 1 构建完成: ${BASE_IMAGE}"
}
build_overlay_image() {
if [[ "$SKIP_OVERLAY" == "true" ]]; then
log_warn "跳过 Layer 3 构建(--skip-overlay)"
return
fi
if ! docker image inspect "${APP_IMAGE}" &> /dev/null; then
log_error "Layer 2 镜像不存在: ${APP_IMAGE}"
exit 1
fi
log_info "=== 构建 Layer 3: openclaw-overlay ==="
cleanup_old_plugins
local overlay_build_dir="${SCRIPT_DIR}/.overlay-build-tmp"
rm -rf "${overlay_build_dir}"
mkdir -p "${overlay_build_dir}"
cp "${SCRIPT_DIR}/Dockerfile.openclaw-overlay" "${overlay_build_dir}/"
cp -r "${SCRIPT_DIR}/subagent-coordinator-dist" "${overlay_build_dir}/" 2>/dev/null || true
if [[ -d "${HERMES_SRC}" ]]; then
cp -r "${HERMES_SRC}" "${overlay_build_dir}/hermes-src"
log_info "Hermes 源码已复制到构建上下文"
else
mkdir -p "${overlay_build_dir}/hermes-src"
log_warn "Hermes 源码不存在,overlay 层将跳过 Hermes 安装"
fi
local mindclaw_skills_src="${SCRIPT_DIR}/../skills"
local mindclaw_skills_count
mindclaw_skills_count=$(ls -1 "${mindclaw_skills_src}/" 2>/dev/null | wc -l | tr -d ' ')
mkdir -p "${overlay_build_dir}/skills"
cp -r "${mindclaw_skills_src}/." "${overlay_build_dir}/skills/"
log_info "mindclaw skills: ${mindclaw_skills_count} 项已复制到构建上下文(将打包到 skills-shared)"
local overlay_rel_dir=".overlay-build-tmp"
docker build \
-t "${FINAL_IMAGE}" \
-f "${overlay_rel_dir}/Dockerfile.openclaw-overlay" \
--build-arg OPENCLAW_APP_IMAGE=${APP_IMAGE} \
${DOCKER_BUILD_OPTS} \
"${overlay_rel_dir}"
rm -rf "${overlay_build_dir}"
log_ok "Layer 3 构建完成: ${FINAL_IMAGE}"
}
show_summary() {
echo ""
echo "=============================================="
log_ok "全部构建完成!"
echo "=============================================="
echo ""
echo "镜像标签:"
echo " Base: ${BASE_IMAGE}"
echo " App: ${APP_IMAGE}"
echo " Final: ${FINAL_IMAGE}"
echo ""
echo "启动示例:"
echo " docker run -d --name openclaw-test \\"
echo " -p 18080:18080 -p 18081:18081 \\"
echo " ${FINAL_IMAGE}"
}
main() {
log_info "OpenClaw 构建脚本(离线模式: ${OFFLINE})"
echo ""
check_docker
if [[ "$SKIP_APP" != "true" ]] || [[ "$SKIP_OVERLAY" != "true" ]]; then
ensure_openclaw_src
fi
if [[ "$SKIP_OVERLAY" != "true" ]]; then
ensure_hermes_src
ensure_plugins_src
fi
if [[ "$SKIP_OVERLAY" != "true" ]]; then
prepare_openclaw_monorepo
prepare_subagent_coordinator
fi
build_base_image
if [[ "$SKIP_APP" != "true" ]]; then
build_app_image
fi
if [[ "$SKIP_OVERLAY" != "true" ]]; then
build_overlay_image
fi
show_summary
}
main "$@"