erayoc commited on
Commit
8d43406
·
verified ·
1 Parent(s): 3757170

Create Dockerfile

Browse files
Files changed (1) hide show
  1. Dockerfile +836 -0
Dockerfile ADDED
@@ -0,0 +1,836 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # OpenClaw on Hugging Face Spaces - 多模型配置版
2
+
3
+ FROM node:22-slim
4
+
5
+ # 1. 基础依赖
6
+ RUN apt-get update && apt-get install -y --no-install-recommends \
7
+ git openssh-client build-essential python3 python3-pip \
8
+ g++ make ca-certificates curl jq \
9
+ && rm -rf /var/lib/apt/lists/*
10
+
11
+ # 2. 安装 Hugging Face Hub
12
+ RUN pip3 install --no-cache-dir huggingface_hub --break-system-packages
13
+
14
+ # 3. Git 配置
15
+ RUN update-ca-certificates && \
16
+ git config --global http.sslVerify false && \
17
+ git config --global url."https://github.com/".insteadOf ssh://git@github.com/
18
+
19
+ # 4. 安装 OpenClaw
20
+ RUN npm install -g openclaw@latest --unsafe-perm
21
+
22
+ # 5. 环境变量预设 - 只保留非模型相关的配置
23
+ ENV \
24
+ # 基础配置
25
+ PORT=${PORT:-7860} \
26
+ NODE_ENV=${NODE_ENV:-production} \
27
+ HOME=${HOME:-/root} \
28
+ \
29
+ # OpenClaw 核心配置
30
+ OPENCLAW_GATEWAY_MODE=${OPENCLAW_GATEWAY_MODE:-local} \
31
+ OPENCLAW_GATEWAY_TOKEN=${OPENCLAW_GATEWAY_TOKEN:-} \
32
+ \
33
+ # 模型配置 - 单个JSON环境变量(包含所有模型和mode)
34
+ MODELS_CONFIG=${MODELS_CONFIG:-'{"mode":"merge","providers":{},"primary":""}'} \
35
+ \
36
+ # 网关配置
37
+ GATEWAY_AUTH_MODE=${GATEWAY_AUTH_MODE:-token} \
38
+ GATEWAY_BIND=${GATEWAY_BIND:-lan} \
39
+ 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} \
40
+ GATEWAY_ALLOWED_ORIGINS=${GATEWAY_ALLOWED_ORIGINS:-https://control.example.com} \
41
+ \
42
+ # 控制UI配置
43
+ CONTROLUI_ALLOW_INSECURE_AUTH=${CONTROLUI_ALLOW_INSECURE_AUTH:-true} \
44
+ CONTROLUI_DANGEROUS_HOST_HEADER=${CONTROLUI_DANGEROUS_HOST_HEADER:-true} \
45
+ CONTROLUI_DANGEROUS_DISABLE_DEVICE_AUTH=${CONTROLUI_DANGEROUS_DISABLE_DEVICE_AUTH:-true} \
46
+ \
47
+ # Telegram 配置
48
+ TELEGRAM_ENABLED=${TELEGRAM_ENABLED:-false} \
49
+ TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN:-} \
50
+ TELEGRAM_DM_POLICY=${TELEGRAM_DM_POLICY:-allowlist} \
51
+ TELEGRAM_ALLOW_FROM=${TELEGRAM_ALLOW_FROM:-} \
52
+ TELEGRAM_PROXY_HOST=${TELEGRAM_PROXY_HOST:-} \
53
+ \
54
+ # 备份配置
55
+ BACKUP_ENABLED=${BACKUP_ENABLED:-true} \
56
+ BACKUP_INTERVAL=${BACKUP_INTERVAL:-21600} \
57
+ BACKUP_RETENTION_DAYS=${BACKUP_RETENTION_DAYS:-5} \
58
+ HF_DATASET=${HF_DATASET:-} \
59
+ HF_TOKEN=${HF_TOKEN:-} \
60
+ # 备份排除列表 - JSON数组格式,默认排除 completions 和 npm 目录
61
+ BACKUP_EXCLUDE=${BACKUP_EXCLUDE:-'["/root/.openclaw/completions","/root/.openclaw/npm"]'}
62
+
63
+ # 6. 模型配置解析脚本
64
+ RUN cat > /usr/local/bin/parse-models-config.py << 'PARSE_EOF'
65
+ #!/usr/bin/env python3
66
+ import json
67
+ import os
68
+ import sys
69
+
70
+ def parse_models_config():
71
+ """解析 MODELS_CONFIG 环境变量,生成完整的 OpenClaw 配置"""
72
+
73
+ # 获取环境变量
74
+ models_config_json = os.getenv("MODELS_CONFIG", "{}")
75
+
76
+ # 默认配置
77
+ default_config = {
78
+ "mode": "merge", # 默认值为 merge
79
+ "providers": {},
80
+ "primary": ""
81
+ }
82
+
83
+ try:
84
+ # 解析 JSON
85
+ if models_config_json and models_config_json != "{}":
86
+ config = json.loads(models_config_json)
87
+ else:
88
+ config = {}
89
+
90
+ # 合并默认值
91
+ mode = config.get("mode", default_config["mode"])
92
+ providers = config.get("providers", {})
93
+ primary = config.get("primary", "")
94
+
95
+ # 构建结果
96
+ result = {
97
+ "mode": mode,
98
+ "providers": {},
99
+ "primary": primary,
100
+ "agents_models": {}
101
+ }
102
+
103
+ # 处理每个 provider
104
+ for provider_name, provider_config in providers.items():
105
+ # 验证必要字段
106
+ base_url = provider_config.get("baseUrl", "")
107
+ api_key = provider_config.get("apiKey", "")
108
+ api_type = provider_config.get("api", "openai-completions")
109
+ models = provider_config.get("models", [])
110
+
111
+ if not base_url or not api_key:
112
+ print(f"⚠️ Provider {provider_name} 缺少 baseUrl 或 apiKey,跳过")
113
+ continue
114
+
115
+ # 添加 provider 配置
116
+ result["providers"][provider_name] = {
117
+ "baseUrl": base_url,
118
+ "apiKey": api_key,
119
+ "api": api_type,
120
+ "models": models
121
+ }
122
+
123
+ # 为每个模型生成 agents.models 条目
124
+ for model in models:
125
+ model_id = model.get("id")
126
+ model_name = model.get("name", model_id)
127
+ if model_id:
128
+ key = f"{provider_name}/{model_id}"
129
+ result["agents_models"][key] = {
130
+ "alias": model_name
131
+ }
132
+
133
+ # 如果没有设置 primary 但有模型,使用第一个模型的完整ID
134
+ if not result["primary"] and result["agents_models"]:
135
+ first_model = list(result["agents_models"].keys())[0]
136
+ result["primary"] = first_model
137
+ print(f"ℹ️ 未设置主模型,自动使用: {first_model}")
138
+
139
+ return result
140
+
141
+ except json.JSONDecodeError as e:
142
+ print(f"❌ MODELS_CONFIG JSON 解析失败: {e}")
143
+ print(f"📄 收到的配置: {models_config_json[:200]}...")
144
+ # 返回默认配置
145
+ return {
146
+ "mode": "merge",
147
+ "providers": {},
148
+ "primary": "",
149
+ "agents_models": {}
150
+ }
151
+
152
+ def generate_full_config():
153
+ """生成完整的 OpenClaw 配置"""
154
+ parsed = parse_models_config()
155
+
156
+ # 构建完整的配置结构
157
+ full_config = {
158
+ "models": {
159
+ "mode": parsed["mode"],
160
+ "providers": parsed["providers"]
161
+ },
162
+ "agents": {
163
+ "defaults": {
164
+ "model": {
165
+ "primary": parsed["primary"]
166
+ },
167
+ "models": parsed["agents_models"],
168
+ "workspace": "/root/.openclaw/workspace"
169
+ }
170
+ }
171
+ }
172
+
173
+ return full_config
174
+
175
+ if __name__ == "__main__":
176
+ if len(sys.argv) > 1:
177
+ if sys.argv[1] == "--full":
178
+ # 输出完整配置
179
+ config = generate_full_config()
180
+ print(json.dumps(config, indent=2))
181
+ elif sys.argv[1] == "--validate":
182
+ # 验证配置
183
+ parsed = parse_models_config()
184
+ provider_count = len(parsed["providers"])
185
+ model_count = len(parsed["agents_models"])
186
+
187
+ if provider_count == 0:
188
+ print("⚠️ 没有配置任何 provider,将使用空配置")
189
+ else:
190
+ print(f"✅ 配置验证成功:")
191
+ print(f" • 模式: {parsed['mode']}")
192
+ print(f" • Providers: {provider_count}")
193
+ print(f" • 模型总数: {model_count}")
194
+ if parsed["primary"]:
195
+ print(f" • 主模型: {parsed['primary']}")
196
+ else:
197
+ print(f" ⚠️ 未设置主模型")
198
+
199
+ # 列出所有配置的模型
200
+ if model_count > 0:
201
+ print("\n📋 已配置的模型:")
202
+ for model_key in parsed["agents_models"].keys():
203
+ print(f" • {model_key}")
204
+
205
+ sys.exit(0 if provider_count > 0 else 1)
206
+ else:
207
+ # 默认输出解析结果
208
+ parsed = parse_models_config()
209
+ print(json.dumps(parsed, indent=2))
210
+ PARSE_EOF
211
+ RUN chmod +x /usr/local/bin/parse-models-config.py
212
+
213
+ # 7. 同步脚本 - 备份和恢复整个 .openclaw 目录(支持排除列表)
214
+ RUN cat > /usr/local/bin/sync.py << 'SYNC_EOF'
215
+ #!/usr/bin/env python3
216
+ import os
217
+ import sys
218
+ import tarfile
219
+ import shutil
220
+ import re
221
+ import json
222
+ from huggingface_hub import HfApi, hf_hub_download
223
+ from datetime import datetime, timedelta
224
+
225
+ api = HfApi()
226
+ repo_id = os.getenv("HF_DATASET", "")
227
+ token = os.getenv("HF_TOKEN", "")
228
+ retention_days = int(os.getenv("BACKUP_RETENTION_DAYS", "5"))
229
+
230
+ OPENCLAW_DIR = "/root/.openclaw"
231
+ BACKUP_DIR = "/tmp/openclaw_backups"
232
+
233
+ def get_exclude_patterns():
234
+ """获取排除列表(支持文件和目录)"""
235
+ exclude_json = os.getenv("BACKUP_EXCLUDE", '["/root/.openclaw/completions","/root/.openclaw/npm"]')
236
+
237
+ try:
238
+ exclude_list = json.loads(exclude_json)
239
+ if not isinstance(exclude_list, list):
240
+ print(f"⚠️ BACKUP_EXCLUDE 不是数组格式,使用默认排除列表")
241
+ exclude_list = ["/root/.openclaw/completions", "/root/.openclaw/npm"]
242
+
243
+ # 标准化路径(去除末尾斜杠)
244
+ exclude_list = [p.rstrip('/') for p in exclude_list]
245
+
246
+ print(f"📋 备份排除列表: {exclude_list if exclude_list else '无'}")
247
+ return exclude_list
248
+ except json.JSONDecodeError as e:
249
+ print(f"⚠️ BACKUP_EXCLUDE JSON 解析失败: {e}")
250
+ print(f" 使用默认排除列表")
251
+ return ["/root/.openclaw/completions", "/root/.openclaw/npm"]
252
+
253
+ def should_exclude(path, exclude_patterns):
254
+ """检查路径是否应该被排除"""
255
+ if not exclude_patterns:
256
+ return False
257
+
258
+ # 标准化路径
259
+ normalized_path = os.path.normpath(path)
260
+
261
+ for pattern in exclude_patterns:
262
+ normalized_pattern = os.path.normpath(pattern)
263
+
264
+ # 完全匹配
265
+ if normalized_path == normalized_pattern:
266
+ return True
267
+
268
+ # 路径以排除模式开头(排除目录下的所有内容)
269
+ if normalized_path.startswith(normalized_pattern + os.sep) or normalized_path.startswith(normalized_pattern + '/'):
270
+ return True
271
+
272
+ # 如果pattern是目录,且当前路径是该目录的直接子项
273
+ if os.path.exists(pattern) and os.path.isdir(pattern):
274
+ parent_dir = os.path.dirname(normalized_path)
275
+ if parent_dir == normalized_pattern:
276
+ return True
277
+
278
+ return False
279
+
280
+ def ensure_backup_dir():
281
+ """确保备份临时目录存在"""
282
+ os.makedirs(BACKUP_DIR, exist_ok=True)
283
+
284
+ def get_backup_timestamp():
285
+ """获取备份文件的时间戳(从文件名中提取)"""
286
+ def extract_timestamp(filename):
287
+ # 匹配新格式: backup_2026-03-23_1774234484.tar.gz
288
+ match = re.search(r'backup_(\d{4}-\d{2}-\d{2})_(\d+)\.tar\.gz', filename)
289
+ if match:
290
+ date_part, timestamp = match.groups()
291
+ return int(timestamp)
292
+ # 匹配旧格式: backup_2026-03-22.tar.gz (兼容)
293
+ match_old = re.search(r'backup_(\d{4}-\d{2}-\d{2})\.tar\.gz', filename)
294
+ if match_old:
295
+ # 旧格式没有时间戳,返回日期对应的秒数作为排序依据
296
+ date_str = match_old.group(1)
297
+ try:
298
+ dt = datetime.strptime(date_str, "%Y-%m-%d")
299
+ return int(dt.timestamp())
300
+ except:
301
+ return 0
302
+ return 0
303
+ return extract_timestamp
304
+
305
+ def get_latest_backup(files):
306
+ """获取最新的备份文件(按时间戳排序)"""
307
+ if not files:
308
+ return None
309
+
310
+ extract_timestamp = get_backup_timestamp()
311
+
312
+ # 按时间戳排序,最新的在前
313
+ files_with_timestamp = [(f, extract_timestamp(f)) for f in files]
314
+ files_with_timestamp.sort(key=lambda x: x[1], reverse=True)
315
+
316
+ return files_with_timestamp[0][0] if files_with_timestamp else None
317
+
318
+ def restore():
319
+ """从 Hugging Face Dataset 恢复最新的 .openclaw 目录"""
320
+ if not repo_id or not token:
321
+ print("⚠️ HF_DATASET 或 HF_TOKEN 未设置,跳过恢复")
322
+ return False
323
+
324
+ if not os.path.exists(OPENCLAW_DIR):
325
+ os.makedirs(OPENCLAW_DIR, mode=0o755, exist_ok=True)
326
+
327
+ try:
328
+ # 列出数据集中的所有文件
329
+ files = api.list_repo_files(repo_id=repo_id, repo_type="dataset", token=token)
330
+ backup_files = [f for f in files if (f.startswith("backup_") and f.endswith(".tar.gz"))]
331
+
332
+ if not backup_files:
333
+ print("ℹ️ 没有找到备份文件")
334
+ return False
335
+
336
+ # 按时间戳排序,获取最新的备份
337
+ latest_backup = get_latest_backup(backup_files)
338
+
339
+ if not latest_backup:
340
+ print("⚠️ 无法识别任何备份文件")
341
+ return False
342
+
343
+ print(f"📥 发现最新备份: {latest_backup}")
344
+
345
+ # 下载备份文件
346
+ ensure_backup_dir()
347
+ local_backup_path = os.path.join(BACKUP_DIR, latest_backup)
348
+
349
+ print(f"⬇️ 下载备份文件...")
350
+ downloaded_path = hf_hub_download(
351
+ repo_id=repo_id,
352
+ filename=latest_backup,
353
+ repo_type="dataset",
354
+ token=token,
355
+ local_dir=BACKUP_DIR,
356
+ local_dir_use_symlinks=False
357
+ )
358
+
359
+ # 创建临时恢复目录
360
+ restore_temp = os.path.join(BACKUP_DIR, "restore_temp")
361
+ if os.path.exists(restore_temp):
362
+ shutil.rmtree(restore_temp)
363
+ os.makedirs(restore_temp)
364
+
365
+ # 解压备份文件
366
+ print(f"📦 解压备份文件...")
367
+ with tarfile.open(downloaded_path, "r:gz") as tar:
368
+ tar.extractall(path=restore_temp)
369
+
370
+ # 检查解压后的内容
371
+ extracted_items = os.listdir(restore_temp)
372
+ print(f"解压内容: {extracted_items}")
373
+
374
+ # 情况1: 解压后直接是 .openclaw 目录
375
+ if ".openclaw" in extracted_items:
376
+ source_dir = os.path.join(restore_temp, ".openclaw")
377
+ # 情况2: 解压后是目录内容(sessions, openclaw.json 等)
378
+ elif set(extracted_items) & {"sessions", "openclaw.json", "workspace"}:
379
+ source_dir = restore_temp
380
+ else:
381
+ print(f"❌ 无法识别的备份格式: {extracted_items}")
382
+ return False
383
+
384
+ # 备份当前目录(如果需要)
385
+ if os.path.exists(OPENCLAW_DIR) and os.listdir(OPENCLAW_DIR):
386
+ backup_current = os.path.join(BACKUP_DIR, "current_before_restore")
387
+ if os.path.exists(backup_current):
388
+ shutil.rmtree(backup_current)
389
+ shutil.copytree(OPENCLAW_DIR, backup_current)
390
+ print(f"💾 已备份当前目录到: {backup_current}")
391
+
392
+ # 清空并恢复目标目录
393
+ print(f"🔄 恢复数据到 {OPENCLAW_DIR}...")
394
+ if os.path.exists(OPENCLAW_DIR):
395
+ # 删除所有内容但不删除目录本身
396
+ for item in os.listdir(OPENCLAW_DIR):
397
+ item_path = os.path.join(OPENCLAW_DIR, item)
398
+ if os.path.isfile(item_path):
399
+ os.remove(item_path)
400
+ elif os.path.isdir(item_path):
401
+ shutil.rmtree(item_path)
402
+
403
+ # 复制所有文件
404
+ for item in os.listdir(source_dir):
405
+ src = os.path.join(source_dir, item)
406
+ dst = os.path.join(OPENCLAW_DIR, item)
407
+ if os.path.isdir(src):
408
+ shutil.copytree(src, dst)
409
+ else:
410
+ shutil.copy2(src, dst)
411
+
412
+ print(f"✅ 恢复完成!")
413
+
414
+ # 清理临时文件
415
+ shutil.rmtree(restore_temp)
416
+ os.remove(downloaded_path)
417
+
418
+ return True
419
+
420
+ except Exception as e:
421
+ print(f"❌ 恢复失败: {e}")
422
+ return False
423
+
424
+ def backup():
425
+ """备份整个 .openclaw 目录到 Hugging Face Dataset(支持排除列表)"""
426
+ if not repo_id or not token:
427
+ print("⚠️ HF_DATASET 或 HF_TOKEN 未设置,跳过备份")
428
+ return
429
+
430
+ if not os.path.exists(OPENCLAW_DIR) or not os.listdir(OPENCLAW_DIR):
431
+ print("ℹ️ .openclaw 目录为空,跳过备份")
432
+ return
433
+
434
+ try:
435
+ # 获取排除列表
436
+ exclude_patterns = get_exclude_patterns()
437
+
438
+ # 生成备份文件名:backup_日期_时间戳.tar.gz
439
+ today = datetime.now().strftime("%Y-%m-%d")
440
+ timestamp = int(datetime.now().timestamp())
441
+ backup_filename = f"backup_{today}_{timestamp}.tar.gz"
442
+ backup_path = os.path.join(BACKUP_DIR, backup_filename)
443
+
444
+ ensure_backup_dir()
445
+
446
+ print(f"📦 创建备份: {backup_filename}")
447
+
448
+ # 统计排除的文件/目录数量
449
+ excluded_count = 0
450
+ included_count = 0
451
+
452
+ # 创建备份(带排除功能)
453
+ with tarfile.open(backup_path, "w:gz") as tar:
454
+ # 遍历 .openclaw 目录
455
+ for root, dirs, files in os.walk(OPENCLAW_DIR):
456
+ # 检查当前目录是否应该被排除
457
+ if should_exclude(root, exclude_patterns):
458
+ print(f" ⏭️ 排除目录: {root}")
459
+ excluded_count += 1
460
+ # 跳过整个目录树
461
+ dirs[:] = []
462
+ continue
463
+
464
+ # 添加文件
465
+ for file in files:
466
+ file_path = os.path.join(root, file)
467
+ arcname = os.path.relpath(file_path, start=os.path.dirname(OPENCLAW_DIR))
468
+
469
+ # 检查文件是否应该被排除
470
+ if should_exclude(file_path, exclude_patterns):
471
+ print(f" ⏭️ 排除文件: {file_path}")
472
+ excluded_count += 1
473
+ continue
474
+
475
+ # 添加到压缩包
476
+ tar.add(file_path, arcname=arcname)
477
+ included_count += 1
478
+
479
+ print(f"📊 备份统计:")
480
+ print(f" • 已包含: {included_count} 个文件/目录")
481
+ print(f" • 已排除: {excluded_count} 个文件/目录")
482
+
483
+ # 上传到 Hugging Face
484
+ print(f"⬆️ 上传到 Hugging Face Dataset: {repo_id}")
485
+ api.upload_file(
486
+ path_or_fileobj=backup_path,
487
+ path_in_repo=backup_filename,
488
+ repo_id=repo_id,
489
+ repo_type="dataset",
490
+ token=token
491
+ )
492
+
493
+ print(f"✅ 备份完成: {backup_filename}")
494
+
495
+ # 清理旧备份
496
+ try:
497
+ files = api.list_repo_files(repo_id=repo_id, repo_type="dataset", token=token)
498
+ backup_files = [f for f in files if f.startswith("backup_") and f.endswith(".tar.gz")]
499
+
500
+ # 按时间戳排序(使用提取函数)
501
+ extract_timestamp = get_backup_timestamp()
502
+ backup_files_with_ts = [(f, extract_timestamp(f)) for f in backup_files]
503
+ backup_files_with_ts.sort(key=lambda x: x[1])
504
+
505
+ # 删除超过保留天数的备份
506
+ now = datetime.now().timestamp()
507
+ while backup_files_with_ts and len(backup_files_with_ts) > retention_days:
508
+ oldest_file, oldest_ts = backup_files_with_ts.pop(0)
509
+ # 检查是否超过保留天数
510
+ if (now - oldest_ts) > (retention_days * 86400):
511
+ print(f"🗑️ 删除旧备份: {oldest_file}")
512
+ api.delete_file(
513
+ path_in_repo=oldest_file,
514
+ repo_id=repo_id,
515
+ repo_type="dataset",
516
+ token=token
517
+ )
518
+ else:
519
+ break
520
+
521
+ except Exception as e:
522
+ print(f"⚠️ 清理旧备份失败: {e}")
523
+
524
+ # 删除本地临时文件
525
+ os.remove(backup_path)
526
+
527
+ except Exception as e:
528
+ print(f"❌ 备份失败: {e}")
529
+
530
+ def list_backups():
531
+ """列出所有可用的备份"""
532
+ if not repo_id or not token:
533
+ print("⚠️ HF_DATASET 或 HF_TOKEN 未设置")
534
+ return
535
+
536
+ try:
537
+ files = api.list_repo_files(repo_id=repo_id, repo_type="dataset", token=token)
538
+ backup_files = [f for f in files if f.startswith("backup_") and f.endswith(".tar.gz")]
539
+
540
+ # 按时间戳排序
541
+ extract_timestamp = get_backup_timestamp()
542
+ backup_files_with_ts = [(f, extract_timestamp(f)) for f in backup_files]
543
+ backup_files_with_ts.sort(key=lambda x: x[1], reverse=True)
544
+
545
+ if backup_files_with_ts:
546
+ print("📋 可用的备份:")
547
+ for i, (f, ts) in enumerate(backup_files_with_ts, 1):
548
+ if ts:
549
+ # 显示人类可读的时间
550
+ dt = datetime.fromtimestamp(ts)
551
+ print(f" {i}. {f} ({dt.strftime('%Y-%m-%d %H:%M:%S')})")
552
+ else:
553
+ print(f" {i}. {f}")
554
+ else:
555
+ print("ℹ️ 没有找到备份文件")
556
+
557
+ except Exception as e:
558
+ print(f"❌ 列出备份失败: {e}")
559
+
560
+ if __name__ == "__main__":
561
+ if len(sys.argv) > 1:
562
+ if sys.argv[1] == "backup":
563
+ backup()
564
+ elif sys.argv[1] == "restore":
565
+ restore()
566
+ elif sys.argv[1] == "list":
567
+ list_backups()
568
+ else:
569
+ print(f"未知命令: {sys.argv[1]}")
570
+ print("可用命令: backup, restore, list")
571
+ else:
572
+ # 默认执行恢复
573
+ restore()
574
+ SYNC_EOF
575
+ RUN chmod +x /usr/local/bin/sync.py
576
+
577
+ # 8. Telegram API 替换脚本
578
+ RUN cat > /usr/local/bin/patch-telegram-api << 'PATCH_EOF'
579
+ #!/bin/bash
580
+
581
+ # 如果设置了 TELEGRAM_PROXY_HOST,则替换所有 Telegram API 地址
582
+ if [ -n "$TELEGRAM_PROXY_HOST" ]; then
583
+ echo "🔧 检测到 TELEGRAM_PROXY_HOST 设置: $TELEGRAM_PROXY_HOST"
584
+ echo "🔄 开始替换 OpenClaw 中的 Telegram API 地址..."
585
+
586
+ OPENCLAW_DIR="/usr/local/lib/node_modules/openclaw"
587
+
588
+ if [ -d "$OPENCLAW_DIR" ]; then
589
+ # 统计替换前的匹配数量
590
+ MATCH_COUNT=$(grep -r "api.telegram.org" "$OPENCLAW_DIR" 2>/dev/null | wc -l)
591
+ echo "📊 找到 $MATCH_COUNT 处需要替换的地址"
592
+
593
+ # 执行替换
594
+ find "$OPENCLAW_DIR" -type f -name "*.js" -exec sed -i "s|api\\.telegram\\.org|$TELEGRAM_PROXY_HOST|g" {} +
595
+
596
+ # 再次检查是否还有未替换的
597
+ REMAINING=$(grep -r "api.telegram.org" "$OPENCLAW_DIR" 2>/dev/null | wc -l)
598
+
599
+ if [ "$REMAINING" -eq 0 ]; then
600
+ echo "✅ Telegram API 地址替换完成!"
601
+ echo " 原始地址: api.telegram.org"
602
+ echo " 新地址: $TELEGRAM_PROXY_HOST"
603
+ else
604
+ echo "⚠️ 仍有 $REMAINING 处未替换,尝试二次替换..."
605
+ find "$OPENCLAW_DIR" -type f \( -name "*.js" -o -name "*.json" -o -name "*.ts" \) -exec sed -i "s|api.telegram.org|$TELEGRAM_PROXY_HOST|g" {} +
606
+
607
+ FINAL_REMAINING=$(grep -r "api.telegram.org" "$OPENCLAW_DIR" 2>/dev/null | wc -l)
608
+ echo "📊 最终剩余未替换: $FINAL_REMAINING 处"
609
+ fi
610
+
611
+ # 验证替换结果
612
+ echo "🔍 验证替换结果(前3处):"
613
+ grep -r "$TELEGRAM_PROXY_HOST" "$OPENCLAW_DIR" 2>/dev/null | head -3 | sed 's|.*| &|'
614
+ else
615
+ echo "❌ OpenClaw 目录不存在: $OPENCLAW_DIR"
616
+ fi
617
+ else
618
+ echo "ℹ️ 未设置 TELEGRAM_PROXY_HOST,跳过 Telegram API 替换"
619
+ fi
620
+ PATCH_EOF
621
+ RUN chmod +x /usr/local/bin/patch-telegram-api
622
+
623
+ # 9. 启动脚本
624
+ RUN cat > /usr/local/bin/start-openclaw << 'START_EOF'
625
+ #!/bin/bash
626
+ set -e
627
+
628
+ # 创建必要的目录
629
+ mkdir -p /root/.openclaw
630
+ mkdir -p /root/.openclaw/sessions
631
+ mkdir -p /root/.openclaw/workspace
632
+
633
+ echo "========================================"
634
+ echo "OpenClaw Gateway Starting..."
635
+ echo "========================================"
636
+
637
+ # 尝试恢复数据(如果配置了备份)
638
+ if [ -n "$HF_DATASET" ] && [ -n "$HF_TOKEN" ]; then
639
+ echo "🔄 尝试从 Hugging Face 恢复数据..."
640
+ python3 /usr/local/bin/sync.py restore || echo "⚠️ 恢复失败,继续启动..."
641
+ fi
642
+
643
+ # 执行 Telegram API 地址替换(如果设置了代理)
644
+ /usr/local/bin/patch-telegram-api
645
+
646
+ # 生成令牌(如果没设置)
647
+ if [ -z "$OPENCLAW_GATEWAY_TOKEN" ]; then
648
+ OPENCLAW_GATEWAY_TOKEN=$(openssl rand -hex 16)
649
+ echo "🔑 生成的网关令牌: $OPENCLAW_GATEWAY_TOKEN"
650
+ fi
651
+
652
+ # 转换可信代理列表为JSON数组
653
+ TRUSTED_PROXIES_JSON=$(echo "$GATEWAY_TRUSTED_PROXIES" | tr ',' '\n' | awk '{ printf "\"%s\",", $0 }' | sed 's/,$//' | sed 's/^/[/' | sed 's/$/]/')
654
+
655
+ # 转换允许的源列表为JSON数组
656
+ ALLOWED_ORIGINS_JSON=$(echo "$GATEWAY_ALLOWED_ORIGINS" | tr ',' '\n' | awk '{ printf "\"%s\",", $0 }' | sed 's/,$//' | sed 's/^/[/' | sed 's/$/]/')
657
+
658
+ # 转换Telegram允许列表为JSON数组
659
+ if [ -n "$TELEGRAM_ALLOW_FROM" ]; then
660
+ TELEGRAM_ALLOW_JSON=$(echo "$TELEGRAM_ALLOW_FROM" | tr ',' '\n' | while read id; do
661
+ id=$(echo "$id" | xargs)
662
+ if [[ "$id" =~ ^[0-9]+$ ]]; then
663
+ echo "\"tg:$id\""
664
+ elif [[ "$id" =~ ^tg: ]]; then
665
+ echo "\"$id\""
666
+ elif [[ "$id" =~ ^@ ]]; then
667
+ echo "\"$id\""
668
+ else
669
+ echo "\"$id\""
670
+ fi
671
+ done | paste -sd ',' | sed 's/^/[/' | sed 's/$/]/')
672
+ else
673
+ TELEGRAM_ALLOW_JSON="[]"
674
+ fi
675
+
676
+ # 创建 OpenClaw 配置(如果配置文件不存在)
677
+ if [ ! -f "/root/.openclaw/openclaw.json" ]; then
678
+ echo "📝 创建 OpenClaw 配置文件..."
679
+
680
+ # 验证模型配置
681
+ echo "🔍 验证模型配置..."
682
+ if python3 /usr/local/bin/parse-models-config.py --validate; then
683
+ # 生成完整的模型配置
684
+ echo "✅ 模型配置验证成功,正在生成配置..."
685
+ MODELS_FULL_CONFIG=$(python3 /usr/local/bin/parse-models-config.py --full)
686
+
687
+ # 提取 models 和 agents 部分
688
+ MODELS_JSON=$(echo "$MODELS_FULL_CONFIG" | jq '.models')
689
+ AGENTS_JSON=$(echo "$MODELS_FULL_CONFIG" | jq '.agents')
690
+
691
+ # 创建完整配置
692
+ cat > /root/.openclaw/openclaw.json <<OPENCLAW_CONFIG
693
+ {
694
+ "models": $MODELS_JSON,
695
+ "agents": $AGENTS_JSON,
696
+ "gateway": {
697
+ "mode": "${OPENCLAW_GATEWAY_MODE}",
698
+ "bind": "${GATEWAY_BIND}",
699
+ "port": ${PORT},
700
+ "trustedProxies": ${TRUSTED_PROXIES_JSON},
701
+ "auth": {
702
+ "mode": "${GATEWAY_AUTH_MODE}",
703
+ "token": "${OPENCLAW_GATEWAY_TOKEN}"
704
+ },
705
+ "remote": {
706
+ "token": "${OPENCLAW_GATEWAY_TOKEN}"
707
+ },
708
+ "controlUi": {
709
+ "allowInsecureAuth": ${CONTROLUI_ALLOW_INSECURE_AUTH},
710
+ "dangerouslyAllowHostHeaderOriginFallback": ${CONTROLUI_DANGEROUS_HOST_HEADER},
711
+ "dangerouslyDisableDeviceAuth": ${CONTROLUI_DANGEROUS_DISABLE_DEVICE_AUTH},
712
+ "allowedOrigins": ${ALLOWED_ORIGINS_JSON}
713
+ }
714
+ },
715
+ "session": {
716
+ "store": "/root/.openclaw/sessions/sessions.json"
717
+ },
718
+ "channels": {
719
+ "telegram": {
720
+ "enabled": ${TELEGRAM_ENABLED},
721
+ "botToken": "${TELEGRAM_BOT_TOKEN}",
722
+ "dmPolicy": "${TELEGRAM_DM_POLICY}",
723
+ "allowFrom": ${TELEGRAM_ALLOW_JSON}
724
+ }
725
+ }
726
+ }
727
+ OPENCLAW_CONFIG
728
+
729
+ # 显示配置摘要
730
+ PROVIDER_COUNT=$(echo "$MODELS_JSON" | jq '.providers | length')
731
+ MODEL_COUNT=$(echo "$AGENTS_JSON" | jq '.defaults.models | length')
732
+ PRIMARY_MODEL=$(echo "$AGENTS_JSON" | jq -r '.defaults.model.primary')
733
+ MODEL_MODE=$(echo "$MODELS_JSON" | jq -r '.mode')
734
+
735
+ echo "✅ 配置已生成:"
736
+ echo " • 模式: $MODEL_MODE"
737
+ echo " • Providers: $PROVIDER_COUNT"
738
+ echo " • 模型总数: $MODEL_COUNT"
739
+ echo " • 主模型: $PRIMARY_MODEL"
740
+ else
741
+ echo "❌ 模型配置验证失败,使用最小配置"
742
+ # 创建最小配置(空配置,让 OpenClaw 使用默认行为)
743
+ cat > /root/.openclaw/openclaw.json <<OPENCLAW_CONFIG
744
+ {
745
+ "models": {
746
+ "mode": "merge",
747
+ "providers": {}
748
+ },
749
+ "agents": {
750
+ "defaults": {
751
+ "model": {
752
+ "primary": ""
753
+ },
754
+ "models": {},
755
+ "workspace": "/root/.openclaw/workspace"
756
+ }
757
+ },
758
+ "gateway": {
759
+ "mode": "${OPENCLAW_GATEWAY_MODE}",
760
+ "bind": "${GATEWAY_BIND}",
761
+ "port": ${PORT},
762
+ "trustedProxies": ${TRUSTED_PROXIES_JSON},
763
+ "auth": {
764
+ "mode": "${GATEWAY_AUTH_MODE}",
765
+ "token": "${OPENCLAW_GATEWAY_TOKEN}"
766
+ },
767
+ "remote": {
768
+ "token": "${OPENCLAW_GATEWAY_TOKEN}"
769
+ },
770
+ "controlUi": {
771
+ "allowInsecureAuth": ${CONTROLUI_ALLOW_INSECURE_AUTH},
772
+ "dangerouslyAllowHostHeaderOriginFallback": ${CONTROLUI_DANGEROUS_HOST_HEADER},
773
+ "dangerouslyDisableDeviceAuth": ${CONTROLUI_DANGEROUS_DISABLE_DEVICE_AUTH},
774
+ "allowedOrigins": ${ALLOWED_ORIGINS_JSON}
775
+ }
776
+ },
777
+ "session": {
778
+ "store": "/root/.openclaw/sessions/sessions.json"
779
+ },
780
+ "channels": {
781
+ "telegram": {
782
+ "enabled": ${TELEGRAM_ENABLED},
783
+ "botToken": "${TELEGRAM_BOT_TOKEN}",
784
+ "dmPolicy": "${TELEGRAM_DM_POLICY}",
785
+ "allowFrom": ${TELEGRAM_ALLOW_JSON}
786
+ }
787
+ }
788
+ }
789
+ OPENCLAW_CONFIG
790
+ echo "⚠️ 使用了最小配置"
791
+ fi
792
+ else
793
+ echo "📁 使用现有的配置文件"
794
+ fi
795
+
796
+ echo "========================================"
797
+ echo "📊 配置信息:"
798
+ echo " • 端口: ${PORT}"
799
+ echo " • 备份: ${BACKUP_ENABLED}"
800
+ echo " • 备份排除策略: 已配置"
801
+ echo " • Allowed Origins: ${GATEWAY_ALLOWED_ORIGINS}"
802
+ echo " • Telegram Enabled: ${TELEGRAM_ENABLED}"
803
+ if [ "${TELEGRAM_ENABLED}" = "true" ]; then
804
+ echo " • Telegram DM Policy: ${TELEGRAM_DM_POLICY}"
805
+ echo " • Telegram Allow From: ${TELEGRAM_ALLOW_FROM}"
806
+ if [ -n "$TELEGRAM_PROXY_HOST" ]; then
807
+ echo " • Telegram Proxy: ${TELEGRAM_PROXY_HOST}"
808
+ fi
809
+ fi
810
+ echo "========================================"
811
+
812
+ # 后台备份循环(如果启用)
813
+ if [ "${BACKUP_ENABLED}" = "true" ] && [ -n "$HF_DATASET" ] && [ -n "$HF_TOKEN" ]; then
814
+ echo "🔄 启动自动备份 (间隔: ${BACKUP_INTERVAL}秒)"
815
+ (
816
+ while true; do
817
+ sleep ${BACKUP_INTERVAL}
818
+ echo "⏰ 执行定时备份..."
819
+ python3 /usr/local/bin/sync.py backup || echo "⚠️ 备份失败"
820
+ done
821
+ ) &
822
+ fi
823
+
824
+ # 运行诊断并启动网关
825
+ echo "🔧 运行 OpenClaw ���断..."
826
+ openclaw doctor --fix || true
827
+
828
+ echo "🚀 启动 OpenClaw Gateway..."
829
+ echo "========================================"
830
+ exec openclaw gateway run --port $PORT
831
+ START_EOF
832
+ RUN chmod +x /usr/local/bin/start-openclaw
833
+
834
+ EXPOSE 7860
835
+
836
+ CMD ["/usr/local/bin/start-openclaw"]