erayoc commited on
Commit
ba81b8f
·
verified ·
1 Parent(s): 52fe381

Create Dockerfile

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