| |
|
|
| 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 \ |
| && 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 false && \ |
| git config --global url."https://github.com/".insteadOf ssh://git@github.com/ |
|
|
| |
| RUN npm install -g openclaw@latest --unsafe-perm |
|
|
| |
| ENV \ |
| |
| PORT=${PORT:-7860} \ |
| NODE_ENV=${NODE_ENV:-production} \ |
| HOME=${HOME:-/root} \ |
| \ |
| |
| OPENCLAW_GATEWAY_MODE=${OPENCLAW_GATEWAY_MODE:-local} \ |
| OPENCLAW_GATEWAY_TOKEN=${OPENCLAW_GATEWAY_TOKEN:-} \ |
| \ |
| |
| MODEL_PROVIDER=${MODEL_PROVIDER:-nvidia} \ |
| MODEL_ID=${MODEL_ID:-moonshotai/kimi-k2.5} \ |
| MODEL_CONTEXT_WINDOW=${MODEL_CONTEXT_WINDOW:-256000} \ |
| MODEL_DISPLAY_NAME=${MODEL_DISPLAY_NAME:-"Kimi K2.5"} \ |
| \ |
| |
| API_BASE_URL=${API_BASE_URL:-https://integrate.api.nvidia.com/v1} \ |
| API_TYPE=${API_TYPE:-openai-completions} \ |
| OPENAI_API_KEY=${OPENAI_API_KEY:-} \ |
| \ |
| |
| GATEWAY_AUTH_MODE=${GATEWAY_AUTH_MODE:-token} \ |
| GATEWAY_BIND=${GATEWAY_BIND:-lan} \ |
| GATEWAY_TRUSTED_PROXIES=${GATEWAY_TRUSTED_PROXIES:-0.0.0.0/0,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16} \ |
| GATEWAY_ALLOWED_ORIGINS=${GATEWAY_ALLOWED_ORIGINS:-https://control.example.com} \ |
| \ |
| |
| CONTROLUI_ALLOW_INSECURE_AUTH=${CONTROLUI_ALLOW_INSECURE_AUTH:-true} \ |
| CONTROLUI_DANGEROUS_HOST_HEADER=${CONTROLUI_DANGEROUS_HOST_HEADER:-true} \ |
| CONTROLUI_DANGEROUS_DISABLE_DEVICE_AUTH=${CONTROLUI_DANGEROUS_DISABLE_DEVICE_AUTH:-true} \ |
| \ |
| |
| TELEGRAM_ENABLED=${TELEGRAM_ENABLED:-false} \ |
| TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN:-} \ |
| TELEGRAM_DM_POLICY=${TELEGRAM_DM_POLICY:-allowlist} \ |
| TELEGRAM_ALLOW_FROM=${TELEGRAM_ALLOW_FROM:-} \ |
| TELEGRAM_PROXY_HOST=${TELEGRAM_PROXY_HOST:-} \ |
| \ |
| |
| BACKUP_ENABLED=${BACKUP_ENABLED:-true} \ |
| BACKUP_INTERVAL=${BACKUP_INTERVAL:-21600} \ |
| BACKUP_RETENTION_DAYS=${BACKUP_RETENTION_DAYS:-5} \ |
| HF_DATASET=${HF_DATASET:-} \ |
| HF_TOKEN=${HF_TOKEN:-} |
|
|
| |
| RUN cat > /usr/local/bin/sync.py << 'SYNC_EOF' |
| |
| import os |
| import sys |
| import tarfile |
| import shutil |
| from huggingface_hub import HfApi, hf_hub_download |
| from datetime import datetime, timedelta |
|
|
| api = HfApi() |
| repo_id = os.getenv("HF_DATASET", "") |
| token = os.getenv("HF_TOKEN", "") |
| retention_days = int(os.getenv("BACKUP_RETENTION_DAYS", "5")) |
|
|
| OPENCLAW_DIR = "/root/.openclaw" |
| BACKUP_DIR = "/tmp/openclaw_backups" |
|
|
| def ensure_backup_dir(): |
| """确保备份临时目录存在""" |
| os.makedirs(BACKUP_DIR, exist_ok=True) |
|
|
| def restore(): |
| """从 Hugging Face Dataset 恢复最新的 .openclaw 目录""" |
| if not repo_id or not token: |
| print("⚠️ HF_DATASET 或 HF_TOKEN 未设置,跳过恢复") |
| return False |
| |
| if not os.path.exists(OPENCLAW_DIR): |
| os.makedirs(OPENCLAW_DIR, mode=0o755, exist_ok=True) |
| |
| try: |
| |
| files = api.list_repo_files(repo_id=repo_id, repo_type="dataset", token=token) |
| backup_files = [f for f in files if f.startswith("backup_") and f.endswith(".tar.gz")] |
| |
| if not backup_files: |
| print("ℹ️ 没有找到备份文件") |
| return False |
| |
| |
| backup_files.sort(reverse=True) |
| latest_backup = backup_files[0] |
| |
| print(f"📥 发现最新备份: {latest_backup}") |
| |
| |
| ensure_backup_dir() |
| local_backup_path = os.path.join(BACKUP_DIR, latest_backup) |
| |
| print(f"⬇️ 下载备份文件...") |
| downloaded_path = hf_hub_download( |
| repo_id=repo_id, |
| filename=latest_backup, |
| repo_type="dataset", |
| token=token, |
| local_dir=BACKUP_DIR, |
| local_dir_use_symlinks=False |
| ) |
| |
| |
| restore_temp = os.path.join(BACKUP_DIR, "restore_temp") |
| if os.path.exists(restore_temp): |
| shutil.rmtree(restore_temp) |
| os.makedirs(restore_temp) |
| |
| |
| print(f"📦 解压备份文件...") |
| with tarfile.open(downloaded_path, "r:gz") as tar: |
| tar.extractall(path=restore_temp) |
| |
| |
| extracted_items = os.listdir(restore_temp) |
| print(f"解压内容: {extracted_items}") |
| |
| |
| if ".openclaw" in extracted_items: |
| source_dir = os.path.join(restore_temp, ".openclaw") |
| |
| elif set(extracted_items) & {"sessions", "openclaw.json", "workspace"}: |
| source_dir = restore_temp |
| else: |
| print(f"❌ 无法识别的备份格式: {extracted_items}") |
| return False |
| |
| |
| if os.path.exists(OPENCLAW_DIR) and os.listdir(OPENCLAW_DIR): |
| backup_current = os.path.join(BACKUP_DIR, "current_before_restore") |
| if os.path.exists(backup_current): |
| shutil.rmtree(backup_current) |
| shutil.copytree(OPENCLAW_DIR, backup_current) |
| print(f"💾 已备份当前目录到: {backup_current}") |
| |
| |
| print(f"🔄 恢复数据到 {OPENCLAW_DIR}...") |
| if os.path.exists(OPENCLAW_DIR): |
| |
| for item in os.listdir(OPENCLAW_DIR): |
| item_path = os.path.join(OPENCLAW_DIR, item) |
| if os.path.isfile(item_path): |
| os.remove(item_path) |
| elif os.path.isdir(item_path): |
| shutil.rmtree(item_path) |
| |
| |
| for item in os.listdir(source_dir): |
| src = os.path.join(source_dir, item) |
| dst = os.path.join(OPENCLAW_DIR, item) |
| if os.path.isdir(src): |
| shutil.copytree(src, dst) |
| else: |
| shutil.copy2(src, dst) |
| |
| print(f"✅ 恢复完成!") |
| |
| |
| shutil.rmtree(restore_temp) |
| os.remove(downloaded_path) |
| |
| return True |
| |
| except Exception as e: |
| print(f"❌ 恢复失败: {e}") |
| return False |
|
|
| def backup(): |
| """备份整个 .openclaw 目录到 Hugging Face Dataset""" |
| if not repo_id or not token: |
| print("⚠️ HF_DATASET 或 HF_TOKEN 未设置,跳过备份") |
| return |
| |
| if not os.path.exists(OPENCLAW_DIR) or not os.listdir(OPENCLAW_DIR): |
| print("ℹ️ .openclaw 目录为空,跳过备份") |
| return |
| |
| try: |
| |
| today = datetime.now().strftime("%Y-%m-%d") |
| backup_filename = f"backup_{today}.tar.gz" |
| backup_path = os.path.join(BACKUP_DIR, backup_filename) |
| |
| ensure_backup_dir() |
| |
| print(f"📦 创建备份: {backup_filename}") |
| |
| |
| with tarfile.open(backup_path, "w:gz") as tar: |
| tar.add(OPENCLAW_DIR, arcname=".openclaw") |
| |
| |
| print(f"⬆️ 上传到 Hugging Face Dataset: {repo_id}") |
| api.upload_file( |
| path_or_fileobj=backup_path, |
| path_in_repo=backup_filename, |
| repo_id=repo_id, |
| repo_type="dataset", |
| token=token |
| ) |
| |
| print(f"✅ 备份完成: {backup_filename}") |
| |
| |
| try: |
| files = api.list_repo_files(repo_id=repo_id, repo_type="dataset", token=token) |
| backup_files = [f for f in files if f.startswith("backup_") and f.endswith(".tar.gz")] |
| backup_files.sort() |
| |
| |
| while len(backup_files) > retention_days: |
| oldest = backup_files.pop(0) |
| print(f"🗑️ 删除旧备份: {oldest}") |
| api.delete_file( |
| path_in_repo=oldest, |
| repo_id=repo_id, |
| repo_type="dataset", |
| token=token |
| ) |
| except Exception as e: |
| print(f"⚠️ 清理旧备份失败: {e}") |
| |
| |
| os.remove(backup_path) |
| |
| except Exception as e: |
| print(f"❌ 备份失败: {e}") |
|
|
| def list_backups(): |
| """列出所有可用的备份""" |
| if not repo_id or not token: |
| print("⚠️ HF_DATASET 或 HF_TOKEN 未设置") |
| return |
| |
| try: |
| files = api.list_repo_files(repo_id=repo_id, repo_type="dataset", token=token) |
| backup_files = [f for f in files if f.startswith("backup_") and f.endswith(".tar.gz")] |
| backup_files.sort(reverse=True) |
| |
| if backup_files: |
| print("📋 可用的备份:") |
| for i, f in enumerate(backup_files, 1): |
| print(f" {i}. {f}") |
| else: |
| print("ℹ️ 没有找到备份文件") |
| |
| except Exception as e: |
| print(f"❌ 列出备份失败: {e}") |
|
|
| if __name__ == "__main__": |
| if len(sys.argv) > 1: |
| if sys.argv[1] == "backup": |
| backup() |
| elif sys.argv[1] == "restore": |
| restore() |
| elif sys.argv[1] == "list": |
| list_backups() |
| else: |
| print(f"未知命令: {sys.argv[1]}") |
| print("可用命令: backup, restore, list") |
| else: |
| |
| restore() |
| SYNC_EOF |
| RUN chmod +x /usr/local/bin/sync.py |
|
|
| |
| RUN cat > /usr/local/bin/patch-telegram-api << 'PATCH_EOF' |
| |
|
|
| |
| if [ -n "$TELEGRAM_PROXY_HOST" ]; then |
| echo "🔧 检测到 TELEGRAM_PROXY_HOST 设置: $TELEGRAM_PROXY_HOST" |
| echo "🔄 开始替换 OpenClaw 中的 Telegram API 地址..." |
| |
| OPENCLAW_DIR="/usr/local/lib/node_modules/openclaw" |
| |
| if [ -d "$OPENCLAW_DIR" ]; then |
| |
| MATCH_COUNT=$(grep -r "api.telegram.org" "$OPENCLAW_DIR" 2>/dev/null | wc -l) |
| echo "📊 找到 $MATCH_COUNT 处需要替换的地址" |
| |
| # 执行替换(使用 sed -i 进行原地替换) |
| # 处理 .js 文件 |
| find "$OPENCLAW_DIR" -type f -name "*.js" -exec sed -i "s|api\\.telegram\\.org|$TELEGRAM_PROXY_HOST|g" {} + |
| |
| # 再次检查是否还有未替换的(可能是其他格式) |
| REMAINING=$(grep -r "api.telegram.org" "$OPENCLAW_DIR" 2>/dev/null | wc -l) |
| |
| if [ "$REMAINING" -eq 0 ]; then |
| echo "✅ Telegram API 地址替换完成!" |
| echo " 原始地址: api.telegram.org" |
| echo " 新地址: $TELEGRAM_PROXY_HOST" |
| else |
| echo "⚠️ 仍有 $REMAINING 处未替换,尝试二次替换..." |
| # 尝试更宽松的匹配(处理可能的转义或编码) |
| find "$OPENCLAW_DIR" -type f \( -name "*.js" -o -name "*.json" -o -name "*.ts" \) -exec sed -i "s|api.telegram.org|$TELEGRAM_PROXY_HOST|g" {} + |
| |
| FINAL_REMAINING=$(grep -r "api.telegram.org" "$OPENCLAW_DIR" 2>/dev/null | wc -l) |
| echo "📊 最终剩余未替换: $FINAL_REMAINING 处" |
| fi |
| |
| |
| echo "🔍 验证替换结果(前3处):" |
| grep -r "$TELEGRAM_PROXY_HOST" "$OPENCLAW_DIR" 2>/dev/null | head -3 | sed 's|.*| &|' |
| else |
| echo "❌ OpenClaw 目录不存在: $OPENCLAW_DIR" |
| fi |
| else |
| echo "ℹ️ 未设置 TELEGRAM_PROXY_HOST,跳过 Telegram API 替换" |
| fi |
| PATCH_EOF |
| RUN chmod +x /usr/local/bin/patch-telegram-api |
|
|
| |
| RUN cat > /usr/local/bin/start-openclaw << 'START_EOF' |
| |
| set -e |
|
|
| |
| mkdir -p /root/.openclaw |
| mkdir -p /root/.openclaw/sessions |
| mkdir -p /root/.openclaw/workspace |
|
|
| echo "========================================" |
| echo "OpenClaw Gateway Starting..." |
| echo "========================================" |
|
|
| |
| if [ -n "$HF_DATASET" ] && [ -n "$HF_TOKEN" ]; then |
| echo "🔄 尝试从 Hugging Face 恢复数据..." |
| python3 /usr/local/bin/sync.py restore || echo "⚠️ 恢复失败,继续启动..." |
| fi |
|
|
| |
| /usr/local/bin/patch-telegram-api |
|
|
| |
| CLEAN_BASE="${API_BASE_URL}" |
| CLEAN_BASE=$(echo "$CLEAN_BASE" | sed 's|/chat/completions||g' | sed 's|/v1/|/v1|g' | sed 's|/v1$|/v1|') |
|
|
| |
| if [ -z "$OPENCLAW_GATEWAY_TOKEN" ]; then |
| OPENCLAW_GATEWAY_TOKEN=$(openssl rand -hex 16) |
| echo "🔑 生成的网关令牌: $OPENCLAW_GATEWAY_TOKEN" |
| fi |
|
|
| |
| TRUSTED_PROXIES_JSON=$(echo "$GATEWAY_TRUSTED_PROXIES" | tr ',' '\n' | awk '{ printf "\"%s\",", $0 }' | sed 's/,$//' | sed 's/^/[/' | sed 's/$/]/') |
| |
| # 转换允许的源列表为JSON数组 |
| ALLOWED_ORIGINS_JSON=$(echo "$GATEWAY_ALLOWED_ORIGINS" | tr ',' '\n' | awk '{ printf "\"%s\",", $0 }' | sed 's/,$//' | sed 's/^/[/' | sed 's/$/]/') |
|
|
| |
| if [ -n "$TELEGRAM_ALLOW_FROM" ]; then |
| |
| TELEGRAM_ALLOW_JSON=$(echo "$TELEGRAM_ALLOW_FROM" | tr ',' '\n' | while read id; do |
| id=$(echo "$id" | xargs) # 去除空格 |
| if [[ "$id" =~ ^[0-9]+$ ]]; then |
| echo "\"tg:$id\"" |
| elif [[ "$id" =~ ^tg: ]]; then |
| echo "\"$id\"" |
| elif [[ "$id" =~ ^@ ]]; then |
| echo "\"$id\"" |
| else |
| echo "\"$id\"" |
| fi |
| done | paste -sd ',' | sed 's/^/[/' | sed 's/$/]/') |
| else |
| TELEGRAM_ALLOW_JSON="[]" |
| fi |
|
|
| |
| if [ ! -f "/root/.openclaw/openclaw.json" ]; then |
| echo "📝 创建 OpenClaw 配置文件..." |
| cat > /root/.openclaw/openclaw.json <<OPENCLAW_CONFIG |
| { |
| "models": { |
| "providers": { |
| "${MODEL_PROVIDER}": { |
| "baseUrl": "${CLEAN_BASE}", |
| "apiKey": "${OPENAI_API_KEY}", |
| "api": "${API_TYPE}", |
| "models": [{ |
| "id": "${MODEL_ID}", |
| "name": "${MODEL_DISPLAY_NAME}", |
| "contextWindow": ${MODEL_CONTEXT_WINDOW} |
| }] |
| } |
| } |
| }, |
| "agents": { |
| "defaults": { |
| "model": { |
| "primary": "${MODEL_PROVIDER}/${MODEL_ID}" |
| }, |
| "workspace": "/root/.openclaw/workspace" |
| } |
| }, |
| "gateway": { |
| "mode": "${OPENCLAW_GATEWAY_MODE}", |
| "bind": "${GATEWAY_BIND}", |
| "port": ${PORT}, |
| "trustedProxies": ${TRUSTED_PROXIES_JSON}, |
| "auth": { |
| "mode": "${GATEWAY_AUTH_MODE}", |
| "token": "${OPENCLAW_GATEWAY_TOKEN}" |
| }, |
| "remote": { |
| "token": "${OPENCLAW_GATEWAY_TOKEN}" |
| }, |
| "controlUi": { |
| "allowInsecureAuth": ${CONTROLUI_ALLOW_INSECURE_AUTH}, |
| "dangerouslyAllowHostHeaderOriginFallback": ${CONTROLUI_DANGEROUS_HOST_HEADER}, |
| "dangerouslyDisableDeviceAuth": ${CONTROLUI_DANGEROUS_DISABLE_DEVICE_AUTH}, |
| "allowedOrigins": ${ALLOWED_ORIGINS_JSON} |
| } |
| }, |
| "session": { |
| "store": "/root/.openclaw/sessions/sessions.json" |
| }, |
| "channels": { |
| "telegram": { |
| "enabled": ${TELEGRAM_ENABLED}, |
| "botToken": "${TELEGRAM_BOT_TOKEN}", |
| "dmPolicy": "${TELEGRAM_DM_POLICY}", |
| "allowFrom": ${TELEGRAM_ALLOW_JSON} |
| } |
| } |
| } |
| OPENCLAW_CONFIG |
| else |
| echo "📁 使用现有的配置文件" |
| fi |
|
|
| echo "========================================" |
| echo "📊 配置信息:" |
| echo " • 模型: ${MODEL_ID}" |
| echo " • Provider: ${MODEL_PROVIDER}" |
| echo " • 端口: ${PORT}" |
| echo " • 备份: ${BACKUP_ENABLED}" |
| echo " • Allowed Origins: ${GATEWAY_ALLOWED_ORIGINS}" |
| echo " • Telegram Enabled: ${TELEGRAM_ENABLED}" |
| if [ "${TELEGRAM_ENABLED}" = "true" ]; then |
| echo " • Telegram DM Policy: ${TELEGRAM_DM_POLICY}" |
| echo " • Telegram Allow From: ${TELEGRAM_ALLOW_FROM}" |
| if [ -n "$TELEGRAM_PROXY_HOST" ]; then |
| echo " • Telegram Proxy: ${TELEGRAM_PROXY_HOST}" |
| fi |
| fi |
| echo "========================================" |
|
|
| |
| if [ "${BACKUP_ENABLED}" = "true" ] && [ -n "$HF_DATASET" ] && [ -n "$HF_TOKEN" ]; then |
| echo "🔄 启动自动备份 (间隔: ${BACKUP_INTERVAL}秒)" |
| ( |
| while true; do |
| sleep ${BACKUP_INTERVAL} |
| echo "⏰ 执行定时备份..." |
| python3 /usr/local/bin/sync.py backup || echo "⚠️ 备份失败" |
| done |
| ) & |
| fi |
|
|
| |
| echo "🔧 运行 OpenClaw 诊断..." |
| openclaw doctor --fix || true |
|
|
| echo "🚀 启动 OpenClaw Gateway..." |
| echo "========================================" |
| exec openclaw gateway run --port $PORT |
| START_EOF |
| RUN chmod +x /usr/local/bin/start-openclaw |
|
|
| EXPOSE 7860 |
|
|
| CMD ["/usr/local/bin/start-openclaw"] |
|
|