Elysiadev11 commited on
Commit
cdf9dc5
·
verified ·
1 Parent(s): c0c9fd7

Upload 5 files

Browse files
Files changed (5) hide show
  1. Dockerfile.txt +37 -0
  2. nginx.conf.template.txt +78 -0
  3. start.sh +326 -0
  4. sync-root-data.sh +89 -0
  5. webhook_server.py +55 -0
Dockerfile.txt ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:22-slim
2
+
3
+ ENV DEBIAN_FRONTEND=noninteractive \
4
+ HOME=/root \
5
+ PROXY_PORT=7860 \
6
+ OPENCLAW_PORT=8080 \
7
+ SYNC_INTERVAL=300 \
8
+ PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
9
+
10
+ RUN apt-get update && apt-get install -y --no-install-recommends \
11
+ ca-certificates \
12
+ curl \
13
+ gettext-base \
14
+ git \
15
+ nginx \
16
+ procps \
17
+ python3 \
18
+ rsync \
19
+ unzip \
20
+ wget \
21
+ && rm -rf /var/lib/apt/lists/*
22
+
23
+ RUN npm install -g openclaw@latest playwright \
24
+ && playwright install --with-deps chromium
25
+
26
+ WORKDIR /root/workspace
27
+
28
+ COPY start.sh /app/start.sh
29
+ COPY sync-root-data.sh /app/sync-root-data.sh
30
+ COPY nginx.conf.template /app/nginx.conf.template
31
+ COPY webhook_server.py /app/webhook_server.py
32
+
33
+ RUN chmod +x /app/start.sh /app/sync-root-data.sh /app/webhook_server.py
34
+
35
+ EXPOSE 7860
36
+
37
+ CMD ["/app/start.sh"]
nginx.conf.template.txt ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ worker_processes auto;
2
+ pid /tmp/nginx.pid;
3
+
4
+ events {
5
+ worker_connections 1024;
6
+ }
7
+
8
+ http {
9
+ access_log /dev/stdout;
10
+ error_log /dev/stderr info;
11
+ include /etc/nginx/mime.types;
12
+ default_type application/octet-stream;
13
+ sendfile on;
14
+ tcp_nodelay on;
15
+
16
+ map $http_upgrade $connection_upgrade {
17
+ default upgrade;
18
+ '' close;
19
+ }
20
+
21
+ server {
22
+ listen ${PROXY_PORT};
23
+ server_name _;
24
+ client_max_body_size 0;
25
+
26
+ proxy_http_version 1.1;
27
+ proxy_set_header Host $host;
28
+ proxy_set_header X-Real-IP $remote_addr;
29
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
30
+ proxy_set_header X-Forwarded-Proto $scheme;
31
+ proxy_set_header Authorization "Bearer ${OPENCLAW_GATEWAY_PASSWORD}";
32
+ proxy_set_header Upgrade $http_upgrade;
33
+ proxy_set_header Connection $connection_upgrade;
34
+ proxy_read_timeout 86400;
35
+ proxy_send_timeout 86400;
36
+ proxy_buffering off;
37
+
38
+ location = /tg-webhook {
39
+ proxy_set_header Authorization "";
40
+ proxy_pass http://127.0.0.1:8787/tg-webhook;
41
+ proxy_buffering off;
42
+ proxy_connect_timeout 60s;
43
+ proxy_send_timeout 60s;
44
+ proxy_read_timeout 60s;
45
+ }
46
+
47
+ location /tg-webhook/ {
48
+ proxy_set_header Authorization "";
49
+ proxy_pass http://127.0.0.1:8787/tg-webhook/;
50
+ proxy_buffering off;
51
+ proxy_connect_timeout 60s;
52
+ proxy_send_timeout 60s;
53
+ proxy_read_timeout 60s;
54
+ }
55
+
56
+ location ~ ^/proxy/([0-9]+)/(.*)$ {
57
+ proxy_set_header X-Forwarded-Prefix /proxy/$1;
58
+ proxy_pass http://127.0.0.1:$1/$2$is_args$args;
59
+ }
60
+
61
+ location ~ ^/proxy/([0-9]+)$ {
62
+ return 308 /proxy/$1/;
63
+ }
64
+
65
+ location ~ ^/([0-9]+)/(.*)$ {
66
+ proxy_set_header X-Forwarded-Prefix /$1;
67
+ proxy_pass http://127.0.0.1:$1/$2$is_args$args;
68
+ }
69
+
70
+ location ~ ^/([0-9]+)$ {
71
+ return 308 /$1/;
72
+ }
73
+
74
+ location / {
75
+ proxy_pass http://127.0.0.1:${OPENCLAW_PORT};
76
+ }
77
+ }
78
+ }
start.sh ADDED
@@ -0,0 +1,326 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ export PROXY_PORT="${PROXY_PORT:-7860}"
5
+ export OPENCLAW_PORT="${OPENCLAW_PORT:-8080}"
6
+ export PORT="${OPENCLAW_PORT}"
7
+ export HOST="0.0.0.0"
8
+ export OPENCLAW_HOST="0.0.0.0"
9
+ export OPENCLAW_PORT
10
+ export OPENCLAW_GATEWAY_PASSWORD="${OPENCLAW_GATEWAY_PASSWORD:-773322}"
11
+ export SYNC_INTERVAL="${SYNC_INTERVAL:-300}"
12
+ export PLAYWRIGHT_BROWSERS_PATH="${PLAYWRIGHT_BROWSERS_PATH:-/ms-playwright}"
13
+ export TELEGRAM_API_ROOT="${TELEGRAM_API_ROOT:-https://tg-relay.markdevil11.workers.dev}"
14
+ export OPENCLAW_TELEGRAM_API_ROOT="${OPENCLAW_TELEGRAM_API_ROOT:-${TELEGRAM_API_ROOT}}"
15
+ export SPACE_URL="${SPACE_URL:-https://elysiadev11-openclaw.hf.space}"
16
+ export TELEGRAM_BOT_TOKEN="${TELEGRAM_BOT_TOKEN:-}"
17
+ export TELEGRAM_ALLOW_ID="${TELEGRAM_ALLOW_ID:-0}"
18
+
19
+ configure_dns() {
20
+ if [ -w /etc/resolv.conf ]; then
21
+ cat > /etc/resolv.conf <<'EOF'
22
+ nameserver 1.1.1.1
23
+ nameserver 1.0.0.1
24
+ nameserver 8.8.8.8
25
+ nameserver 8.8.4.4
26
+ options timeout:2 attempts:3 rotate
27
+ EOF
28
+ echo "DNS configured with Cloudflare and Google resolvers."
29
+ else
30
+ echo "WARN: /etc/resolv.conf is not writable; keeping existing DNS."
31
+ fi
32
+ }
33
+
34
+ mkdir -p /root/workspace
35
+ configure_dns
36
+
37
+ /app/sync-root-data.sh restore
38
+ mkdir -p /root/.openclaw
39
+
40
+ generate_openclaw_config() {
41
+ local clean_base
42
+ clean_base="$(printf '%s' "${OPENAI_API_BASE:-}" | sed 's|/chat/completions||g' | sed 's|/v1/|/v1|g' | sed 's|/v1$|/v1|g')"
43
+ local allow_id
44
+ allow_id="${TELEGRAM_ALLOW_ID:-0}"
45
+ local primary_model="kilo_gateway/kilo-auto/free"
46
+ local nvidia_provider=""
47
+
48
+ if [ -n "$clean_base" ] && [ -n "${OPENAI_API_KEY:-}" ]; then
49
+ primary_model="nvidia/${MODEL:-gpt-5.5}"
50
+ nvidia_provider=',
51
+ "nvidia": {
52
+ "baseUrl": "'"${clean_base}"'",
53
+ "apiKey": "'"${OPENAI_API_KEY:-}"'",
54
+ "api": "openai-completions",
55
+ "models": [
56
+ { "id": "'"${MODEL:-gpt-5.5}"'", "name": "'"${MODEL:-gpt-5.5}"'", "contextWindow": 128000 },
57
+ { "id": "'"${VISION_MODEL:-${MODEL:-gpt-5.5}}"'", "name": "Nvidia Vision", "contextWindow": 128000, "input": ["text", "image"] }
58
+ ]
59
+ }'
60
+ fi
61
+
62
+ cat > /root/.openclaw/openclaw.json <<JSONEOF
63
+ {
64
+ "models": {
65
+ "providers": {
66
+ "kilo_gateway": {
67
+ "baseUrl": "https://api.kilo.ai/api/gateway",
68
+ "apiKey": "anonymous",
69
+ "api": "openai-completions",
70
+ "models": [
71
+ { "id": "kilo-auto/free", "name": "Kilo Auto Free", "contextWindow": 128000, "maxTokens": 16000 }
72
+ ]
73
+ }${nvidia_provider}
74
+ }
75
+ },
76
+ "agents": {
77
+ "defaults": {
78
+ "model": {
79
+ "primary": "${primary_model}",
80
+ "fallbacks": ["kilo_gateway/kilo-auto/free"]
81
+ },
82
+ "imageModel": {
83
+ "primary": "${primary_model}"
84
+ }
85
+ }
86
+ },
87
+ "commands": { "restart": true },
88
+ "browser": {
89
+ "enabled": true,
90
+ "headless": true,
91
+ "noSandbox": true,
92
+ "defaultProfile": "openclaw",
93
+ "ssrfPolicy": { "dangerouslyAllowPrivateNetwork": true },
94
+ "profiles": {
95
+ "openclaw": {
96
+ "cdpPort": 18800,
97
+ "color": "0088FF"
98
+ }
99
+ }
100
+ },
101
+ "channels": {
102
+ "telegram": {
103
+ "enabled": true,
104
+ "botToken": "${TELEGRAM_BOT_TOKEN:-}",
105
+ "dmPolicy": "allowlist",
106
+ "allowFrom": [${allow_id}],
107
+ "apiRoot": "${TELEGRAM_API_ROOT}",
108
+ "webhookUrl": "${SPACE_URL}/tg-webhook",
109
+ "webhookSecret": "${OPENCLAW_GATEWAY_PASSWORD:-}",
110
+ "webhookPath": "/tg-webhook",
111
+ "webhookHost": "0.0.0.0",
112
+ "webhookPort": 8787,
113
+ "streaming": {
114
+ "mode": "partial"
115
+ }
116
+ }
117
+ },
118
+ "gateway": {
119
+ "mode": "local",
120
+ "bind": "lan",
121
+ "port": ${OPENCLAW_PORT},
122
+ "trustedProxies": ["0.0.0.0/0"],
123
+ "auth": { "mode": "token", "token": "${OPENCLAW_GATEWAY_PASSWORD:-}" },
124
+ "http": {
125
+ "endpoints": {
126
+ "chatCompletions": { "enabled": true }
127
+ }
128
+ },
129
+ "controlUi": {
130
+ "enabled": true,
131
+ "allowedOrigins": ["${SPACE_URL}"],
132
+ "allowInsecureAuth": true,
133
+ "dangerouslyDisableDeviceAuth": true,
134
+ "dangerouslyAllowHostHeaderOriginFallback": true
135
+ }
136
+ }
137
+ }
138
+ JSONEOF
139
+ }
140
+
141
+ config_is_complete() {
142
+ python3 - <<'PY'
143
+ import json
144
+ from pathlib import Path
145
+
146
+ path = Path('/root/.openclaw/openclaw.json')
147
+ if not path.exists():
148
+ raise SystemExit(1)
149
+
150
+ try:
151
+ data = json.loads(path.read_text())
152
+ except Exception:
153
+ raise SystemExit(1)
154
+
155
+ required_paths = [
156
+ ('models', 'providers'),
157
+ ('models', 'providers', 'kilo_gateway'),
158
+ ('agents', 'defaults'),
159
+ ('browser',),
160
+ ('gateway',),
161
+ ('gateway', 'controlUi'),
162
+ ('channels', 'telegram'),
163
+ ]
164
+
165
+ for parts in required_paths:
166
+ cur = data
167
+ for part in parts:
168
+ if not isinstance(cur, dict) or part not in cur:
169
+ raise SystemExit(1)
170
+ cur = cur[part]
171
+
172
+ raise SystemExit(0)
173
+ PY
174
+ }
175
+
176
+ if ! config_is_complete; then
177
+ echo "Generating full /root/.openclaw/openclaw.json before OpenClaw startup..."
178
+ generate_openclaw_config
179
+ else
180
+ echo "Complete /root/.openclaw/openclaw.json found; keeping it."
181
+ fi
182
+
183
+ python3 - <<'PY'
184
+ import json
185
+ import os
186
+ from pathlib import Path
187
+
188
+ path = Path('/root/.openclaw/openclaw.json')
189
+ api_root = os.environ.get('TELEGRAM_API_ROOT', 'https://tg-relay.markdevil11.workers.dev')
190
+ space_url = os.environ.get('SPACE_URL', 'https://elysiadev11-openclaw.hf.space')
191
+ if not path.exists():
192
+ raise SystemExit(0)
193
+
194
+ try:
195
+ data = json.loads(path.read_text())
196
+ except Exception as exc:
197
+ print(f'WARN: cannot update Telegram apiRoot in openclaw.json: {exc}')
198
+ raise SystemExit(0)
199
+
200
+ channels = data.setdefault('channels', {})
201
+ telegram = channels.setdefault('telegram', {})
202
+ old = telegram.get('apiRoot')
203
+ providers = data.setdefault('models', {}).setdefault('providers', {})
204
+ nvidia = providers.get('nvidia')
205
+ if isinstance(nvidia, dict) and not nvidia.get('baseUrl'):
206
+ providers.pop('nvidia', None)
207
+ agents = data.setdefault('agents', {})
208
+ defaults = agents.setdefault('defaults', {})
209
+ model = defaults.setdefault('model', {})
210
+ if model.get('primary', '').startswith('nvidia/') and 'nvidia' not in providers:
211
+ model['primary'] = 'kilo_gateway/kilo-auto/free'
212
+ fallbacks = model.setdefault('fallbacks', [])
213
+ if 'kilo_gateway/kilo-auto/free' not in fallbacks:
214
+ fallbacks.append('kilo_gateway/kilo-auto/free')
215
+ image_model = defaults.setdefault('imageModel', {})
216
+ if image_model.get('primary', '').startswith('nvidia/') and 'nvidia' not in providers:
217
+ image_model['primary'] = model['primary']
218
+ telegram['webhookUrl'] = f'{space_url}/tg-webhook'
219
+ telegram['webhookPath'] = '/tg-webhook'
220
+ telegram['webhookHost'] = '0.0.0.0'
221
+ telegram['webhookPort'] = 8787
222
+ browser = data.get('browser')
223
+ if isinstance(browser, dict):
224
+ browser.pop('requirePairing', None)
225
+ if isinstance(defaults, dict):
226
+ defaults.pop('llm', None)
227
+ gateway = data.setdefault('gateway', {})
228
+ auth = gateway.setdefault('auth', {})
229
+ auth['mode'] = 'token'
230
+ auth['token'] = os.environ.get('OPENCLAW_GATEWAY_PASSWORD', '773322')
231
+ control_ui = gateway.setdefault('controlUi', {})
232
+ control_ui['enabled'] = True
233
+ control_ui['allowInsecureAuth'] = True
234
+ control_ui['dangerouslyDisableDeviceAuth'] = True
235
+ control_ui['dangerouslyAllowHostHeaderOriginFallback'] = True
236
+ origins = control_ui.setdefault('allowedOrigins', [])
237
+ if space_url not in origins:
238
+ origins.append(space_url)
239
+ if old != api_root:
240
+ telegram['apiRoot'] = api_root
241
+ print(f'Updated Telegram apiRoot: {old!r} -> {api_root!r}')
242
+ else:
243
+ print(f'Telegram apiRoot already set to {api_root}')
244
+ print(f'Telegram webhookUrl set to {telegram.get("webhookUrl")}')
245
+ path.write_text(json.dumps(data, indent=2) + '\n')
246
+ PY
247
+
248
+ /app/sync-root-data.sh reconcile
249
+
250
+ /app/sync-root-data.sh loop &
251
+
252
+ run_openclaw() {
253
+ while true; do
254
+ echo "Starting OpenClaw on 127.0.0.1:${OPENCLAW_PORT}..."
255
+ echo "OpenClaw config:"
256
+ cat /root/.openclaw/openclaw.json
257
+ if [ -n "${OPENCLAW_CMD:-}" ]; then
258
+ sh -lc "$OPENCLAW_CMD"
259
+ else
260
+ openclaw gateway --port "${OPENCLAW_PORT}" --allow-unconfigured &
261
+ OPENCLAW_PID=$!
262
+ echo "OpenClaw started with PID: $OPENCLAW_PID"
263
+ wait $OPENCLAW_PID
264
+ fi
265
+ echo "OpenClaw exited; restarting in 2 seconds..."
266
+ sleep 2
267
+ done
268
+ }
269
+
270
+ run_nginx() {
271
+ while true; do
272
+ envsubst '${PROXY_PORT} ${OPENCLAW_PORT} ${OPENCLAW_GATEWAY_PASSWORD}' < /app/nginx.conf.template > /tmp/nginx.conf
273
+ nginx -c /tmp/nginx.conf -g 'daemon off;'
274
+ echo "Nginx exited; restarting in 2 seconds..."
275
+ sleep 2
276
+ done
277
+ }
278
+
279
+ run_openclaw &
280
+ run_nginx &
281
+
282
+ # Start a simple webhook server if OpenClaw doesn't start one
283
+ run_simple_webhook() {
284
+ while true; do
285
+ echo "Starting simple webhook server on port 8787..."
286
+ python3 /app/webhook_server.py &
287
+ WEBHOOK_PID=$!
288
+ echo "Simple webhook server started with PID: $WEBHOOK_PID"
289
+ wait $WEBHOOK_PID
290
+ echo "Webhook server exited; restarting in 2 seconds..."
291
+ sleep 2
292
+ done
293
+ }
294
+
295
+
296
+ # Wait for services to start
297
+ echo "Waiting for services to start..."
298
+ sleep 10
299
+
300
+ # Check if webhook port is listening
301
+ echo "Checking webhook port 8787..."
302
+ if command -v ss &> /dev/null && ss -tuln 2>/dev/null | grep -q ":8787"; then
303
+ echo "Webhook server is listening on port 8787"
304
+ # Test webhook endpoint
305
+ echo "Testing webhook endpoint..."
306
+ curl -s -o /dev/null -w "Webhook test status: %{http_code}\n" http://0.0.0.0:8787/tg-webhook || curl -s -o /dev/null -w "Webhook test status: %{http_code}\n" http://localhost:8787/tg-webhook || echo "Webhook test failed"
307
+ elif sleep 2 && curl -s http://0.0.0.0:8787/tg-webhook &>/dev/null; then
308
+ echo "Webhook server is accessible on port 8787"
309
+ else
310
+ echo "WARNING: Webhook server is NOT listening on port 8787"
311
+ echo "Starting simple webhook server as fallback..."
312
+ run_simple_webhook &
313
+ fi
314
+
315
+ # Check if OpenClaw port is listening
316
+ echo "Checking OpenClaw port ${OPENCLAW_PORT}..."
317
+ if command -v ss &> /dev/null && ss -tuln 2>/dev/null | grep -q ":${OPENCLAW_PORT}"; then
318
+ echo "OpenClaw is listening on port ${OPENCLAW_PORT}"
319
+ elif sleep 2 && curl -s http://127.0.0.1:${OPENCLAW_PORT}/health &>/dev/null; then
320
+ echo "OpenClaw is accessible on port ${OPENCLAW_PORT}"
321
+ else
322
+ echo "WARNING: OpenClaw is NOT listening on port ${OPENCLAW_PORT}"
323
+ fi
324
+
325
+ wait -n
326
+ exit 1
sync-root-data.sh ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ DATA_ROOT="${DATA_ROOT:-/data/root}"
5
+ SYNC_INTERVAL="${SYNC_INTERVAL:-300}"
6
+ CONFIG_PATH=".openclaw/openclaw.json"
7
+
8
+ EXCLUDES=(
9
+ "--exclude=.cache"
10
+ "--exclude=.npm/_cacache"
11
+ "--exclude=.npm/_update-notifier-last-checked*"
12
+ "--exclude=.pnpm-store"
13
+ "--exclude=node_modules"
14
+ "--exclude=.next"
15
+ "--exclude=dist"
16
+ "--exclude=build"
17
+ "--exclude=coverage"
18
+ "--exclude=tmp"
19
+ )
20
+
21
+ data_available() {
22
+ [ -d /data ] || mkdir -p /data 2>/dev/null || return 1
23
+ mkdir -p "$DATA_ROOT" 2>/dev/null || return 1
24
+ }
25
+
26
+ restore_root() {
27
+ if data_available; then
28
+ echo "Restoring persistent data from $DATA_ROOT to /root..."
29
+ rsync -rlptD --no-specials --no-devices --update "${EXCLUDES[@]}" "$DATA_ROOT/" /root/ || true
30
+ else
31
+ echo "Persistent /data is not available; skipping restore."
32
+ fi
33
+ }
34
+
35
+ persist_root() {
36
+ if data_available; then
37
+ echo "Syncing /root to $DATA_ROOT..."
38
+ rsync -rlptD --no-specials --no-devices --update --delete "${EXCLUDES[@]}" /root/ "$DATA_ROOT/" || true
39
+ else
40
+ echo "Persistent /data is not available; skipping sync."
41
+ fi
42
+ }
43
+
44
+ sync_config_newer() {
45
+ local root_config="/root/$CONFIG_PATH"
46
+ local data_config="$DATA_ROOT/$CONFIG_PATH"
47
+
48
+ if [ -f "$data_config" ] && { [ ! -f "$root_config" ] || [ "$data_config" -nt "$root_config" ]; }; then
49
+ mkdir -p "$(dirname "$root_config")"
50
+ cp -p "$data_config" "$root_config" || true
51
+ echo "Pulled newer OpenClaw config from $data_config to $root_config."
52
+ elif [ -f "$root_config" ] && { [ ! -f "$data_config" ] || [ "$root_config" -nt "$data_config" ]; }; then
53
+ mkdir -p "$(dirname "$data_config")"
54
+ cp -p "$root_config" "$data_config" || true
55
+ echo "Persisted newer OpenClaw config from $root_config to $data_config."
56
+ fi
57
+ }
58
+
59
+ reconcile_root_data() {
60
+ if data_available; then
61
+ restore_root
62
+ sync_config_newer
63
+ persist_root
64
+ else
65
+ echo "Persistent /data is not available; skipping two-way sync."
66
+ fi
67
+ }
68
+
69
+ case "${1:-loop}" in
70
+ restore)
71
+ restore_root
72
+ ;;
73
+ persist)
74
+ persist_root
75
+ ;;
76
+ reconcile)
77
+ reconcile_root_data
78
+ ;;
79
+ loop)
80
+ while true; do
81
+ reconcile_root_data
82
+ sleep "$SYNC_INTERVAL"
83
+ done
84
+ ;;
85
+ *)
86
+ echo "Usage: $0 {restore|persist|loop}"
87
+ exit 2
88
+ ;;
89
+ esac
webhook_server.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ import os
3
+ import json
4
+ from http.server import HTTPServer, BaseHTTPRequestHandler
5
+ from urllib.parse import urlparse, parse_qs
6
+
7
+ class WebhookHandler(BaseHTTPRequestHandler):
8
+ def do_GET(self):
9
+ self.send_response(200)
10
+ self.send_header('Content-type', 'text/plain')
11
+ self.end_headers()
12
+ self.wfile.write(b'Webhook server is running')
13
+
14
+ def do_POST(self):
15
+ content_length = int(self.headers.get('Content-Length', 0))
16
+ if content_length > 0:
17
+ post_data = self.rfile.read(content_length)
18
+ else:
19
+ post_data = b''
20
+
21
+ # Log the webhook request
22
+ print(f"Webhook received: {self.path}")
23
+ print(f"Headers: {dict(self.headers)}")
24
+ if post_data:
25
+ print(f"Body: {post_data.decode('utf-8', errors='ignore')}")
26
+
27
+ # Try to parse as JSON
28
+ try:
29
+ data = json.loads(post_data.decode('utf-8'))
30
+ print(f"JSON data: {json.dumps(data, indent=2)}")
31
+ except:
32
+ pass
33
+
34
+ # Respond with 200 OK
35
+ self.send_response(200)
36
+ self.send_header('Content-type', 'application/json')
37
+ self.end_headers()
38
+ self.wfile.write(b'{"status": "ok"}')
39
+
40
+ def log_message(self, format, *args):
41
+ # Suppress default logging
42
+ pass
43
+
44
+ def run_server(port=8787, host='0.0.0.0'):
45
+ server_address = (host, port)
46
+ httpd = HTTPServer(server_address, WebhookHandler)
47
+ print(f"Webhook server running on {host}:{port}")
48
+ try:
49
+ httpd.serve_forever()
50
+ except KeyboardInterrupt:
51
+ print("\nShutting down webhook server")
52
+ httpd.shutdown()
53
+
54
+ if __name__ == '__main__':
55
+ run_server()