# =============================================================================
# OpenClaw App Image - Layer 2(继承 Layer 1 + 执行官方构建)
# =============================================================================
# 功能:
#   - 继承 Layer 1 (openclaw-base) 的基础设施
#   - 执行 OpenClaw 源码构建(pnpm install + build)
#
# 构建参数:
#   OPENCLAW_VERSION        → OpenClaw 版本
#   OPENCLAW_EXTENSIONS     → 扩展插件列表(空格分隔)
#   NPM_REGISTRY           → npm 镜像地址
#
# 构建上下文:
#   必须在 OpenClaw 源码根目录执行(即包含 packages/ 和 extensions/ 的目录),
#   --mount=type=bind 的 source 路径(packages、${OPENCLAW_BUNDLED_PLUGIN_DIR})
#   均相对于构建上下文解析,从 docker/ 目录构建会导致这些路径解析失败。
#
# 构建顺序:
#   1. cd <openclaw-src> && docker build -t openclaw:base-X.Y.Z -f docker/Dockerfile.openclaw-base .
#   2. docker build -t openclaw:app-X.Y.Z -f docker/Dockerfile.openclaw-app . --build-arg OPENCLAW_VERSION=X.Y.Z
#   3. docker build -t openclaw:X.Y.Z -f docker/Dockerfile.openclaw-overlay docker/
# =============================================================================

ARG OPENCLAW_BASE_IMAGE=openclaw:base-2026.5.22
FROM ${OPENCLAW_BASE_IMAGE}

LABEL org.opencontainers.image.title="OpenClaw App"
LABEL org.opencontainers.image.description="OpenClaw with build tools"
LABEL org.openclaw.layer="app"
LABEL org.openclaw.version="${OPENCLAW_VERSION:-2026.5.22}"

ARG OPENCLAW_EXTENSIONS=""
ARG OPENCLAW_BUNDLED_PLUGIN_DIR=extensions

FROM ${OPENCLAW_BASE_IMAGE} AS workspace-deps
ARG OPENCLAW_EXTENSIONS
ARG OPENCLAW_BUNDLED_PLUGIN_DIR=extensions
USER root
# Copy package.json files for workspace packages used by the install layer.
RUN --mount=type=bind,source=packages,target=/tmp/packages,readonly \
    --mount=type=bind,source=${OPENCLAW_BUNDLED_PLUGIN_DIR},target=/tmp/${OPENCLAW_BUNDLED_PLUGIN_DIR},readonly \
    mkdir -p /out/packages "/out/${OPENCLAW_BUNDLED_PLUGIN_DIR}" && \
    for manifest in /tmp/packages/*/package.json; do \
      [ -f "$manifest" ] || continue; \
      pkg_dir="${manifest%/package.json}"; \
      pkg_name="${pkg_dir##*/}"; \
      mkdir -p "/out/packages/$pkg_name" && \
      cp "$manifest" "/out/packages/$pkg_name/package.json"; \
    done && \
    for ext in $(printf '%s\n' "$OPENCLAW_EXTENSIONS" | tr ',' ' '); do \
      if [ -f "/tmp/${OPENCLAW_BUNDLED_PLUGIN_DIR}/$ext/package.json" ]; then \
        mkdir -p "/out/${OPENCLAW_BUNDLED_PLUGIN_DIR}/$ext" && \
        cp "/tmp/${OPENCLAW_BUNDLED_PLUGIN_DIR}/$ext/package.json" "/out/${OPENCLAW_BUNDLED_PLUGIN_DIR}/$ext/package.json"; \
      fi; \
    done

FROM ${OPENCLAW_BASE_IMAGE} AS build
ARG OPENCLAW_BUNDLED_PLUGIN_DIR=extensions
USER root

# base 的 /root/.npmrc 预设了 registry=https://registry.npmmirror.com,此处覆盖为官方源
ARG NPM_REGISTRY="https://registry.npmjs.org/"
RUN npm config set registry ${NPM_REGISTRY}

# pnpm 超时/重试配置(大文件下载易超时)
RUN pnpm config set fetch-timeout 300000 && \
    pnpm config set fetch-retries 5 && \
    pnpm config set fetch-retry-mintimeout 20000 && \
    pnpm config set fetch-retry-maxtimeout 120000

WORKDIR /app

COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./
COPY openclaw.mjs ./
COPY ui/package.json ./ui/package.json
COPY patches ./patches
COPY scripts/postinstall-bundled-plugins.mjs scripts/preinstall-package-manager-warning.mjs scripts/npm-runner.mjs scripts/windows-cmd-helpers.mjs ./scripts/
COPY scripts/lib/package-dist-imports.mjs ./scripts/lib/package-dist-imports.mjs

COPY --from=workspace-deps /out/packages/ ./packages/
COPY --from=workspace-deps /out/${OPENCLAW_BUNDLED_PLUGIN_DIR}/ ./${OPENCLAW_BUNDLED_PLUGIN_DIR}/

# pnpm install 触发 prepare 生命周期,Docker 构建中 git hooks 无意义,创建空脚本绕过
RUN echo 'process.exit(0)' > /app/scripts/prepare-git-hooks.mjs

RUN --mount=type=cache,id=openclaw-pnpm-store,target=/root/.local/share/pnpm/store,sharing=locked \
    NODE_OPTIONS=--max-old-space-size=2048 \
    pnpm install --frozen-lockfile \
      --config.supportedArchitectures.os=linux \
      --config.supportedArchitectures.cpu="$(node -p 'process.arch')" \
      --config.supportedArchitectures.libc=glibc

RUN set -eux; \
    echo "==> Verifying critical native addons..."; \
    for attempt in 1 2 3 4 5; do \
      if find /app/node_modules -name "matrix-sdk-crypto*.node" 2>/dev/null | grep -q .; then \
        exit 0; \
      fi; \
      echo "matrix-sdk-crypto native addon missing; retrying download (${attempt}/5)"; \
      node /app/node_modules/@matrix-org/matrix-sdk-crypto-nodejs/download-lib.js || true; \
      sleep $((attempt * 2)); \
    done; \
    find /app/node_modules -name "matrix-sdk-crypto*.node" 2>/dev/null | grep -q . || \
      (echo "ERROR: matrix-sdk-crypto native addon missing after retries" >&2 && exit 1)

COPY . .

# 修复二进制/脚本可执行权限(构建上下文已排除 node_modules,无需备份)
RUN find /app/node_modules/.bin -type f -exec chmod +x {} + 2>/dev/null || true; \
    find /app/node_modules/.bin -type l -exec chmod +x {} + 2>/dev/null || true; \
    find /app/node_modules -type f -name 'esbuild' -exec chmod +x {} + 2>/dev/null || true; \
    find /app/node_modules -type f -name '*.node' -exec chmod +x {} + 2>/dev/null || true;

# 规范化扩展目录权限,避免运行时 COPY 添加额外层
RUN for dir in /app/${OPENCLAW_BUNDLED_PLUGIN_DIR} /app/.agent /app/.agents; do \
      if [ -d "$dir" ]; then \
        find "$dir" -type d -exec chmod 755 {} +; \
        find "$dir" -type f -exec chmod 644 {} +; \
      fi; \
    done

RUN pnpm_config_verify_deps_before_run=false pnpm canvas:a2ui:bundle || \
    (echo "A2UI bundle: creating stub (non-fatal)" && \
     mkdir -p extensions/canvas/src/host/a2ui && \
     echo "/* A2UI bundle unavailable in this build */" > extensions/canvas/src/host/a2ui/a2ui.bundle.js && \
     echo "stub" > extensions/canvas/src/host/a2ui/.bundle.hash && \
     rm -rf vendor/a2ui apps/shared/OpenClawKit/Tools/CanvasA2UI)

# build:docker 前修复可执行权限(canvas:a2ui:bundle 可能丢失 +x)
RUN find /app/node_modules -path '*/node_modules/.bin/*' -type f -exec chmod +x {} + 2>/dev/null; \
    find /app/node_modules -path '*/node_modules/.bin/*' -type l -exec chmod +x {} + 2>/dev/null || true; \
    find /app/node_modules -type f -name 'esbuild' -exec chmod +x {} + 2>/dev/null || true; \
    find /app/node_modules -type f -name '*.node' -exec chmod +x {} + 2>/dev/null || true; \
    NODE_OPTIONS=--max-old-space-size=8192 pnpm_config_verify_deps_before_run=false pnpm build:docker

ENV OPENCLAW_PREFER_PNPM=1
RUN pnpm_config_verify_deps_before_run=false pnpm ui:build

RUN pnpm_config_verify_deps_before_run=false pnpm qa:lab:build

FROM build AS runtime-assets
ARG OPENCLAW_EXTENSIONS
ARG OPENCLAW_BUNDLED_PLUGIN_DIR=extensions
# 跳过 pnpm prune --prod(离线环境下不可靠),直接使用完整 node_modules
RUN echo "cache-bust: skip-pnpm-prune 20260609" && \
    OPENCLAW_EXTENSIONS="$OPENCLAW_EXTENSIONS" OPENCLAW_BUNDLED_PLUGIN_DIR="$OPENCLAW_BUNDLED_PLUGIN_DIR" node scripts/prune-docker-plugin-dist.mjs && \
    node scripts/postinstall-bundled-plugins.mjs && \
    find dist -type f \( -name '*.d.ts' -o -name '*.d.mts' -o -name '*.d.cts' -o -name '*.map' \) -delete && \
    node scripts/check-package-dist-imports.mjs /app

FROM ${OPENCLAW_BASE_IMAGE} AS base-runtime
USER root

LABEL org.opencontainers.image.base.name="openclaw-base" \
  org.opencontainers.image.description="Based on openclaw-base with Playwright/bun/pnpm/system tools"

FROM base-runtime
ARG OPENCLAW_BUNDLED_PLUGIN_DIR=extensions
USER root

# OCI base-image metadata for downstream image consumers.
LABEL org.opencontainers.image.source="https://github.com/openclaw/openclaw" \
  org.opencontainers.image.url="https://openclaw.ai" \
  org.opencontainers.image.documentation="https://docs.openclaw.ai/install/docker" \
  org.opencontainers.image.licenses="MIT" \
  org.opencontainers.image.title="OpenClaw" \
  org.opencontainers.image.description="OpenClaw gateway and CLI runtime container image"

WORKDIR /app

# openclaw-base 已包含大部分系统包,仅安装缺失的 tini 和 ca-certificates
RUN --mount=type=cache,id=openclaw-bookworm-apt-cache,target=/var/cache/apt,sharing=locked \
    --mount=type=cache,id=openclaw-bookworm-apt-lists,target=/var/lib/apt,sharing=locked \
    mkdir -p /var/lib/apt/lists/partial && \
    apt-get update && \
    DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
      ca-certificates tini && \
    update-ca-certificates

RUN chown node:node /app

COPY --from=runtime-assets --chown=node:node /app/dist ./dist
COPY --from=build --chown=node:node /app/node_modules ./node_modules
COPY --from=build --chown=node:node /app/package.json .
COPY --from=build --chown=node:node /app/pnpm-workspace.yaml .
COPY --from=build --chown=node:node /app/patches ./patches
COPY --from=build --chown=node:node /app/openclaw.mjs .
COPY --from=build --chown=node:node /app/${OPENCLAW_BUNDLED_PLUGIN_DIR} ./${OPENCLAW_BUNDLED_PLUGIN_DIR}
COPY --from=build --chown=node:node /app/skills ./skills
COPY --from=build --chown=node:node /app/docs ./docs
COPY --from=build --chown=node:node /app/qa ./qa

# 复制 workspace 模板到 /app/src/agents/templates/(gateway 运行时需要)
RUN mkdir -p /app/src/agents/templates && \
    if [ -d /app/node_modules/openclaw/src/agents/templates ]; then \
      cp /app/node_modules/openclaw/src/agents/templates/* /app/src/agents/templates/ 2>/dev/null || true; \
    fi && \
    if [ -d /app/node_modules/openclaw/docs/reference/templates ]; then \
      cp -n /app/node_modules/openclaw/docs/reference/templates/* /app/src/agents/templates/ 2>/dev/null || true; \
    fi && \
    if [ -d /app/docs/reference/templates ]; then \
      cp -n /app/docs/reference/templates/* /app/src/agents/templates/ 2>/dev/null || true; \
    fi && \
    chown -R node:node /app/src

# 使用 corepack 确保 pnpm 版本与 packageManager 字段一致
ENV COREPACK_HOME=/usr/local/share/corepack
RUN install -d -m 0755 "$COREPACK_HOME" && \
    corepack enable && \
    for attempt in 1 2 3 4 5; do \
      if corepack prepare "$(node -p "require('./package.json').packageManager")" --activate; then \
        break; \
      fi; \
      if [ "$attempt" -eq 5 ]; then \
        exit 1; \
      fi; \
      sleep $((attempt * 2)); \
    done && \
    chmod -R a+rX "$COREPACK_HOME"

# 可选 APT 包安装
ARG OPENCLAW_IMAGE_APT_PACKAGES
ARG OPENCLAW_DOCKER_APT_PACKAGES=""
RUN --mount=type=cache,id=openclaw-bookworm-apt-cache,target=/var/cache/apt,sharing=locked \
    --mount=type=cache,id=openclaw-bookworm-apt-lists,target=/var/lib/apt,sharing=locked \
    packages="${OPENCLAW_IMAGE_APT_PACKAGES-$OPENCLAW_DOCKER_APT_PACKAGES}"; \
    if [ -n "$packages" ]; then \
      mkdir -p /var/lib/apt/lists/partial && \
      apt-get update && \
      DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends $packages; \
    fi

# 可选 PIP 包安装(openclaw-base 已包含基础 pip 包)
ARG OPENCLAW_IMAGE_PIP_PACKAGES=""
RUN --mount=type=cache,id=openclaw-bookworm-apt-cache,target=/var/cache/apt,sharing=locked \
    --mount=type=cache,id=openclaw-bookworm-apt-lists,target=/var/lib/apt,sharing=locked \
    if [ -n "$OPENCLAW_IMAGE_PIP_PACKAGES" ]; then \
      if ! python3 -m pip --version >/dev/null 2>&1; then \
        mkdir -p /var/lib/apt/lists/partial && \
        apt-get update && \
        DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends python3-pip; \
      fi && \
      python3 -m pip install --no-cache-dir --break-system-packages $OPENCLAW_IMAGE_PIP_PACKAGES; \
    fi

ENV PLAYWRIGHT_BROWSERS_PATH=/home/node/.cache/ms-playwright

# 可选 Docker CLI 安装(sandbox 容器管理需要,增加约 50MB)
ARG OPENCLAW_INSTALL_DOCKER_CLI=""
ARG OPENCLAW_DOCKER_GPG_FINGERPRINT="9DC858229FC7DD38854AE2D88D81803C0EBFCD88"
RUN --mount=type=cache,id=openclaw-bookworm-apt-cache,target=/var/cache/apt,sharing=locked \
    --mount=type=cache,id=openclaw-bookworm-apt-lists,target=/var/lib/apt,sharing=locked \
    if [ -n "$OPENCLAW_INSTALL_DOCKER_CLI" ]; then \
      mkdir -p /var/lib/apt/lists/partial && \
      apt-get update && \
      DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
        ca-certificates curl gnupg && \
      install -m 0755 -d /etc/apt/keyrings && \
      curl -fsSL https://download.docker.com/linux/debian/gpg -o /tmp/docker.gpg.asc && \
      expected_fingerprint="$(printf '%s' "$OPENCLAW_DOCKER_GPG_FINGERPRINT" | tr '[:lower:]' '[:upper:]' | tr -d '[:space:]')" && \
      docker_gpg_pub_count="$(gpg --batch --show-keys --with-colons /tmp/docker.gpg.asc | awk -F: '$1 == "pub" { c++ } END { print c+0 }')" && \
      if [ "$docker_gpg_pub_count" != "1" ]; then \
        echo "ERROR: Docker apt key must contain exactly one public key (found $docker_gpg_pub_count); refusing a multi-key file." >&2; \
        exit 1; \
      fi && \
      actual_fingerprint="$(gpg --batch --show-keys --with-colons /tmp/docker.gpg.asc | awk -F: 'BEGIN{f="f";p="p";r="r";t=sprintf("%s%s%s",f,p,r)}$1==t{print toupper($10);exit}')" && \
      if [ -z "$actual_fingerprint" ] || [ "$actual_fingerprint" != "$expected_fingerprint" ]; then \
        echo "ERROR: Docker apt key fingerprint mismatch (expected $expected_fingerprint, got ${actual_fingerprint:-<empty>})" >&2; \
        exit 1; \
      fi && \
      gpg --dearmor -o /etc/apt/keyrings/docker.gpg /tmp/docker.gpg.asc && \
      rm -f /tmp/docker.gpg.asc && \
      chmod a+r /etc/apt/keyrings/docker.gpg && \
      printf 'deb [arch=%s signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian bookworm stable\n' \
        "$(dpkg --print-architecture)" > /etc/apt/sources.list.d/docker.list && \
      apt-get update && \
      DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
        docker-ce-cli docker-compose-plugin; \
    fi

RUN ln -sf /app/openclaw.mjs /usr/local/bin/openclaw \
 && chmod 755 /app/openclaw.mjs

# 预创建 volume 挂载点,确保目录属主为 node
RUN install -d -m 0700 -o node -g node \
      /home/node/.openclaw \
      /home/node/.openclaw/workspace \
      /home/node/.config/openclaw && \
    stat -c '%U:%G %a' /home/node/.openclaw | grep -qx 'node:node 700' && \
    stat -c '%U:%G %a' /home/node/.openclaw/workspace | grep -qx 'node:node 700' && \
    stat -c '%U:%G %a' /home/node/.config/openclaw | grep -qx 'node:node 700'

ENV NODE_ENV=production

USER node

# 网关入口:绑定 127.0.0.1:18789,需 --network host 或覆盖 --bind lan 方可从外部访问
HEALTHCHECK --interval=3m --timeout=10s --start-period=15s --retries=3 \
  CMD node -e "fetch('http://127.0.0.1:18789/healthz').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
ENTRYPOINT ["tini", "-s", "--"]
CMD ["node", "openclaw.mjs", "gateway"]