Spaces:
Running
Running
Commit ·
5a73172
1
Parent(s): 6b45d1f
Debounce OpenClaw config syncs until stable
Browse files- .env.example +6 -0
- README.md +3 -1
- multi-provider-key-rotator.cjs +8 -3
- openclaw-sync.py +120 -6
- start.sh +26 -8
.env.example
CHANGED
|
@@ -285,6 +285,12 @@ KEEP_ALIVE_INTERVAL=300
|
|
| 285 |
# Workspace auto-sync interval (seconds). Default: 180.
|
| 286 |
SYNC_INTERVAL=180
|
| 287 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 288 |
# Webhooks: Standard POST notifications for lifecycle events
|
| 289 |
# WEBHOOK_URL=https://your-webhook-endpoint.com/log
|
| 290 |
|
|
|
|
| 285 |
# Workspace auto-sync interval (seconds). Default: 180.
|
| 286 |
SYNC_INTERVAL=180
|
| 287 |
|
| 288 |
+
# Check openclaw.json for settings changes this often (seconds). Default: 1.
|
| 289 |
+
OPENCLAW_CONFIG_WATCH_INTERVAL=1
|
| 290 |
+
|
| 291 |
+
# Wait for openclaw.json to stay valid and unchanged before syncing. Default: 3.
|
| 292 |
+
OPENCLAW_CONFIG_SETTLE_SECONDS=3
|
| 293 |
+
|
| 294 |
# Webhooks: Standard POST notifications for lifecycle events
|
| 295 |
# WEBHOOK_URL=https://your-webhook-endpoint.com/log
|
| 296 |
|
README.md
CHANGED
|
@@ -162,7 +162,9 @@ HuggingClaw automatically syncs your workspace (chats, settings, sessions) to a
|
|
| 162 |
| Variable | Default | Description |
|
| 163 |
| :--- | :--- | :--- |
|
| 164 |
| `HF_TOKEN` | — | HF token with **Write** access |
|
| 165 |
-
| `SYNC_INTERVAL` | `180` |
|
|
|
|
|
|
|
| 166 |
|
| 167 |
## 📦 Ephemeral Package Re-install *(Optional)*
|
| 168 |
|
|
|
|
| 162 |
| Variable | Default | Description |
|
| 163 |
| :--- | :--- | :--- |
|
| 164 |
| `HF_TOKEN` | — | HF token with **Write** access |
|
| 165 |
+
| `SYNC_INTERVAL` | `180` | Full backup frequency in seconds |
|
| 166 |
+
| `OPENCLAW_CONFIG_WATCH_INTERVAL` | `1` | How often to check `openclaw.json` for immediate settings sync |
|
| 167 |
+
| `OPENCLAW_CONFIG_SETTLE_SECONDS` | `3` | How long `openclaw.json` must stay valid and unchanged before syncing |
|
| 168 |
|
| 169 |
## 📦 Ephemeral Package Re-install *(Optional)*
|
| 170 |
|
multi-provider-key-rotator.cjs
CHANGED
|
@@ -19,6 +19,11 @@
|
|
| 19 |
const http = require('node:http');
|
| 20 |
const https = require('node:https');
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
// ─── Provider definitions ────────────────────────────────────────────────────
|
| 23 |
//
|
| 24 |
// hostname – regex tested against the request hostname (case-insensitive)
|
|
@@ -185,7 +190,7 @@ const providerState = PROVIDERS.map(p => {
|
|
| 185 |
: normalizeKeys(process.env.LLM_API_KEY || '');
|
| 186 |
|
| 187 |
if (hasDedicated) {
|
| 188 |
-
|
| 189 |
} else if (!keys.length) {
|
| 190 |
console.warn(`[key-rotator] No keys for provider "${p.name}"`);
|
| 191 |
}
|
|
@@ -202,7 +207,7 @@ const fallbackCount = providerState.filter(p => {
|
|
| 202 |
return dedicated.length === 0 && p.keys.length > 0;
|
| 203 |
}).length;
|
| 204 |
if (fallbackCount > 0) {
|
| 205 |
-
|
| 206 |
}
|
| 207 |
|
| 208 |
// ─── Runtime helpers ─────────────────────────────────────────────────────────
|
|
@@ -332,4 +337,4 @@ patchFetch();
|
|
| 332 |
patchHttpModule(http);
|
| 333 |
patchHttpModule(https);
|
| 334 |
|
| 335 |
-
|
|
|
|
| 19 |
const http = require('node:http');
|
| 20 |
const https = require('node:https');
|
| 21 |
|
| 22 |
+
// This file is preloaded through NODE_OPTIONS, so it also runs inside npm and
|
| 23 |
+
// OpenClaw helper subprocesses that may emit machine-readable JSON on stdout.
|
| 24 |
+
// Keep rotator diagnostics on stderr to avoid corrupting those stdout streams.
|
| 25 |
+
const log = (...args) => console.error(...args);
|
| 26 |
+
|
| 27 |
// ─── Provider definitions ────────────────────────────────────────────────────
|
| 28 |
//
|
| 29 |
// hostname – regex tested against the request hostname (case-insensitive)
|
|
|
|
| 190 |
: normalizeKeys(process.env.LLM_API_KEY || '');
|
| 191 |
|
| 192 |
if (hasDedicated) {
|
| 193 |
+
log(`[key-rotator] ${p.name}: ${keys.length} key${keys.length === 1 ? '' : 's'}`);
|
| 194 |
} else if (!keys.length) {
|
| 195 |
console.warn(`[key-rotator] No keys for provider "${p.name}"`);
|
| 196 |
}
|
|
|
|
| 207 |
return dedicated.length === 0 && p.keys.length > 0;
|
| 208 |
}).length;
|
| 209 |
if (fallbackCount > 0) {
|
| 210 |
+
log(`[key-rotator] ${fallbackCount} provider(s) using LLM_API_KEY fallback`);
|
| 211 |
}
|
| 212 |
|
| 213 |
// ─── Runtime helpers ─────────────────────────────────────────────────────────
|
|
|
|
| 337 |
patchHttpModule(http);
|
| 338 |
patchHttpModule(https);
|
| 339 |
|
| 340 |
+
log('[key-rotator] loaded — all providers active');
|
openclaw-sync.py
CHANGED
|
@@ -34,10 +34,19 @@ from huggingface_hub.errors import HfHubHTTPError, RepositoryNotFoundError
|
|
| 34 |
logging.getLogger("huggingface_hub").setLevel(logging.ERROR)
|
| 35 |
|
| 36 |
OPENCLAW_HOME = Path("/home/node/.openclaw")
|
|
|
|
| 37 |
WORKSPACE = OPENCLAW_HOME / "workspace"
|
| 38 |
STATUS_FILE = Path("/tmp/sync-status.json")
|
| 39 |
INTERVAL = int(os.environ.get("SYNC_INTERVAL", "180"))
|
| 40 |
INITIAL_DELAY = int(os.environ.get("SYNC_START_DELAY", "10"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
HF_TOKEN = os.environ.get("HF_TOKEN", "").strip()
|
| 42 |
HF_USERNAME = os.environ.get("HF_USERNAME", "").strip()
|
| 43 |
SPACE_AUTHOR_NAME = os.environ.get("SPACE_AUTHOR_NAME", "").strip()
|
|
@@ -78,6 +87,13 @@ def write_status(status: str, message: str) -> None:
|
|
| 78 |
tmp_path.replace(STATUS_FILE)
|
| 79 |
|
| 80 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
def count_files(path: Path) -> int:
|
| 82 |
if not path.exists():
|
| 83 |
return 0
|
|
@@ -250,6 +266,18 @@ def _should_exclude(rel_posix: str, path: Path) -> bool:
|
|
| 250 |
return False
|
| 251 |
|
| 252 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
def metadata_marker(root: Path) -> tuple[int, int, int]:
|
| 254 |
if not root.exists():
|
| 255 |
return (0, 0, 0)
|
|
@@ -416,10 +444,64 @@ def handle_signal(_sig, _frame) -> None:
|
|
| 416 |
STOP_EVENT.set()
|
| 417 |
|
| 418 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 419 |
def loop() -> int:
|
| 420 |
signal.signal(signal.SIGTERM, handle_signal)
|
| 421 |
signal.signal(signal.SIGINT, handle_signal)
|
| 422 |
|
|
|
|
|
|
|
| 423 |
try:
|
| 424 |
repo_id = resolve_backup_namespace()
|
| 425 |
write_status("configured", f"Backup loop active for {repo_id} with {INTERVAL}s interval.")
|
|
@@ -431,24 +513,56 @@ def loop() -> int:
|
|
| 431 |
time.sleep(INITIAL_DELAY)
|
| 432 |
print(f"Workspace sync started: every {INTERVAL}s -> {repo_id}")
|
| 433 |
|
| 434 |
-
#
|
| 435 |
-
#
|
| 436 |
-
#
|
| 437 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 438 |
snapshot_state_into_workspace()
|
| 439 |
last_fingerprint = fingerprint_dir(WORKSPACE)
|
| 440 |
last_marker = metadata_marker(WORKSPACE)
|
| 441 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 442 |
|
| 443 |
while not STOP_EVENT.is_set():
|
| 444 |
try:
|
|
|
|
| 445 |
last_fingerprint, last_marker = sync_once(last_fingerprint, last_marker)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 446 |
except Exception as exc:
|
| 447 |
write_status("error", f"Sync failed: {exc}")
|
| 448 |
print(f"Workspace sync failed: {exc}")
|
|
|
|
| 449 |
|
| 450 |
-
|
|
|
|
| 451 |
break
|
|
|
|
|
|
|
| 452 |
|
| 453 |
return 0
|
| 454 |
|
|
|
|
| 34 |
logging.getLogger("huggingface_hub").setLevel(logging.ERROR)
|
| 35 |
|
| 36 |
OPENCLAW_HOME = Path("/home/node/.openclaw")
|
| 37 |
+
OPENCLAW_CONFIG_FILE = OPENCLAW_HOME / "openclaw.json"
|
| 38 |
WORKSPACE = OPENCLAW_HOME / "workspace"
|
| 39 |
STATUS_FILE = Path("/tmp/sync-status.json")
|
| 40 |
INTERVAL = int(os.environ.get("SYNC_INTERVAL", "180"))
|
| 41 |
INITIAL_DELAY = int(os.environ.get("SYNC_START_DELAY", "10"))
|
| 42 |
+
CONFIG_WATCH_INTERVAL = max(
|
| 43 |
+
0.5,
|
| 44 |
+
float(os.environ.get("OPENCLAW_CONFIG_WATCH_INTERVAL", "1")),
|
| 45 |
+
)
|
| 46 |
+
CONFIG_SETTLE_SECONDS = max(
|
| 47 |
+
0.0,
|
| 48 |
+
float(os.environ.get("OPENCLAW_CONFIG_SETTLE_SECONDS", "3")),
|
| 49 |
+
)
|
| 50 |
HF_TOKEN = os.environ.get("HF_TOKEN", "").strip()
|
| 51 |
HF_USERNAME = os.environ.get("HF_USERNAME", "").strip()
|
| 52 |
SPACE_AUTHOR_NAME = os.environ.get("SPACE_AUTHOR_NAME", "").strip()
|
|
|
|
| 87 |
tmp_path.replace(STATUS_FILE)
|
| 88 |
|
| 89 |
|
| 90 |
+
def read_status() -> dict[str, str]:
|
| 91 |
+
try:
|
| 92 |
+
return json.loads(STATUS_FILE.read_text(encoding="utf-8"))
|
| 93 |
+
except Exception:
|
| 94 |
+
return {}
|
| 95 |
+
|
| 96 |
+
|
| 97 |
def count_files(path: Path) -> int:
|
| 98 |
if not path.exists():
|
| 99 |
return 0
|
|
|
|
| 266 |
return False
|
| 267 |
|
| 268 |
|
| 269 |
+
def file_marker(path: Path) -> tuple[int, int, int]:
|
| 270 |
+
try:
|
| 271 |
+
stat = path.stat()
|
| 272 |
+
except OSError:
|
| 273 |
+
return (0, 0, 0)
|
| 274 |
+
|
| 275 |
+
if not path.is_file():
|
| 276 |
+
return (0, 0, 0)
|
| 277 |
+
|
| 278 |
+
return (1, int(stat.st_size), int(stat.st_mtime_ns))
|
| 279 |
+
|
| 280 |
+
|
| 281 |
def metadata_marker(root: Path) -> tuple[int, int, int]:
|
| 282 |
if not root.exists():
|
| 283 |
return (0, 0, 0)
|
|
|
|
| 444 |
STOP_EVENT.set()
|
| 445 |
|
| 446 |
|
| 447 |
+
def is_valid_json_file(path: Path) -> bool:
|
| 448 |
+
if not path.exists():
|
| 449 |
+
return True
|
| 450 |
+
|
| 451 |
+
try:
|
| 452 |
+
json.loads(path.read_text(encoding="utf-8"))
|
| 453 |
+
return True
|
| 454 |
+
except Exception:
|
| 455 |
+
return False
|
| 456 |
+
|
| 457 |
+
|
| 458 |
+
def wait_for_config_settle(config_marker: tuple[int, int, int]) -> tuple[str, tuple[int, int, int]]:
|
| 459 |
+
stable_since = time.monotonic()
|
| 460 |
+
current_marker = config_marker
|
| 461 |
+
|
| 462 |
+
while not STOP_EVENT.is_set():
|
| 463 |
+
latest_marker = file_marker(OPENCLAW_CONFIG_FILE)
|
| 464 |
+
if latest_marker != current_marker:
|
| 465 |
+
current_marker = latest_marker
|
| 466 |
+
stable_since = time.monotonic()
|
| 467 |
+
|
| 468 |
+
if (
|
| 469 |
+
time.monotonic() - stable_since >= CONFIG_SETTLE_SECONDS
|
| 470 |
+
and is_valid_json_file(OPENCLAW_CONFIG_FILE)
|
| 471 |
+
):
|
| 472 |
+
return ("settled", current_marker)
|
| 473 |
+
|
| 474 |
+
if STOP_EVENT.wait(CONFIG_WATCH_INTERVAL):
|
| 475 |
+
return ("stopped", current_marker)
|
| 476 |
+
|
| 477 |
+
return ("stopped", current_marker)
|
| 478 |
+
|
| 479 |
+
|
| 480 |
+
def wait_for_sync_trigger(config_marker: tuple[int, int, int]) -> tuple[str, tuple[int, int, int]]:
|
| 481 |
+
deadline = time.monotonic() + max(0, INTERVAL)
|
| 482 |
+
|
| 483 |
+
while not STOP_EVENT.is_set():
|
| 484 |
+
current_config_marker = file_marker(OPENCLAW_CONFIG_FILE)
|
| 485 |
+
if current_config_marker != config_marker:
|
| 486 |
+
return wait_for_config_settle(current_config_marker)
|
| 487 |
+
|
| 488 |
+
remaining = deadline - time.monotonic()
|
| 489 |
+
if remaining <= 0:
|
| 490 |
+
return ("interval", current_config_marker)
|
| 491 |
+
|
| 492 |
+
wait_seconds = min(CONFIG_WATCH_INTERVAL, remaining)
|
| 493 |
+
if STOP_EVENT.wait(wait_seconds):
|
| 494 |
+
return ("stopped", current_config_marker)
|
| 495 |
+
|
| 496 |
+
return ("stopped", config_marker)
|
| 497 |
+
|
| 498 |
+
|
| 499 |
def loop() -> int:
|
| 500 |
signal.signal(signal.SIGTERM, handle_signal)
|
| 501 |
signal.signal(signal.SIGINT, handle_signal)
|
| 502 |
|
| 503 |
+
previous_status = read_status().get("status", "")
|
| 504 |
+
|
| 505 |
try:
|
| 506 |
repo_id = resolve_backup_namespace()
|
| 507 |
write_status("configured", f"Backup loop active for {repo_id} with {INTERVAL}s interval.")
|
|
|
|
| 513 |
time.sleep(INITIAL_DELAY)
|
| 514 |
print(f"Workspace sync started: every {INTERVAL}s -> {repo_id}")
|
| 515 |
|
| 516 |
+
# Capture the restored dataset state before refreshing the embedded
|
| 517 |
+
# /home/node/.openclaw backup. Startup may have patched openclaw.json
|
| 518 |
+
# after restore (token/model/logging/channel toggles), and that patch only
|
| 519 |
+
# becomes part of the dataset once snapshot_state_into_workspace() copies it
|
| 520 |
+
# into workspace/huggingclaw-state/openclaw/. If the snapshot changes the
|
| 521 |
+
# workspace, seed the first sync with the pre-snapshot fingerprint so the
|
| 522 |
+
# updated openclaw.json is uploaded instead of being treated as the baseline.
|
| 523 |
+
pre_snapshot_fingerprint = fingerprint_dir(WORKSPACE)
|
| 524 |
+
pre_snapshot_marker = metadata_marker(WORKSPACE)
|
| 525 |
snapshot_state_into_workspace()
|
| 526 |
last_fingerprint = fingerprint_dir(WORKSPACE)
|
| 527 |
last_marker = metadata_marker(WORKSPACE)
|
| 528 |
+
|
| 529 |
+
if last_fingerprint != pre_snapshot_fingerprint:
|
| 530 |
+
if previous_status == "error":
|
| 531 |
+
print(
|
| 532 |
+
"Initial state snapshot changed, but restore previously failed; "
|
| 533 |
+
"keeping current state as baseline to avoid overwriting the remote backup."
|
| 534 |
+
)
|
| 535 |
+
else:
|
| 536 |
+
last_fingerprint = pre_snapshot_fingerprint
|
| 537 |
+
last_marker = pre_snapshot_marker
|
| 538 |
+
print("Initial state snapshot changed; first sync will upload refreshed OpenClaw state.")
|
| 539 |
+
else:
|
| 540 |
+
print("Initial workspace fingerprint captured.")
|
| 541 |
+
|
| 542 |
+
config_marker = file_marker(OPENCLAW_CONFIG_FILE)
|
| 543 |
|
| 544 |
while not STOP_EVENT.is_set():
|
| 545 |
try:
|
| 546 |
+
sync_started_config_marker = file_marker(OPENCLAW_CONFIG_FILE)
|
| 547 |
last_fingerprint, last_marker = sync_once(last_fingerprint, last_marker)
|
| 548 |
+
config_marker = file_marker(OPENCLAW_CONFIG_FILE)
|
| 549 |
+
|
| 550 |
+
if config_marker != sync_started_config_marker:
|
| 551 |
+
trigger, config_marker = wait_for_config_settle(config_marker)
|
| 552 |
+
if trigger == "stopped":
|
| 553 |
+
break
|
| 554 |
+
print("OpenClaw config changed during sync; syncing again after it settled.")
|
| 555 |
+
continue
|
| 556 |
except Exception as exc:
|
| 557 |
write_status("error", f"Sync failed: {exc}")
|
| 558 |
print(f"Workspace sync failed: {exc}")
|
| 559 |
+
config_marker = file_marker(OPENCLAW_CONFIG_FILE)
|
| 560 |
|
| 561 |
+
trigger, config_marker = wait_for_sync_trigger(config_marker)
|
| 562 |
+
if trigger == "stopped":
|
| 563 |
break
|
| 564 |
+
if trigger == "settled":
|
| 565 |
+
print("OpenClaw config changed and settled; syncing immediately.")
|
| 566 |
|
| 567 |
return 0
|
| 568 |
|
start.sh
CHANGED
|
@@ -11,6 +11,14 @@ umask 0077
|
|
| 11 |
OPENCLAW_VERSION="${OPENCLAW_VERSION:-latest}"
|
| 12 |
OPENCLAW_APP_DIR="/home/node/.openclaw/openclaw-app"
|
| 13 |
OPENCLAW_RUNTIME_VERSION=""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
WHATSAPP_ENABLED="${WHATSAPP_ENABLED:-false}"
|
| 15 |
WHATSAPP_ENABLED_NORMALIZED=$(printf '%s' "$WHATSAPP_ENABLED" | tr '[:upper:]' '[:lower:]')
|
| 16 |
SYNC_INTERVAL="${SYNC_INTERVAL:-180}"
|
|
@@ -445,22 +453,32 @@ if [ -f "$EXISTING_CONFIG" ]; then
|
|
| 445 |
--arg consoleLevel "$OPENCLAW_CONSOLE_LOG_LEVEL" \
|
| 446 |
--arg consoleStyle "$OPENCLAW_CONSOLE_LOG_STYLE" \
|
| 447 |
--argjson desired "$CONFIG_JSON" \
|
|
|
|
|
|
|
|
|
|
|
|
|
| 448 |
--argjson whatsappEnabled "$WHATSAPP_CONFIG_ENABLED" \
|
| 449 |
-
'.
|
|
|
|
| 450 |
| .agents.defaults.model = $model
|
| 451 |
-
| .logging.level = $fileLevel
|
| 452 |
-
| .logging.consoleLevel = $consoleLevel
|
| 453 |
-
| .logging.consoleStyle = $consoleStyle
|
| 454 |
| .channels = ((.channels // {}) * ($desired.channels // {}))
|
| 455 |
| .plugins.allow = (((.plugins.allow // []) + ($desired.plugins.allow // [])) | unique)
|
| 456 |
| .plugins.deny = (((.plugins.deny // []) + ($desired.plugins.deny // [])) | unique)
|
| 457 |
-
| .plugins.entries = ((.plugins.entries // {}) * (
|
| 458 |
| if $whatsappEnabled then
|
| 459 |
-
.
|
| 460 |
-
| .
|
| 461 |
-
|
|
|
|
|
|
|
|
|
|
| 462 |
.plugins.entries.whatsapp.enabled = false
|
| 463 |
| del(.channels.whatsapp)
|
|
|
|
|
|
|
| 464 |
end' \
|
| 465 |
"$EXISTING_CONFIG" 2>/dev/null)
|
| 466 |
|
|
|
|
| 11 |
OPENCLAW_VERSION="${OPENCLAW_VERSION:-latest}"
|
| 12 |
OPENCLAW_APP_DIR="/home/node/.openclaw/openclaw-app"
|
| 13 |
OPENCLAW_RUNTIME_VERSION=""
|
| 14 |
+
OPENCLAW_FILE_LOG_LEVEL_CONFIGURED=false
|
| 15 |
+
OPENCLAW_CONSOLE_LOG_LEVEL_CONFIGURED=false
|
| 16 |
+
OPENCLAW_CONSOLE_LOG_STYLE_CONFIGURED=false
|
| 17 |
+
WHATSAPP_ENABLED_CONFIGURED=false
|
| 18 |
+
[ "${OPENCLAW_FILE_LOG_LEVEL+x}" = "x" ] && OPENCLAW_FILE_LOG_LEVEL_CONFIGURED=true
|
| 19 |
+
[ "${OPENCLAW_CONSOLE_LOG_LEVEL+x}" = "x" ] && OPENCLAW_CONSOLE_LOG_LEVEL_CONFIGURED=true
|
| 20 |
+
[ "${OPENCLAW_CONSOLE_LOG_STYLE+x}" = "x" ] && OPENCLAW_CONSOLE_LOG_STYLE_CONFIGURED=true
|
| 21 |
+
[ "${WHATSAPP_ENABLED+x}" = "x" ] && WHATSAPP_ENABLED_CONFIGURED=true
|
| 22 |
WHATSAPP_ENABLED="${WHATSAPP_ENABLED:-false}"
|
| 23 |
WHATSAPP_ENABLED_NORMALIZED=$(printf '%s' "$WHATSAPP_ENABLED" | tr '[:upper:]' '[:lower:]')
|
| 24 |
SYNC_INTERVAL="${SYNC_INTERVAL:-180}"
|
|
|
|
| 453 |
--arg consoleLevel "$OPENCLAW_CONSOLE_LOG_LEVEL" \
|
| 454 |
--arg consoleStyle "$OPENCLAW_CONSOLE_LOG_STYLE" \
|
| 455 |
--argjson desired "$CONFIG_JSON" \
|
| 456 |
+
--argjson fileLogConfigured "$OPENCLAW_FILE_LOG_LEVEL_CONFIGURED" \
|
| 457 |
+
--argjson consoleLogConfigured "$OPENCLAW_CONSOLE_LOG_LEVEL_CONFIGURED" \
|
| 458 |
+
--argjson consoleStyleConfigured "$OPENCLAW_CONSOLE_LOG_STYLE_CONFIGURED" \
|
| 459 |
+
--argjson whatsappConfigured "$WHATSAPP_ENABLED_CONFIGURED" \
|
| 460 |
--argjson whatsappEnabled "$WHATSAPP_CONFIG_ENABLED" \
|
| 461 |
+
'(.channels.whatsapp // {}) as $existingWhatsapp
|
| 462 |
+
| .gateway.auth.token = $token
|
| 463 |
| .agents.defaults.model = $model
|
| 464 |
+
| if $fileLogConfigured then .logging.level = $fileLevel else . end
|
| 465 |
+
| if $consoleLogConfigured then .logging.consoleLevel = $consoleLevel else . end
|
| 466 |
+
| if $consoleStyleConfigured then .logging.consoleStyle = $consoleStyle else . end
|
| 467 |
| .channels = ((.channels // {}) * ($desired.channels // {}))
|
| 468 |
| .plugins.allow = (((.plugins.allow // []) + ($desired.plugins.allow // [])) | unique)
|
| 469 |
| .plugins.deny = (((.plugins.deny // []) + ($desired.plugins.deny // [])) | unique)
|
| 470 |
+
| .plugins.entries = (($desired.plugins.entries // {}) * (.plugins.entries // {}))
|
| 471 |
| if $whatsappEnabled then
|
| 472 |
+
($desired.channels.whatsapp // {"dmPolicy": "pairing"}) as $desiredWhatsapp
|
| 473 |
+
| .plugins.entries.whatsapp.enabled = true
|
| 474 |
+
| .channels.whatsapp = (($existingWhatsapp * $desiredWhatsapp)
|
| 475 |
+
| if ($existingWhatsapp | has("dmPolicy")) then .dmPolicy = $existingWhatsapp.dmPolicy else . end
|
| 476 |
+
| if ($existingWhatsapp | has("allowFrom")) then .allowFrom = $existingWhatsapp.allowFrom else . end)
|
| 477 |
+
elif $whatsappConfigured then
|
| 478 |
.plugins.entries.whatsapp.enabled = false
|
| 479 |
| del(.channels.whatsapp)
|
| 480 |
+
else
|
| 481 |
+
.
|
| 482 |
end' \
|
| 483 |
"$EXISTING_CONFIG" 2>/dev/null)
|
| 484 |
|