wenyin commited on
Commit
eaa272b
·
verified ·
1 Parent(s): 42cefa7

Upload 3 files

Browse files
Files changed (3) hide show
  1. Dockerfile +46 -0
  2. start-openclaw.sh +212 -0
  3. sync.py +161 -0
Dockerfile ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:22-slim
2
+
3
+ # ─── 1. 系统基础依赖 ──────────────────────────────────────────────────────────
4
+ RUN apt-get update && apt-get install -y --no-install-recommends \
5
+ git openssh-client build-essential python3 python3-pip \
6
+ make g++ curl wget ca-certificates tini procps jq unzip zip \
7
+ locales \
8
+ && sed -i '/zh_CN.UTF-8/s/^# //g' /etc/locale.gen \
9
+ && locale-gen \
10
+ && rm -rf /var/lib/apt/lists/*
11
+
12
+ # ─── 2. Python 工具 ───────────────────────────────────────────────────────────
13
+ RUN pip3 install --no-cache-dir huggingface_hub requests httpx --break-system-packages
14
+
15
+ # ─── 3. 全局安装 OpenClaw ─────────────────────────────────────────────────────
16
+ RUN npm install -g openclaw@latest --registry https://registry.npmmirror.com && npm cache clean --force
17
+
18
+ # ─── 4. 环境变量 ──────────────────────────────────────────────────────────────
19
+ ENV LANG=zh_CN.UTF-8 \
20
+ LANGUAGE=zh_CN:zh \
21
+ LC_ALL=zh_CN.UTF-8 \
22
+ HOME=/home/node \
23
+ PORT=7860 \
24
+ NPM_CONFIG_REGISTRY=https://registry.npmmirror.com
25
+
26
+ # ─── 5. Git 强制 HTTPS ────────────────────────────────────────────────────────
27
+ RUN git config --global url."https://github.com/".insteadOf ssh://git@github.com/ \
28
+ && git config --global url."https://github.com/".insteadOf git@github.com:
29
+
30
+ # ─── 6. 工作目录 & 脚本 ──────────────────────────────────────────────────────
31
+ WORKDIR /app
32
+ COPY --chown=node:node sync.py .
33
+ COPY --chown=node:node start-openclaw.sh .
34
+ RUN chmod +x start-openclaw.sh
35
+
36
+ # ─── 7. 权限调整 ──────────────────────────────────────────────────────────────
37
+ RUN mkdir -p /home/node/.openclaw \
38
+ && chown -R node:node /home/node
39
+
40
+ # ─── 8. 切换至非 root 用户(内置 node 用户 UID = 1000,符合 HF 规范)─────────
41
+ USER 1000
42
+
43
+ # ─── 9. 暴露端口并启动 ───────────────────────────────────────────────────────
44
+ EXPOSE 7860
45
+ ENTRYPOINT ["tini", "--"]
46
+ CMD ["./start-openclaw.sh"]
start-openclaw.sh ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ BASE_DIR="/home/node/.openclaw"
5
+
6
+ # ─── 清理函数:退出时强制杀掉所有后台任务 ────────────────────────────────────
7
+ cleanup() {
8
+ echo "[cleanup] Received exit signal, cleaning up..."
9
+
10
+ if [ -n "$BACKUP_PID" ]; then
11
+ kill "$BACKUP_PID" 2>/dev/null || true
12
+ echo "[cleanup] Backup process $BACKUP_PID stopped."
13
+ fi
14
+
15
+ echo "[cleanup] Done."
16
+ }
17
+
18
+ trap cleanup EXIT SIGTERM SIGINT
19
+
20
+ # ─── 1. 创建必要的目录结构 ────────────────────────────────────────────────────
21
+ mkdir -p $BASE_DIR/agents/main/sessions
22
+ mkdir -p $BASE_DIR/credentials
23
+ mkdir -p $BASE_DIR/sessions
24
+ mkdir -p $BASE_DIR/workspace
25
+ mkdir -p $BASE_DIR/extensions
26
+
27
+ # ─── 2. 执行恢复(从 HF Dataset 恢复旧的会话和配置) ─────────────────────────
28
+ python3 /app/sync.py restore || true
29
+
30
+ # ─── 3. 生成 openclaw.json(仅在文件不存在时) ───────────────────────────────
31
+ if [ ! -f $BASE_DIR/openclaw.json ]; then
32
+ echo "openclaw.json not found, generating..."
33
+ cat > $BASE_DIR/openclaw.json <<EOF
34
+ {
35
+ "meta": {
36
+ "lastTouchedVersion": "2026.3.13",
37
+ "lastTouchedAt": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
38
+ },
39
+ "wizard": {
40
+ "lastRunAt": "2026-03-22T14:56:52.442Z",
41
+ "lastRunVersion": "2026.3.13",
42
+ "lastRunCommand": "onboard",
43
+ "lastRunMode": "local"
44
+ },
45
+ "models": {
46
+ "mode": "merge",
47
+ "providers": {
48
+ "custom-nvidia": {
49
+ "baseUrl": "https://integrate.api.nvidia.com/v1",
50
+ "apiKey": "${NVIDIA_API_KEY}",
51
+ "api": "openai-completions",
52
+ "models": [
53
+ {
54
+ "id": "minimaxai/minimax-m2.5",
55
+ "name": "minimaxai/minimax-m2.5 (Custom Provider)",
56
+ "reasoning": false,
57
+ "input": ["text"],
58
+ "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 },
59
+ "contextWindow": 200000,
60
+ "maxTokens": 8192
61
+ },
62
+ {
63
+ "id": "nvidia/nemotron-3-super-120b-a12b",
64
+ "input": ["text", "image"],
65
+ "name": "nvidia/nemotron-3-super-120b-a12b",
66
+ "reasoning": false,
67
+ "contextWindow": 200000,
68
+ "maxTokens": 16384
69
+ },
70
+ {
71
+ "id": "openai/gpt-oss-120b",
72
+ "input": ["text", "image"],
73
+ "name": "openai/gpt-oss-120b",
74
+ "reasoning": false,
75
+ "contextWindow": 200000,
76
+ "maxTokens": 16384
77
+ },
78
+ {
79
+ "id": "qwen/qwen3.5-397b-a17b",
80
+ "input": ["text", "image"],
81
+ "name": "qwen/qwen3.5-397b-a17b",
82
+ "reasoning": false,
83
+ "contextWindow": 200000,
84
+ "maxTokens": 16384
85
+ }
86
+ ]
87
+ },
88
+ "gemini-auth": {
89
+ "baseUrl": "http://107.173.211.130:3000/gemini-cli-oauth/v1",
90
+ "apiKey": "${GEMINI_API_KEY}",
91
+ "api": "openai-completions",
92
+ "models": [
93
+ {
94
+ "id": "gemini-3-flash-preview",
95
+ "input": ["text", "image"],
96
+ "name": "gemini-3-flash-preview",
97
+ "reasoning": false,
98
+ "contextWindow": 200000,
99
+ "maxTokens": 16384
100
+ },
101
+ {
102
+ "id": "gemini-3.1-flash-lite-preview",
103
+ "input": ["text", "image"],
104
+ "name": "gemini-3.1-flash-lite-preview",
105
+ "reasoning": false,
106
+ "contextWindow": 200000,
107
+ "maxTokens": 16384
108
+ }
109
+ ]
110
+ },
111
+ "qwen-auth": {
112
+ "baseUrl": "http://107.173.211.130:3000/openai-qwen-oauth/v1",
113
+ "apiKey": "${QWEN_API_KEY}",
114
+ "api": "openai-completions",
115
+ "models": [
116
+ {
117
+ "id": "qwen3-coder-flash",
118
+ "input": ["text", "image"],
119
+ "name": "qwen3-coder-flash",
120
+ "reasoning": false,
121
+ "contextWindow": 200000,
122
+ "maxTokens": 16384
123
+ }
124
+ ]
125
+ }
126
+ }
127
+ },
128
+ "agents": {
129
+ "defaults": {
130
+ "model": {
131
+ "primary": "gemini-auth/gemini-3.1-flash-lite-preview",
132
+ "fallbacks": [
133
+ "custom-nvidia/minimaxai/minimax-m2.5",
134
+ "custom-nvidia/nvidia/nemotron-3-super-120b-a12b",
135
+ "custom-nvidia/openai/gpt-oss-120b",
136
+ "custom-nvidia/qwen/qwen3.5-397b-a17b",
137
+ "gemini-auth/gemini-3-flash-preview",
138
+ "qwen-auth/qwen3-coder-flash"
139
+ ]
140
+ },
141
+ "models": {
142
+ "gemini-auth/gemini-3.1-flash-lite-preview": {},
143
+ "custom-nvidia/minimaxai/minimax-m2.5": {},
144
+ "custom-nvidia/nvidia/nemotron-3-super-120b-a12b": {},
145
+ "custom-nvidia/openai/gpt-oss-120b": {},
146
+ "custom-nvidia/qwen/qwen3.5-397b-a17b": {},
147
+ "gemini-auth/gemini-3-flash-preview": {},
148
+ "qwen-auth/qwen3-coder-flash": {}
149
+ },
150
+ "workspace": "$BASE_DIR/workspace",
151
+ "compaction": { "mode": "safeguard" },
152
+ "sandbox": { "mode": "off" }
153
+ }
154
+ },
155
+ "tools": {
156
+ "profile": "full",
157
+ "sessions": { "visibility": "all" }
158
+ },
159
+ "commands": {
160
+ "native": "auto",
161
+ "nativeSkills": "auto",
162
+ "restart": true,
163
+ "ownerDisplay": "raw"
164
+ },
165
+ "session": { "dmScope": "per-channel-peer" },
166
+ "gateway": {
167
+ "port": 7860,
168
+ "mode": "local",
169
+ "bind": "lan",
170
+ "auth": {
171
+ "mode": "token",
172
+ "token": "${OPENCLAW_TOKEN}"
173
+ },
174
+ "nodes": {
175
+ "denyCommands": [
176
+ "camera.snap", "camera.clip", "screen.record", "contacts.add",
177
+ "calendar.add", "reminders.add", "sms.send"
178
+ ]
179
+ },
180
+ "controlUi": {
181
+ "allowedOrigins": ["*"],
182
+ "allowInsecureAuth": true,
183
+ "dangerouslyDisableDeviceAuth": true,
184
+ "dangerouslyAllowHostHeaderOriginFallback": true
185
+ }
186
+ }
187
+ }
188
+ EOF
189
+ else
190
+ echo "openclaw.json already exists (restored from backup), skipping."
191
+ fi
192
+
193
+ # ─── 4. 安装微信插件(跳过已安装) ───────────────────────────────────────────
194
+ WEIXIN_FLAG="$BASE_DIR/.weixin_installed"
195
+ if [ ! -f "$WEIXIN_FLAG" ]; then
196
+ echo "[setup] Installing WeChat plugin..."
197
+ CI=true npx -y @tencent-weixin/openclaw-weixin-cli@latest install --force
198
+ touch "$WEIXIN_FLAG"
199
+ echo "[setup] WeChat plugin installed."
200
+ else
201
+ echo "[setup] WeChat plugin already installed, skipping."
202
+ fi
203
+
204
+ # ─── 5. 启动定时备份(每 1 小时) ────────────────────────────────────────────
205
+ (while true; do sleep 3600; python3 /app/sync.py backup; done) &
206
+ BACKUP_PID=$!
207
+ echo "[setup] Backup loop started (PID: $BACKUP_PID)"
208
+
209
+ # ─── 6. 运行自检并启动 ───────────────────────────────────────────────────────
210
+ openclaw doctor --fix
211
+
212
+ exec openclaw gateway run --port 7860 --bind lan
sync.py ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import tarfile
4
+ import hashlib
5
+ import logging
6
+ from datetime import datetime
7
+ from huggingface_hub import HfApi, hf_hub_download
8
+ from huggingface_hub.utils import EntryNotFoundError, RepositoryNotFoundError
9
+
10
+ # ── 日志配置
11
+ logging.basicConfig(
12
+ level=logging.INFO,
13
+ format="%(asctime)s [%(levelname)s] %(message)s",
14
+ datefmt="%Y-%m-%dT%H:%M:%SZ",
15
+ )
16
+ log = logging.getLogger("sync")
17
+
18
+ # ── 配置
19
+ api = HfApi()
20
+ repo_id = os.getenv("HF_DATASET")
21
+ token = os.getenv("HF_TOKEN")
22
+
23
+ FILENAME = "latest_backup.tar.gz"
24
+ BACKUP_PATH = f"/tmp/{FILENAME}"
25
+ BASE_DIR = "/home/node/.openclaw"
26
+
27
+ PATHS_TO_BACKUP = [
28
+ f"{BASE_DIR}/sessions",
29
+ f"{BASE_DIR}/agents/main/sessions",
30
+ f"{BASE_DIR}/credentials",
31
+ f"{BASE_DIR}/workspace",
32
+ f"{BASE_DIR}/extensions",
33
+ f"{BASE_DIR}/openclaw.json",
34
+ ]
35
+
36
+ # ── 工具函数
37
+ def _check_env() -> bool:
38
+ if not repo_id or not token:
39
+ log.warning("HF_DATASET 或 HF_TOKEN 未设置,跳过同步。")
40
+ return False
41
+ return True
42
+
43
+ def _sha256(path: str) -> str:
44
+ h = hashlib.sha256()
45
+ with open(path, "rb") as f:
46
+ for chunk in iter(lambda: f.read(65536), b""):
47
+ h.update(chunk)
48
+ return h.hexdigest()
49
+
50
+ def _verify_tar(path: str) -> bool:
51
+ try:
52
+ with tarfile.open(path, "r:gz") as tar:
53
+ members = tar.getmembers()
54
+ if not members:
55
+ log.warning("压缩包为空,跳过。")
56
+ return False
57
+ log.info(f"压缩包验证通过,共 {len(members)} 个条目。")
58
+ return True
59
+ except tarfile.TarError as e:
60
+ log.error(f"压缩包损坏: {e}")
61
+ return False
62
+
63
+ # ── restore
64
+ def restore() -> bool:
65
+ if not _check_env():
66
+ return False
67
+
68
+ log.info(f"开始恢复:从 {repo_id} 下载 {FILENAME} ...")
69
+
70
+ try:
71
+ path = hf_hub_download(
72
+ repo_id=repo_id,
73
+ filename=FILENAME,
74
+ repo_type="dataset",
75
+ token=token,
76
+ )
77
+ except (EntryNotFoundError, RepositoryNotFoundError):
78
+ log.info("仓库中尚无备份文件,首次运行,跳过恢复。")
79
+ return False
80
+ except Exception as e:
81
+ log.error(f"下载失败: {e}")
82
+ return False
83
+
84
+ if not _verify_tar(path):
85
+ log.error("备份文件验证失败,放弃解压。")
86
+ return False
87
+
88
+ log.info(f"文件 SHA-256: {_sha256(path)}")
89
+
90
+ try:
91
+ os.makedirs(BASE_DIR, exist_ok=True)
92
+ with tarfile.open(path, "r:gz") as tar:
93
+ # 兼容处理:如果你之前的备份带有 /root 路径,解压时会自动映射到当前目录
94
+ tar.extractall(path=BASE_DIR)
95
+ log.info(f"恢复成功 → {BASE_DIR}")
96
+ return True
97
+ except Exception as e:
98
+ log.error(f"解压失败: {e}")
99
+ return False
100
+
101
+ # ── backup
102
+ def backup() -> bool:
103
+ if not _check_env():
104
+ return False
105
+
106
+ existing = [p for p in PATHS_TO_BACKUP if os.path.exists(p)]
107
+ if not existing:
108
+ log.warning("所有备份路径均不存在,跳过备份。")
109
+ return False
110
+
111
+ log.info(f"开始备份,共 {len(existing)} 个路径...")
112
+
113
+ try:
114
+ with tarfile.open(BACKUP_PATH, "w:gz") as tar:
115
+ for p in existing:
116
+ # 剥离前缀,确保解压时不带绝对路径
117
+ arcname = p.replace(f"{BASE_DIR}/", "")
118
+ tar.add(p, arcname=arcname, recursive=True)
119
+ log.info(f" 已打包: {p} → {arcname}")
120
+ except Exception as e:
121
+ log.error(f"打包失败: {e}")
122
+ return False
123
+
124
+ if not _verify_tar(BACKUP_PATH):
125
+ log.error("生成的压缩包验证失败,取消上传。")
126
+ return False
127
+
128
+ log.info(f"压缩包大小: {os.path.getsize(BACKUP_PATH)/1024:.1f} KB,SHA-256: {_sha256(BACKUP_PATH)}")
129
+
130
+ try:
131
+ api.upload_file(
132
+ path_or_fileobj=BACKUP_PATH,
133
+ path_in_repo=FILENAME,
134
+ repo_id=repo_id,
135
+ repo_type="dataset",
136
+ token=token,
137
+ commit_message=f"backup {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC",
138
+ )
139
+ log.info(f"备份上传成功 → {repo_id}/{FILENAME}")
140
+ return True
141
+ except Exception as e:
142
+ log.error(f"上传失败: {e}")
143
+ return False
144
+ finally:
145
+ if os.path.exists(BACKUP_PATH):
146
+ os.remove(BACKUP_PATH)
147
+ log.info("本地临时文件已清理。")
148
+
149
+ # ── 入口
150
+ if __name__ == "__main__":
151
+ action = sys.argv[1] if len(sys.argv) > 1 else "restore"
152
+
153
+ if action == "backup":
154
+ success = backup()
155
+ elif action == "restore":
156
+ success = restore()
157
+ else:
158
+ log.error(f"未知命令: {action},用法: python sync.py [backup|restore]")
159
+ sys.exit(1)
160
+
161
+ sys.exit(0 if success else 1)