| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| FROM node:22-slim |
|
|
| |
| |
| |
| RUN apt-get update && apt-get install -y --no-install-recommends \ |
| git openssh-client build-essential python3 python3-pip \ |
| g++ make ca-certificates curl jq \ |
| && rm -rf /var/lib/apt/lists/* |
|
|
| |
| |
| |
| RUN pip3 install --no-cache-dir huggingface_hub --break-system-packages |
|
|
| |
| |
| |
| RUN update-ca-certificates && \ |
| git config --global http.sslVerify true && \ |
| git config --global url."https://github.com/".insteadOf ssh://git@github.com/ |
|
|
| |
| |
| |
| RUN npm install -g openclaw@latest --unsafe-perm |
|
|
| |
| |
| |
| |
| ENV PORT=7860 \ |
| OPENCLAW_GATEWAY_MODE=local \ |
| HOME=/root |
|
|
| |
| |
| |
| RUN cat > /usr/local/bin/sync.py <<'PY' |
| |
| |
|
|
| import os, sys, tarfile |
| from huggingface_hub import HfApi, hf_hub_download, login |
| from datetime import datetime, timedelta |
|
|
| api = HfApi() |
| repo_id = os.getenv("HF_DATASET") |
| token = os.getenv("HF_TOKEN") |
|
|
| BASE_DIR = "/root/.openclaw" |
| CONFIG_PATH = os.path.join(BASE_DIR, "openclaw.json") |
|
|
| def is_safe_arc(arcpath: str) -> bool: |
| |
| return not (arcpath.startswith("/") or arcpath.startswith("..") or "/../" in arcpath) |
|
|
| def safe_extract(tar: tarfile.TarFile, path: str) -> None: |
| """ |
| 安全解包:确保每个成员最终落在目标目录下,避免 tar 路径穿越。 |
| """ |
| base = os.path.realpath(path) |
| for m in tar.getmembers(): |
| |
| if m.issym() or m.islnk() or m.ischr() or m.isblk() or m.isfifo(): |
| continue |
|
|
| name = m.name or "" |
| |
| if not is_safe_arc(name): |
| continue |
|
|
| dest = os.path.realpath(os.path.join(base, name)) |
| if dest != base and not dest.startswith(base + os.sep): |
| |
| continue |
|
|
| tar.extract(m, path=base) |
|
|
| def restore() -> bool: |
| try: |
| if not repo_id or not token: |
| print("[SYNC] 跳过恢复: 未配置 HF_DATASET 或 HF_TOKEN") |
| return True |
|
|
| try: |
| login(token=token) |
| except Exception: |
| pass |
|
|
| files = api.list_repo_files(repo_id=repo_id, repo_type="dataset", token=token) |
| now = datetime.now() |
|
|
| for i in range(5): |
| name = f"backup_{(now - timedelta(days=i)).strftime('%Y-%m-%d')}.tar.gz" |
| if name in files: |
| print(f"[SYNC] 下载并恢复: {name}") |
| path = hf_hub_download(repo_id=repo_id, filename=name, repo_type="dataset", token=token) |
| with tarfile.open(path, "r:gz") as tar: |
| safe_extract(tar, BASE_DIR) |
| print("[SYNC] 恢复成功") |
| return True |
|
|
| print("[SYNC] 未找到最近 5 天备份(首次部署/无备份属正常情况)") |
| return True |
|
|
| except Exception as e: |
| print(f"[SYNC] 恢复异常: {e}") |
| return False |
|
|
| def backup() -> bool: |
| try: |
| if not repo_id or not token: |
| print("[SYNC] 跳过备份: 未配置 HF_DATASET 或 HF_TOKEN") |
| return True |
|
|
| try: |
| login(token=token) |
| except Exception: |
| pass |
|
|
| day = datetime.now().strftime("%Y-%m-%d") |
| out_path = f"/tmp/backup_{day}.tar.gz" |
| print(f"[SYNC] 正在备份: {out_path}") |
|
|
| |
| targets = ["sessions", "workspace", "agents", "cron", "identity", "devices"] |
|
|
| with tarfile.open(out_path, "w:gz") as tar: |
| for t in targets: |
| full = os.path.join(BASE_DIR, t) |
| if not os.path.exists(full): |
| continue |
| for root, dirs, files in os.walk(full): |
| |
| dirs[:] = [d for d in dirs if d != "credentials"] |
| for f in files: |
| fp = os.path.join(root, f) |
| arc = os.path.relpath(fp, BASE_DIR) |
| if is_safe_arc(arc): |
| tar.add(fp, arcname=arc) |
|
|
| |
| if os.path.exists(CONFIG_PATH): |
| tar.add(CONFIG_PATH, arcname="openclaw.json") |
|
|
| print(f"[SYNC] 上传备份到 dataset: {repo_id}") |
| api.upload_file( |
| path_or_fileobj=out_path, |
| path_in_repo=f"backup_{day}.tar.gz", |
| repo_id=repo_id, |
| repo_type="dataset", |
| token=token |
| ) |
| print("[SYNC] 备份上传成功") |
| return True |
|
|
| except Exception as e: |
| print(f"[SYNC] 备份失败: {e}") |
| return False |
|
|
| if __name__ == "__main__": |
| cmd = sys.argv[1] if len(sys.argv) > 1 else "restore" |
| if cmd == "backup": |
| sys.exit(0 if backup() else 1) |
| else: |
| sys.exit(0 if restore() else 1) |
| PY |
| RUN chmod +x /usr/local/bin/sync.py |
|
|
| |
| |
| |
| |
| |
| |
| RUN cat > /usr/local/bin/generate-config.sh <<'BASH' |
| |
| generate_config() { |
| OUT="${1:-/root/.openclaw/openclaw.json}" |
|
|
| GATEWAY_PORT=${PORT:-7860} |
|
|
| |
| PROVIDER=${PROVIDER:-"nvidia"} |
| MODEL=${MODEL:-"moonshotai/kimi-k2.5"} |
|
|
| |
| NVIDIA_BASE=${OPENAI_API_BASE:-"https://integrate.api.nvidia.com/v1"} |
|
|
| |
| NVIDIA_KEY=${OPENAI_API_KEY:-""} |
| OR_KEY=${OPENROUTER_API_KEY:-""} |
| VERCEL_KEY=${VERCEL_AI_GATEWAY_API_KEY:-""} |
|
|
| |
| GATEWAY_PASSWORD=${OPENCLAW_GATEWAY_PASSWORD:-"default_change_me"} |
|
|
| cat > "$OUT" << EOF |
| { |
| "models": { |
| "providers": { |
| "nvidia": { |
| "baseUrl": "${NVIDIA_BASE}", |
| "apiKey": "${NVIDIA_KEY}", |
| "api": "openai-completions", |
| "models": [ |
| { "id": "z-ai/glm4.7", "name": "GLM", "contextWindow": 131072, "reasoning": false, "input": ["text"], "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 }, "maxTokens": 131072 }, |
| { "id": "deepseek-ai/deepseek-v3.2", "name": "DeepSeek V3.2", "contextWindow": 128000, "reasoning": false, "input": ["text"], "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 }, "maxTokens": 8192 }, |
| { "id": "minimaxai/minimax-m2.1", "name": "MiniMax M2.1", "contextWindow": 128000, "reasoning": false, "input": ["text"], "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 }, "maxTokens": 8192 }, |
| { "id": "moonshotai/kimi-k2.5", "name": "Kimi K2.5", "contextWindow": 262144, "reasoning": false, "input": ["text"], "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 }, "maxTokens": 8192 }, |
| { "id": "qwen/qwen3-coder-480b-a35b-instruct", "name": "Qwen3 Coder 480B", "contextWindow": 262144, "reasoning": false, "input": ["text"], "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 }, "maxTokens": 8192 } |
| ] |
| }, |
| "openrouterai": { |
| "baseUrl": "https://openrouter.ai/api/v1", |
| "apiKey": "${OR_KEY}", |
| "api": "openai-completions", |
| "models": [ |
| { "id": "openrouter/pony-alpha", "name": "pony alpha", "contextWindow": 200000, "reasoning": false, "input": ["text"], "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 }, "maxTokens": 131000 }, |
| { "id": "arcee-ai/trinity-large-preview:free", "name": "trinity large preview free", "contextWindow": 131000, "reasoning": false, "input": ["text"], "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 }, "maxTokens": 8192 } |
| ] |
| }, |
| "vercel": { |
| "baseUrl": "https://ai-gateway.vercel.sh/v1", |
| "apiKey": "${VERCEL_KEY}", |
| "api": "openai-completions", |
| "models": [ |
| { "id": "kwaipilot/kat-coder-pro-v1", "name": "kat coder pro v1", "contextWindow": 256000, "reasoning": false, "input": ["text"], "cost": { "input": 0.03, "output": 1.20, "cacheRead": 0.06, "cacheWrite": 0 }, "maxTokens": 32000 } |
| ] |
| } |
| } |
| }, |
| "agents": { |
| "defaults": { |
| "model": { "primary": "${PROVIDER}/${MODEL}" }, |
| "maxConcurrent": 4, |
| "subagents": { "maxConcurrent": 8 } |
| } |
| }, |
| "commands": { "native": "auto", "nativeSkills": "auto", "restart": true }, |
| "messages": { "ackReactionScope": "group-mentions" }, |
| "gateway": { |
| "port": ${GATEWAY_PORT}, |
| "mode": "local", |
| "bind": "lan", |
| "trustedProxies": ["0.0.0.0/0", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"], |
| "auth": { "mode": "token", "token": "${GATEWAY_PASSWORD}" }, |
| "controlUi": { "allowInsecureAuth": true } |
| } |
| } |
| EOF |
| } |
| BASH |
| RUN chmod +x /usr/local/bin/generate-config.sh |
|
|
| |
| |
| |
| RUN cat > /usr/local/bin/start.sh <<'BASH' |
| |
| set -e |
|
|
| GATEWAY_PORT=${PORT:-7860} |
|
|
| |
| RAW_BI="${BACKUP_INTERVAL:-10800}" |
| if echo "$RAW_BI" | grep -Eq '^[0-9]+$'; then |
| BI="$RAW_BI" |
| else |
| echo "[WARN] BACKUP_INTERVAL 非数字: '${RAW_BI}', 回退为 10800" |
| BI="10800" |
| fi |
|
|
| |
| BACKUP_ENABLED=true |
| if [ "$BI" -le 0 ]; then |
| BACKUP_ENABLED=false |
| elif [ "$BI" -lt 60 ]; then |
| echo "[WARN] BACKUP_INTERVAL=${BI} 太小,提升到 60" |
| BI="60" |
| fi |
|
|
| |
| RAW_FLAG="${GENERATE_NEW_OPENCLAW_CONFIG-}" |
| FLAG_NORM="$(printf '%s' "$RAW_FLAG" | tr '[:upper:]' '[:lower:]' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" |
| FORCE_NEW=false |
| case "${FLAG_NORM}" in |
| ""|"0"|"no"|"null"|"not") FORCE_NEW=false ;; |
| *) FORCE_NEW=true ;; |
| esac |
|
|
| echo "===== Application Startup (v2.1.9) =====" |
| echo "FORCE_NEW_OPENCLAW_CONFIG: ${FORCE_NEW} (GENERATE_NEW_OPENCLAW_CONFIG='${RAW_FLAG}')" |
| echo "PORT: ${GATEWAY_PORT}" |
| echo "BACKUP_INTERVAL(env): ${BI}s (enabled=${BACKUP_ENABLED})" |
|
|
| mkdir -p /root/.openclaw/{sessions,workspace,credentials,agents/main/sessions,cron,identity,devices} |
|
|
| command -v jq >/dev/null 2>&1 || { echo "[FATAL] jq 不存在"; exit 1; } |
|
|
| |
| echo "[START] Restore from HF Dataset..." |
| set +e |
| python3 /usr/local/bin/sync.py restore |
| RESTORE_CODE=$? |
| set -e |
| if [ $RESTORE_CODE -ne 0 ]; then |
| echo "[WARN] restore 失败(代码: $RESTORE_CODE),将继续启动" |
| fi |
|
|
| |
| TEMPLATE_FULL="/tmp/openclaw.template.full.json" |
| source /usr/local/bin/generate-config.sh |
| generate_config "$TEMPLATE_FULL" |
|
|
| |
| |
| TEMPLATE_MERGE="/tmp/openclaw.template.merge.json" |
| cp "$TEMPLATE_FULL" "$TEMPLATE_MERGE" |
|
|
| |
| if [ -z "${OPENCLAW_GATEWAY_PASSWORD:-}" ]; then |
| tmp=$(mktemp) |
| jq '.gateway.auth.token = null' "$TEMPLATE_MERGE" > "$tmp" && mv "$tmp" "$TEMPLATE_MERGE" |
| fi |
|
|
| |
| if [ -z "${OPENAI_API_KEY:-}" ]; then |
| tmp=$(mktemp) |
| jq '.models.providers.nvidia.apiKey = null' "$TEMPLATE_MERGE" > "$tmp" && mv "$tmp" "$TEMPLATE_MERGE" |
| fi |
| if [ -z "${OPENROUTER_API_KEY:-}" ]; then |
| tmp=$(mktemp) |
| jq '.models.providers.openrouterai.apiKey = null' "$TEMPLATE_MERGE" > "$tmp" && mv "$tmp" "$TEMPLATE_MERGE" |
| fi |
| if [ -z "${VERCEL_AI_GATEWAY_API_KEY:-}" ]; then |
| tmp=$(mktemp) |
| jq '.models.providers.vercel.apiKey = null' "$TEMPLATE_MERGE" > "$tmp" && mv "$tmp" "$TEMPLATE_MERGE" |
| fi |
|
|
| |
| if [ "${FORCE_NEW}" = "true" ]; then |
| echo "[MODE] A: 强制生成新 openclaw.json(忽略 restore 的 openclaw.json)" |
| cp "$TEMPLATE_FULL" /root/.openclaw/openclaw.json |
| else |
| |
| if [ ! -f /root/.openclaw/openclaw.json ]; then |
| echo "[MODE] B: 未发现 restore 的 openclaw.json => 直接使用模板生成" |
| cp "$TEMPLATE_FULL" /root/.openclaw/openclaw.json |
| else |
| echo "[MODE] B: 合并 openclaw.json(模板优先 + 保留旧独有字段 + 数组不丢 old-only)" |
|
|
| |
| CLEAN="/tmp/openclaw.cleaned.json" |
| jq ' |
| def walk(f): |
| . as $in |
| | if type=="object" then reduce keys[] as $k ({}; . + {($k): ($in[$k] | walk(f))}) | f |
| elif type=="array" then map(walk(f)) | f |
| else f end; |
| walk(if type=="object" then del(."__comment") else . end) |
| ' /root/.openclaw/openclaw.json > "$CLEAN" |
|
|
| |
| MERGED="/tmp/openclaw.merged.json" |
| jq -s ' |
| def obj_with_id: (type=="object" and (.id? != null)); |
| |
| # 去重合并:模板数组在前,旧数组追加旧独有元素 |
| def union_by_json(a;b): ((b // []) + (a // [])) | unique_by(tojson); |
| |
| # 对象数组(有 id)按 id 合并:模板优先;旧额外 id 保留;无 id 项保留 |
| def merge_array_by_id(a;b): |
| (a // []) as $a |
| | (b // []) as $b |
| | ($a | map(select(obj_with_id)) | map({key: .id, value: .}) | from_entries) as $idx |
| | ($b | map(if obj_with_id then rmerge(($idx[.id] // {}); .) else . end)) as $b_merged |
| | ($b | map(select(obj_with_id) | .id)) as $b_ids |
| | ($a | map(select(obj_with_id and (.id as $id | ($b_ids | index($id)) == null)))) as $a_extra_id |
| | ($a | map(select(obj_with_id | not))) as $a_no_id |
| | union_by_json(($a_extra_id + $a_no_id); $b_merged); |
| |
| # 递归合并:对象深合并;冲突模板(b)覆盖旧(a) |
| def rmerge(a;b): |
| if (a|type)=="object" and (b|type)=="object" then |
| reduce ((a|keys_unsorted) + (b|keys_unsorted) | unique)[] as $k |
| ({}; . + {($k): |
| if (a[$k] != null and b[$k] != null) then rmerge(a[$k]; b[$k]) |
| elif (b[$k] != null) then b[$k] |
| else a[$k] end |
| }) |
| elif (a|type)=="array" and (b|type)=="array" then |
| (b | map(obj_with_id) | any) as $hasid |
| | if $hasid then merge_array_by_id(a;b) else union_by_json(a;b) end |
| else |
| b |
| end; |
| |
| rmerge(.[0]; .[1]) |
| ' "$CLEAN" "$TEMPLATE_MERGE" > "$MERGED" || { echo "[FATAL] jq 合并失败"; exit 1; } |
|
|
| |
| jq --arg port "${GATEWAY_PORT}" ' |
| .gateway |= (. // {}) |
| | .gateway.port = ($port | tonumber) |
| | .gateway.bind = ( |
| if (.gateway.bind | type) != "string" then "lan" |
| elif (.gateway.bind=="lan" or .gateway.bind=="loopback" or .gateway.bind=="tailnet" or .gateway.bind=="auto") |
| then .gateway.bind |
| else "lan" |
| end |
| ) |
| ' "$MERGED" > /root/.openclaw/openclaw.json |
| fi |
| fi |
|
|
| |
| |
| if [ -n "${OPENCLAW_GATEWAY_PASSWORD:-}" ]; then |
| tmp=$(mktemp) |
| jq --arg t "${OPENCLAW_GATEWAY_PASSWORD}" ' |
| .gateway |= (. // {}) |
| | .gateway.auth |= (. // {}) |
| | .gateway.auth.mode = "token" |
| | .gateway.auth.token = $t |
| ' /root/.openclaw/openclaw.json > "$tmp" && mv "$tmp" /root/.openclaw/openclaw.json |
| else |
| |
| tmp=$(mktemp) |
| jq ' |
| .gateway |= (. // {}) |
| | .gateway.auth |= (. // {}) |
| | .gateway.auth.mode = "token" |
| | .gateway.auth.token = ( |
| if (.gateway.auth.token | type) == "string" and (.gateway.auth.token | length) > 0 |
| then .gateway.auth.token |
| else "default_change_me" |
| end |
| ) |
| ' /root/.openclaw/openclaw.json > "$tmp" && mv "$tmp" /root/.openclaw/openclaw.json |
| fi |
|
|
| |
| if [ -n "${OPENAI_API_KEY:-}" ]; then |
| tmp=$(mktemp) |
| jq --arg k "${OPENAI_API_KEY}" ' |
| .models.providers.nvidia |= (. // {}) |
| | .models.providers.nvidia.apiKey = $k |
| ' /root/.openclaw/openclaw.json > "$tmp" && mv "$tmp" /root/.openclaw/openclaw.json |
| else |
| tmp=$(mktemp) |
| jq ' |
| .models.providers.nvidia |= (. // {}) |
| | .models.providers.nvidia.apiKey = ( |
| if (.models.providers.nvidia.apiKey | type) == "string" then .models.providers.nvidia.apiKey else "" end |
| ) |
| ' /root/.openclaw/openclaw.json > "$tmp" && mv "$tmp" /root/.openclaw/openclaw.json |
| fi |
|
|
| if [ -n "${OPENROUTER_API_KEY:-}" ]; then |
| tmp=$(mktemp) |
| jq --arg k "${OPENROUTER_API_KEY}" ' |
| .models.providers.openrouterai |= (. // {}) |
| | .models.providers.openrouterai.apiKey = $k |
| ' /root/.openclaw/openclaw.json > "$tmp" && mv "$tmp" /root/.openclaw/openclaw.json |
| else |
| tmp=$(mktemp) |
| jq ' |
| .models.providers.openrouterai |= (. // {}) |
| | .models.providers.openrouterai.apiKey = ( |
| if (.models.providers.openrouterai.apiKey | type) == "string" then .models.providers.openrouterai.apiKey else "" end |
| ) |
| ' /root/.openclaw/openclaw.json > "$tmp" && mv "$tmp" /root/.openclaw/openclaw.json |
| fi |
|
|
| if [ -n "${VERCEL_AI_GATEWAY_API_KEY:-}" ]; then |
| tmp=$(mktemp) |
| jq --arg k "${VERCEL_AI_GATEWAY_API_KEY}" ' |
| .models.providers.vercel |= (. // {}) |
| | .models.providers.vercel.apiKey = $k |
| ' /root/.openclaw/openclaw.json > "$tmp" && mv "$tmp" /root/.openclaw/openclaw.json |
| else |
| tmp=$(mktemp) |
| jq ' |
| .models.providers.vercel |= (. // {}) |
| | .models.providers.vercel.apiKey = ( |
| if (.models.providers.vercel.apiKey | type) == "string" then .models.providers.vercel.apiKey else "" end |
| ) |
| ' /root/.openclaw/openclaw.json > "$tmp" && mv "$tmp" /root/.openclaw/openclaw.json |
| fi |
|
|
| |
| PROVIDER_TO_SET=${PROVIDER:-"nvidia"} |
| MODEL_TO_SET=${MODEL:-"moonshotai/kimi-k2.5"} |
| tmp=$(mktemp) |
| jq --arg p "$PROVIDER_TO_SET" --arg m "$MODEL_TO_SET" ' |
| .agents |= (. // {}) |
| | .agents.defaults |= (. // {}) |
| | .agents.defaults.model |= (. // {}) |
| | .agents.defaults.model.primary = ($p + "/" + $m) |
| ' /root/.openclaw/openclaw.json > "$tmp" && mv "$tmp" /root/.openclaw/openclaw.json |
|
|
| |
| |
| DISABLE_FIX_NORM="$(printf '%s' "${OPENCLAW_DISABLE_DOCTOR_FIX-}" | tr '[:upper:]' '[:lower:]' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" |
| if [ "${DISABLE_FIX_NORM}" != "1" ] && [ "${DISABLE_FIX_NORM}" != "true" ] && [ "${DISABLE_FIX_NORM}" != "yes" ]; then |
| echo "[FIX] 运行 openclaw doctor --fix(最多 20 秒,避免卡死)..." |
| set +e |
| timeout 20s openclaw doctor --fix >/tmp/openclaw_doctor_fix.log 2>&1 |
| code=$? |
| set -e |
| if [ $code -eq 0 ]; then |
| echo "[FIX] doctor --fix 完成" |
| elif [ $code -eq 124 ]; then |
| echo "[WARN] doctor --fix 超时(已跳过)" |
| else |
| echo "[WARN] doctor --fix 失败(代码: $code),将继续启动" |
| fi |
|
|
| |
| tmp=$(mktemp) |
| jq --arg port "${GATEWAY_PORT}" ' |
| .gateway |= (. // {}) |
| | .gateway.port = ($port | tonumber) |
| | .gateway.bind = ( |
| if (.gateway.bind | type) != "string" then "lan" |
| elif (.gateway.bind=="lan" or .gateway.bind=="loopback" or .gateway.bind=="tailnet" or .gateway.bind=="auto") |
| then .gateway.bind |
| else "lan" |
| end |
| ) |
| ' /root/.openclaw/openclaw.json > "$tmp" && mv "$tmp" /root/.openclaw/openclaw.json |
| fi |
|
|
| PRIMARY_FINAL=$(jq -r '.agents.defaults.model.primary // ""' /root/.openclaw/openclaw.json 2>/dev/null || true) |
|
|
| echo "========================================" |
| echo " 配置摘要" |
| echo "========================================" |
| echo " Primary (final): ${PRIMARY_FINAL}" |
| echo " Gateway Port: ${GATEWAY_PORT}" |
| echo " Health Check: http://localhost:${GATEWAY_PORT}/health" |
| echo "========================================" |
|
|
| |
| if [ "${BACKUP_ENABLED}" = "true" ]; then |
| echo "[START] 启动定时备份 (间隔: ${BI}秒)" |
| ( |
| while true; do |
| sleep "$BI" |
| echo "[BACKUP] 开始备份: $(date -u +%Y-%m-%dT%H:%M:%SZ)" |
| set +e |
| python3 /usr/local/bin/sync.py backup |
| code=$? |
| set -e |
| if [ $code -eq 0 ]; then |
| echo "[BACKUP] 备份成功" |
| else |
| echo "[BACKUP] 备份失败 (代码: $code)" >&2 |
| fi |
| done |
| ) & |
| else |
| echo "[START] 定时备份已禁用(BACKUP_INTERVAL<=0)" |
| fi |
|
|
| echo "[START] 启动 OpenClaw Gateway (端口: ${GATEWAY_PORT})..." |
| exec openclaw gateway run --port "${GATEWAY_PORT}" |
| BASH |
| RUN chmod +x /usr/local/bin/start.sh |
|
|
| EXPOSE 7860 |
|
|
| HEALTHCHECK --interval=30s --timeout=10s --retries=3 \ |
| CMD sh -c 'curl -f "http://localhost:${PORT:-7860}/health" || exit 1' |
|
|
| CMD ["/usr/local/bin/start.sh"] |