Spaces:
Running
Running
Merge pull request #17 from anurag162008/main
Browse files## feat: Merge JupyterLab terminal into HuggingClaw (DEV_MODE gated)
- .env.example +4 -0
- .gitattributes +34 -0
- .gitignore +4 -7
- CHANGELOG.md +13 -0
- CONTRIBUTING.md +3 -2
- Dockerfile +33 -11
- README.md +50 -12
- SECURITY.md +1 -0
- cloudflare-proxy.js +1 -0
- env-builder.html +819 -0
- env-builder.js +2341 -0
- health-server.js +463 -309
- iframe-fix.cjs +1 -0
- jupyter-devdata-sync.py +302 -0
- login.html +59 -0
- multi-provider-key-rotator.cjs +35 -5
- openclaw-sync.py +5 -2
- start.sh +396 -23
- wa-guardian.js +15 -2
.env.example
CHANGED
|
@@ -247,6 +247,10 @@ LLM_API_KEY_FALLBACK_ENABLED=true
|
|
| 247 |
# Generate: openssl rand -hex 32
|
| 248 |
GATEWAY_TOKEN=your_gateway_token_here
|
| 249 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
# (Optional) Password auth — simpler alternative to token for casual users
|
| 251 |
# If set, users can log in with this password instead of the token
|
| 252 |
# OPENCLAW_PASSWORD=your_password_here
|
|
|
|
| 247 |
# Generate: openssl rand -hex 32
|
| 248 |
GATEWAY_TOKEN=your_gateway_token_here
|
| 249 |
|
| 250 |
+
# [OPTIONAL] JupyterLab terminal token for /terminal/
|
| 251 |
+
# Defaults to "huggingface" if unset. Set a strong token for private deployments.
|
| 252 |
+
JUPYTER_TOKEN=huggingface
|
| 253 |
+
|
| 254 |
# (Optional) Password auth — simpler alternative to token for casual users
|
| 255 |
# If set, users can log in with this password instead of the token
|
| 256 |
# OPENCLAW_PASSWORD=your_password_here
|
.gitattributes
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
.gitignore
CHANGED
|
@@ -1,21 +1,20 @@
|
|
| 1 |
-
# personal workflows
|
| 2 |
-
.github/workflows/
|
| 3 |
-
.git*
|
| 4 |
# env/secrets
|
| 5 |
.env
|
| 6 |
.env.*
|
|
|
|
| 7 |
*.pem
|
| 8 |
*.key
|
| 9 |
|
| 10 |
# node
|
| 11 |
node_modules/
|
|
|
|
| 12 |
npm-debug.log*
|
| 13 |
yarn-debug.log*
|
| 14 |
pnpm-debug.log*
|
| 15 |
|
| 16 |
# python
|
| 17 |
__pycache__/
|
| 18 |
-
*.
|
| 19 |
.venv/
|
| 20 |
venv/
|
| 21 |
|
|
@@ -33,12 +32,10 @@ build/
|
|
| 33 |
.cache/
|
| 34 |
|
| 35 |
# temp
|
|
|
|
| 36 |
tmp/
|
| 37 |
temp/
|
| 38 |
|
| 39 |
# os junk
|
| 40 |
.DS_Store
|
| 41 |
Thumbs.db
|
| 42 |
-
|
| 43 |
-
# gitignore itself
|
| 44 |
-
.gitignore
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# env/secrets
|
| 2 |
.env
|
| 3 |
.env.*
|
| 4 |
+
!.env.example
|
| 5 |
*.pem
|
| 6 |
*.key
|
| 7 |
|
| 8 |
# node
|
| 9 |
node_modules/
|
| 10 |
+
gui/node_modules/
|
| 11 |
npm-debug.log*
|
| 12 |
yarn-debug.log*
|
| 13 |
pnpm-debug.log*
|
| 14 |
|
| 15 |
# python
|
| 16 |
__pycache__/
|
| 17 |
+
*.py[cod]
|
| 18 |
.venv/
|
| 19 |
venv/
|
| 20 |
|
|
|
|
| 32 |
.cache/
|
| 33 |
|
| 34 |
# temp
|
| 35 |
+
.tmp/
|
| 36 |
tmp/
|
| 37 |
temp/
|
| 38 |
|
| 39 |
# os junk
|
| 40 |
.DS_Store
|
| 41 |
Thumbs.db
|
|
|
|
|
|
|
|
|
CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
| 2 |
|
| 3 |
All notable changes to this project will be documented in this file.
|
| 4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
## [1.5.0] - 2026-05-13
|
| 6 |
|
| 7 |
### Added
|
|
|
|
| 2 |
|
| 3 |
All notable changes to this project will be documented in this file.
|
| 4 |
|
| 5 |
+
## [1.6.0] - 2026-05-14
|
| 6 |
+
|
| 7 |
+
### Added
|
| 8 |
+
|
| 9 |
+
- **Merged JupyterLab terminal** — added the Hugging Face JupyterLab template login page and `/terminal/` routing alongside the HuggingClaw dashboard and `/app/` OpenClaw Control UI.
|
| 10 |
+
- **HF template parity files** — restored Git LFS defaults and project metadata/docs that were missing from the merged repository.
|
| 11 |
+
|
| 12 |
+
### Fixed
|
| 13 |
+
|
| 14 |
+
- **HF Spaces subpath routing** — normalized internal redirects and WebSocket forwarding so `/app/` and `/terminal/` stay mounted behind the single public Spaces port.
|
| 15 |
+
- **Startup log noise** — removed the stale disabled `plugins.entries.acpx` config entry, switched Jupyter token/cookie options to `IdentityProvider.*`, and printed `/app/` with the trailing slash.
|
| 16 |
+
- **Private Space navigation** — dashboard buttons now stay in-frame and startup logs print relative routes, avoiding raw `.hf.space` links that can show Hugging Face's outer 404 page on private Spaces.
|
| 17 |
+
|
| 18 |
## [1.5.0] - 2026-05-13
|
| 19 |
|
| 20 |
### Added
|
CONTRIBUTING.md
CHANGED
|
@@ -17,7 +17,7 @@ Thanks for your interest in contributing! 🦞
|
|
| 17 |
1. Fork the repo
|
| 18 |
2. Create a feature branch: `git checkout -b feature/my-feature`
|
| 19 |
3. Make your changes
|
| 20 |
-
4. Test locally with Docker: `docker build -t huggingclaw . && docker run -p
|
| 21 |
5. Commit with a clear message
|
| 22 |
6. Push and open a PR
|
| 23 |
|
|
@@ -27,6 +27,7 @@ Thanks for your interest in contributing! 🦞
|
|
| 27 |
- No unnecessary dependencies
|
| 28 |
|
| 29 |
### Testing
|
|
|
|
| 30 |
- Test with at least one LLM provider (Anthropic, OpenAI, or Google)
|
| 31 |
- Test with and without Telegram enabled
|
| 32 |
- Test with and without workspace backup enabled
|
|
@@ -38,7 +39,7 @@ Thanks for your interest in contributing! 🦞
|
|
| 38 |
cp .env.example .env
|
| 39 |
# Fill in your values
|
| 40 |
docker build -t huggingclaw .
|
| 41 |
-
docker run -p
|
| 42 |
```
|
| 43 |
|
| 44 |
## Questions?
|
|
|
|
| 17 |
1. Fork the repo
|
| 18 |
2. Create a feature branch: `git checkout -b feature/my-feature`
|
| 19 |
3. Make your changes
|
| 20 |
+
4. Test locally with Docker: `docker build -t huggingclaw . && docker run -p 7861:7861 --env-file .env huggingclaw`
|
| 21 |
5. Commit with a clear message
|
| 22 |
6. Push and open a PR
|
| 23 |
|
|
|
|
| 27 |
- No unnecessary dependencies
|
| 28 |
|
| 29 |
### Testing
|
| 30 |
+
- If touching routing or Docker startup, verify both `/app/` (OpenClaw UI) and `/terminal/` (JupyterLab terminal).
|
| 31 |
- Test with at least one LLM provider (Anthropic, OpenAI, or Google)
|
| 32 |
- Test with and without Telegram enabled
|
| 33 |
- Test with and without workspace backup enabled
|
|
|
|
| 39 |
cp .env.example .env
|
| 40 |
# Fill in your values
|
| 41 |
docker build -t huggingclaw .
|
| 42 |
+
docker run -p 7861:7861 --env-file .env huggingclaw
|
| 43 |
```
|
| 44 |
|
| 45 |
## Questions?
|
Dockerfile
CHANGED
|
@@ -1,7 +1,11 @@
|
|
| 1 |
# ════════════════════════════════════════════════════════════════
|
| 2 |
-
# 🦞 HuggingClaw
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
# ════════════════════════════════════════════════════════════════
|
| 4 |
-
# Multi-stage build: uses pre-built OpenClaw image for fast builds
|
| 5 |
|
| 6 |
# ── Stage 1: Pull pre-built OpenClaw ──
|
| 7 |
ARG OPENCLAW_VERSION=latest
|
|
@@ -10,8 +14,10 @@ FROM ghcr.io/openclaw/openclaw:${OPENCLAW_VERSION} AS openclaw
|
|
| 10 |
# ── Stage 2: Runtime ──
|
| 11 |
FROM node:22-slim
|
| 12 |
ARG OPENCLAW_VERSION=latest
|
|
|
|
|
|
|
| 13 |
|
| 14 |
-
# Install system dependencies
|
| 15 |
RUN apt-get update && apt-get install -y \
|
| 16 |
git \
|
| 17 |
sudo \
|
|
@@ -45,9 +51,19 @@ RUN apt-get update && apt-get install -y \
|
|
| 45 |
pip3 install --no-cache-dir --break-system-packages huggingface_hub && \
|
| 46 |
rm -rf /var/lib/apt/lists/*
|
| 47 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
# Reuse existing node user (UID 1000). Allow passwordless package-manager
|
| 49 |
-
# commands only so runtime apt installs can be replayed after HF Space restarts
|
| 50 |
-
# without granting unrestricted sudo access.
|
| 51 |
RUN mkdir -p /home/node/app /home/node/.openclaw && \
|
| 52 |
chown -R 1000:1000 /home/node && \
|
| 53 |
printf '%s\n' \
|
|
@@ -60,8 +76,7 @@ RUN mkdir -p /home/node/app /home/node/.openclaw && \
|
|
| 60 |
# Copy pre-built OpenClaw (skips npm install entirely — much faster!)
|
| 61 |
COPY --from=openclaw --chown=1000:1000 /app /home/node/.openclaw/openclaw-app
|
| 62 |
|
| 63 |
-
# Add Playwright in an isolated sidecar node_modules
|
| 64 |
-
# bundled OpenClaw app dependency tree.
|
| 65 |
RUN mkdir -p /home/node/browser-deps && \
|
| 66 |
cd /home/node/browser-deps && \
|
| 67 |
npm init -y && \
|
|
@@ -75,14 +90,22 @@ RUN ln -s /home/node/.openclaw/openclaw-app/openclaw.mjs /usr/local/bin/openclaw
|
|
| 75 |
COPY --chown=1000:1000 cloudflare-proxy.js /opt/cloudflare-proxy.js
|
| 76 |
COPY --chown=1000:1000 cloudflare-proxy-setup.py /home/node/app/cloudflare-proxy-setup.py
|
| 77 |
COPY --chown=1000:1000 health-server.js /home/node/app/health-server.js
|
|
|
|
| 78 |
COPY --chown=1000:1000 iframe-fix.cjs /home/node/app/iframe-fix.cjs
|
| 79 |
COPY --chown=1000:1000 start.sh /home/node/app/start.sh
|
| 80 |
COPY --chown=1000:1000 wa-guardian.js /home/node/app/wa-guardian.js
|
| 81 |
COPY --chown=1000:1000 cloudflare-keepalive-setup.py /home/node/app/cloudflare-keepalive-setup.py
|
| 82 |
COPY --chown=1000:1000 openclaw-sync.py /home/node/app/openclaw-sync.py
|
| 83 |
-
RUN chmod +x /home/node/app/start.sh /home/node/app/cloudflare-proxy-setup.py /home/node/app/cloudflare-keepalive-setup.py /home/node/app/openclaw-sync.py
|
| 84 |
COPY --chown=1000:1000 multi-provider-key-rotator.cjs /home/node/app/multi-provider-key-rotator.cjs
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
|
| 87 |
USER node
|
| 88 |
|
|
@@ -94,10 +117,9 @@ ENV HOME=/home/node \
|
|
| 94 |
|
| 95 |
WORKDIR /home/node/app
|
| 96 |
|
|
|
|
| 97 |
EXPOSE 7861
|
| 98 |
|
| 99 |
-
# health-server.js exposes /health on 7861 and proxies to the gateway on 7860.
|
| 100 |
-
# 90s start period covers OpenClaw's plugin install + gateway boot on cold start.
|
| 101 |
HEALTHCHECK --interval=30s --timeout=5s --start-period=90s \
|
| 102 |
CMD curl -fsS http://localhost:7861/health || exit 1
|
| 103 |
|
|
|
|
| 1 |
# ════════════════════════════════════════════════════════════════
|
| 2 |
+
# 🦞 HuggingClaw + 💻 JupyterLab Terminal
|
| 3 |
+
# ════════════════════════════════════════════════════════════════
|
| 4 |
+
# Port 7861 (exposed): Dashboard + reverse proxy
|
| 5 |
+
# / → HuggingClaw dashboard
|
| 6 |
+
# /app/ → OpenClaw gateway (internal :7860)
|
| 7 |
+
# /terminal/ → JupyterLab terminal (internal :8888)
|
| 8 |
# ════════════════════════════════════════════════════════════════
|
|
|
|
| 9 |
|
| 10 |
# ── Stage 1: Pull pre-built OpenClaw ──
|
| 11 |
ARG OPENCLAW_VERSION=latest
|
|
|
|
| 14 |
# ── Stage 2: Runtime ──
|
| 15 |
FROM node:22-slim
|
| 16 |
ARG OPENCLAW_VERSION=latest
|
| 17 |
+
ARG DEV_MODE=false
|
| 18 |
+
ENV DEV_MODE=${DEV_MODE}
|
| 19 |
|
| 20 |
+
# Install system dependencies (+ optional JupyterLab deps in DEV_MODE)
|
| 21 |
RUN apt-get update && apt-get install -y \
|
| 22 |
git \
|
| 23 |
sudo \
|
|
|
|
| 51 |
pip3 install --no-cache-dir --break-system-packages huggingface_hub && \
|
| 52 |
rm -rf /var/lib/apt/lists/*
|
| 53 |
|
| 54 |
+
# Install JupyterLab only when DEV_MODE is enabled (build-time)
|
| 55 |
+
# This avoids installing large packages when terminal is not needed
|
| 56 |
+
RUN if [ "${DEV_MODE}" = "true" ] || [ "${DEV_MODE}" = "1" ] || [ "${DEV_MODE}" = "yes" ] || [ "${DEV_MODE}" = "on" ]; then \
|
| 57 |
+
pip3 install --no-cache-dir --break-system-packages \
|
| 58 |
+
jupyterlab==4.5.7 \
|
| 59 |
+
tornado==6.5.5 \
|
| 60 |
+
ipywidgets==8.1.8 && \
|
| 61 |
+
# Copy login template into jupyter_server templates dir
|
| 62 |
+
python3 -c "from pathlib import Path; import shutil, jupyter_server; d=Path(jupyter_server.__file__).parent/'templates'; d.mkdir(parents=True,exist_ok=True); shutil.copyfile('/home/node/app/login.html', d/'login.html')" || true; \
|
| 63 |
+
fi
|
| 64 |
+
|
| 65 |
# Reuse existing node user (UID 1000). Allow passwordless package-manager
|
| 66 |
+
# commands only so runtime apt installs can be replayed after HF Space restarts.
|
|
|
|
| 67 |
RUN mkdir -p /home/node/app /home/node/.openclaw && \
|
| 68 |
chown -R 1000:1000 /home/node && \
|
| 69 |
printf '%s\n' \
|
|
|
|
| 76 |
# Copy pre-built OpenClaw (skips npm install entirely — much faster!)
|
| 77 |
COPY --from=openclaw --chown=1000:1000 /app /home/node/.openclaw/openclaw-app
|
| 78 |
|
| 79 |
+
# Add Playwright in an isolated sidecar node_modules
|
|
|
|
| 80 |
RUN mkdir -p /home/node/browser-deps && \
|
| 81 |
cd /home/node/browser-deps && \
|
| 82 |
npm init -y && \
|
|
|
|
| 90 |
COPY --chown=1000:1000 cloudflare-proxy.js /opt/cloudflare-proxy.js
|
| 91 |
COPY --chown=1000:1000 cloudflare-proxy-setup.py /home/node/app/cloudflare-proxy-setup.py
|
| 92 |
COPY --chown=1000:1000 health-server.js /home/node/app/health-server.js
|
| 93 |
+
COPY --chown=1000:1000 login.html /home/node/app/login.html
|
| 94 |
COPY --chown=1000:1000 iframe-fix.cjs /home/node/app/iframe-fix.cjs
|
| 95 |
COPY --chown=1000:1000 start.sh /home/node/app/start.sh
|
| 96 |
COPY --chown=1000:1000 wa-guardian.js /home/node/app/wa-guardian.js
|
| 97 |
COPY --chown=1000:1000 cloudflare-keepalive-setup.py /home/node/app/cloudflare-keepalive-setup.py
|
| 98 |
COPY --chown=1000:1000 openclaw-sync.py /home/node/app/openclaw-sync.py
|
|
|
|
| 99 |
COPY --chown=1000:1000 multi-provider-key-rotator.cjs /home/node/app/multi-provider-key-rotator.cjs
|
| 100 |
+
COPY --chown=1000:1000 env-builder.html /home/node/app/env-builder.html
|
| 101 |
+
COPY --chown=1000:1000 env-builder.js /home/node/app/env-builder.js
|
| 102 |
+
COPY --chown=1000:1000 jupyter-devdata-sync.py /home/node/app/jupyter-devdata-sync.py
|
| 103 |
+
# login.html template is now copied inside the DEV_MODE install block above
|
| 104 |
+
RUN chmod +x /home/node/app/start.sh \
|
| 105 |
+
/home/node/app/cloudflare-proxy-setup.py \
|
| 106 |
+
/home/node/app/cloudflare-keepalive-setup.py \
|
| 107 |
+
/home/node/app/openclaw-sync.py \
|
| 108 |
+
/home/node/app/multi-provider-key-rotator.cjs
|
| 109 |
|
| 110 |
USER node
|
| 111 |
|
|
|
|
| 117 |
|
| 118 |
WORKDIR /home/node/app
|
| 119 |
|
| 120 |
+
# 7861 = public entrypoint (dashboard + proxy for both OpenClaw and JupyterLab)
|
| 121 |
EXPOSE 7861
|
| 122 |
|
|
|
|
|
|
|
| 123 |
HEALTHCHECK --interval=30s --timeout=5s --start-period=90s \
|
| 124 |
CMD curl -fsS http://localhost:7861/health || exit 1
|
| 125 |
|
README.md
CHANGED
|
@@ -1,12 +1,17 @@
|
|
| 1 |
---
|
| 2 |
title: HuggingClaw
|
| 3 |
emoji: 🦞
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
app_port: 7861
|
| 8 |
-
pinned:
|
| 9 |
license: mit
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
secrets:
|
| 11 |
- name: LLM_API_KEY
|
| 12 |
description: "Your LLM provider API key (e.g. Anthropic, OpenAI, Google, OpenRouter)."
|
|
@@ -14,6 +19,8 @@ secrets:
|
|
| 14 |
description: "Model ID to use, e.g. google/gemini-2.5-flash or openai/gpt-4o."
|
| 15 |
- name: GATEWAY_TOKEN
|
| 16 |
description: "Strong token to secure your OpenClaw Control UI (generate: openssl rand -hex 32)."
|
|
|
|
|
|
|
| 17 |
- name: CLOUDFLARE_WORKERS_TOKEN
|
| 18 |
description: "Cloudflare API token — auto-creates a Worker proxy and KeepAlive monitor."
|
| 19 |
- name: TELEGRAM_ALLOWED_USERS
|
|
@@ -32,7 +39,7 @@ secrets:
|
|
| 32 |
[](https://huggingface.co/spaces)
|
| 33 |
[](https://github.com/openclaw/openclaw)
|
| 34 |
|
| 35 |
-
**Your always-on AI assistant — free, no server needed.**
|
| 36 |
|
| 37 |
## Table of Contents
|
| 38 |
|
|
@@ -49,6 +56,8 @@ secrets:
|
|
| 49 |
- [🤖 LLM Providers](#-llm-providers)
|
| 50 |
- [💻 Local Development](#-local-development)
|
| 51 |
- [🔗 CLI Access](#-cli-access)
|
|
|
|
|
|
|
| 52 |
- [🏗️ Architecture](#-architecture)
|
| 53 |
- [💓 Staying Alive](#-staying-alive)
|
| 54 |
- [🐛 Troubleshooting](#-troubleshooting)
|
|
@@ -69,6 +78,7 @@ secrets:
|
|
| 69 |
- 📊 **Visual Dashboard:** Beautiful Web UI to monitor uptime, sync status, and active models.
|
| 70 |
- 🔔 **Webhooks:** Get notified on restarts or backup failures via standard webhooks.
|
| 71 |
- 🔐 **Flexible Auth:** Secure the Control UI with either a gateway token or password.
|
|
|
|
| 72 |
- 🏠 **100% HF-Native:** Runs entirely on HuggingFace’s free infrastructure (2 vCPU, 16GB RAM).
|
| 73 |
|
| 74 |
## 🎥 Video Tutorial
|
|
@@ -94,7 +104,7 @@ Navigate to your new Space's **Settings**, scroll down to the **Variables and se
|
|
| 94 |
> [!TIP]
|
| 95 |
> HuggingClaw is completely flexible! You only need these three secrets to get started. You can set other secrets later.
|
| 96 |
|
| 97 |
-
Optional:
|
| 98 |
|
| 99 |
### Step 3: Deploy & Run
|
| 100 |
|
|
@@ -348,6 +358,30 @@ openclaw channels login --gateway https://YOUR_SPACE_NAME.hf.space
|
|
| 348 |
# When prompted, enter your GATEWAY_TOKEN
|
| 349 |
```
|
| 350 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 351 |
## 🏗️ Architecture
|
| 352 |
|
| 353 |
HuggingClaw uses a multi-layered approach to ensure stability and persistence on Hugging Face's ephemeral infrastructure.
|
|
@@ -355,8 +389,9 @@ HuggingClaw uses a multi-layered approach to ensure stability and persistence on
|
|
| 355 |
<details>
|
| 356 |
<summary><b>Click to view technical details</b></summary>
|
| 357 |
|
| 358 |
-
- **Dashboard (`/`)**: Management, monitoring, and keep-alive tools.
|
| 359 |
-
- **Control UI (`/
|
|
|
|
| 360 |
- **Health Check (`/health`)**: Endpoint for uptime monitoring and readiness probes.
|
| 361 |
- **Sync Engine**: Python background process managing HF Dataset persistence.
|
| 362 |
- **Transparent Proxy**: Interceptor for requests to blocked domains (Telegram, etc.).
|
|
@@ -367,12 +402,15 @@ HuggingClaw uses a multi-layered approach to ensure stability and persistence on
|
|
| 367 |
2. Resolve backup namespace and restore workspace from HF Dataset.
|
| 368 |
3. Generate `openclaw.json` configuration.
|
| 369 |
4. Launch background tasks (auto-sync, channel helpers).
|
| 370 |
-
5. Start OpenClaw gateway
|
| 371 |
|
| 372 |
</details>
|
| 373 |
|
| 374 |
## 🐛 Troubleshooting
|
| 375 |
|
|
|
|
|
|
|
|
|
|
| 376 |
- **Missing secrets:** Ensure `LLM_API_KEY`, `LLM_MODEL`, and `GATEWAY_TOKEN` are set in your Space **Settings → Secrets**.
|
| 377 |
- **Telegram bot issues:** Verify your `TELEGRAM_BOT_TOKEN`. Check Space logs for lines like `📱 Enabling Telegram`.
|
| 378 |
- **Backup restore failing:** Make sure `HF_TOKEN` is valid and has write access to your HF account dataset. Set `HF_USERNAME` only if auto-detection is not available in your environment.
|
|
@@ -397,9 +435,9 @@ Similar projects by [@somratpro](https://github.com/somratpro) — all free, one
|
|
| 397 |
|
| 398 |
## 📚 Links
|
| 399 |
|
| 400 |
-
- [OpenClaw Docs](https://docs.openclaw.ai)
|
| 401 |
-
- [OpenClaw GitHub](https://github.com/openclaw/openclaw)
|
| 402 |
-
- [HuggingFace Spaces Docs](https://huggingface.co/docs/hub/spaces)
|
| 403 |
|
| 404 |
## ❤️ Support
|
| 405 |
|
|
@@ -422,4 +460,4 @@ Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for gui
|
|
| 422 |
|
| 423 |
MIT — see [LICENSE](LICENSE) for details.
|
| 424 |
|
| 425 |
-
*Made with ❤️ by [@somratpro](https://github.com/somratpro) for the OpenClaw community.*
|
|
|
|
| 1 |
---
|
| 2 |
title: HuggingClaw
|
| 3 |
emoji: 🦞
|
| 4 |
+
colorFrom: red
|
| 5 |
+
colorTo: blue
|
| 6 |
sdk: docker
|
| 7 |
app_port: 7861
|
| 8 |
+
pinned: false
|
| 9 |
license: mit
|
| 10 |
+
tags:
|
| 11 |
+
- openclaw
|
| 12 |
+
- jupyterlab
|
| 13 |
+
- terminal
|
| 14 |
+
- llm-gateway
|
| 15 |
secrets:
|
| 16 |
- name: LLM_API_KEY
|
| 17 |
description: "Your LLM provider API key (e.g. Anthropic, OpenAI, Google, OpenRouter)."
|
|
|
|
| 19 |
description: "Model ID to use, e.g. google/gemini-2.5-flash or openai/gpt-4o."
|
| 20 |
- name: GATEWAY_TOKEN
|
| 21 |
description: "Strong token to secure your OpenClaw Control UI (generate: openssl rand -hex 32)."
|
| 22 |
+
- name: JUPYTER_TOKEN
|
| 23 |
+
description: "Optional strong token for the JupyterLab terminal at /terminal/ (defaults to huggingface)."
|
| 24 |
- name: CLOUDFLARE_WORKERS_TOKEN
|
| 25 |
description: "Cloudflare API token — auto-creates a Worker proxy and KeepAlive monitor."
|
| 26 |
- name: TELEGRAM_ALLOWED_USERS
|
|
|
|
| 39 |
[](https://huggingface.co/spaces)
|
| 40 |
[](https://github.com/openclaw/openclaw)
|
| 41 |
|
| 42 |
+
**Your always-on AI assistant — free, no server needed.** This merged Space runs [OpenClaw](https://openclaw.ai) plus a Hugging Face-style JupyterLab terminal on one HF Spaces port, giving you a 24/7 AI chat assistant on Telegram and WhatsApp. It works with *any* large language model (LLM) – Claude, ChatGPT, Gemini, etc. – and even supports custom models via [OpenRouter](https://openrouter.ai). Deploy in minutes on the free HF Spaces tier (2 vCPU, 16GB RAM, 50GB) with automatic workspace backup to a HuggingFace Dataset so your chat history and settings persist across restarts.
|
| 43 |
|
| 44 |
## Table of Contents
|
| 45 |
|
|
|
|
| 56 |
- [🤖 LLM Providers](#-llm-providers)
|
| 57 |
- [💻 Local Development](#-local-development)
|
| 58 |
- [🔗 CLI Access](#-cli-access)
|
| 59 |
+
- [💻 JupyterLab Terminal](#-jupyterlab-terminal)
|
| 60 |
+
- [🔍 Merge Comparison](#-merge-comparison)
|
| 61 |
- [🏗️ Architecture](#-architecture)
|
| 62 |
- [💓 Staying Alive](#-staying-alive)
|
| 63 |
- [🐛 Troubleshooting](#-troubleshooting)
|
|
|
|
| 78 |
- 📊 **Visual Dashboard:** Beautiful Web UI to monitor uptime, sync status, and active models.
|
| 79 |
- 🔔 **Webhooks:** Get notified on restarts or backup failures via standard webhooks.
|
| 80 |
- 🔐 **Flexible Auth:** Secure the Control UI with either a gateway token or password.
|
| 81 |
+
- 💻 **Optional Dev Terminal:** JupyterLab is available at `/terminal/` only when `DEV_MODE=true` (disabled by default).
|
| 82 |
- 🏠 **100% HF-Native:** Runs entirely on HuggingFace’s free infrastructure (2 vCPU, 16GB RAM).
|
| 83 |
|
| 84 |
## 🎥 Video Tutorial
|
|
|
|
| 104 |
> [!TIP]
|
| 105 |
> HuggingClaw is completely flexible! You only need these three secrets to get started. You can set other secrets later.
|
| 106 |
|
| 107 |
+
Optional: set `DEV_MODE=true` (Variable) to enable JupyterLab support and install Jupyter dependencies at build time. You can also set `JUPYTER_TOKEN` as a Secret to replace the default terminal token (`huggingface`). If you want to pin a specific OpenClaw release instead of `latest`, add `OPENCLAW_VERSION` under **Variables** in your Space settings. For Docker Spaces, HF passes Variables as build args during image build, so these should be Variables, not Secrets (except tokens).
|
| 108 |
|
| 109 |
### Step 3: Deploy & Run
|
| 110 |
|
|
|
|
| 358 |
# When prompted, enter your GATEWAY_TOKEN
|
| 359 |
```
|
| 360 |
|
| 361 |
+
## 💻 JupyterLab Terminal
|
| 362 |
+
|
| 363 |
+
The merged Space includes the Hugging Face JupyterLab template behavior inside the same container:
|
| 364 |
+
|
| 365 |
+
| Path | Service | Internal Port | Notes |
|
| 366 |
+
| :--- | :--- | :--- | :--- |
|
| 367 |
+
| `/` | HuggingClaw dashboard | `7861` | Public HF Spaces entrypoint |
|
| 368 |
+
| `/app/` | OpenClaw Control UI | `7860` | Mounted behind the local reverse proxy |
|
| 369 |
+
| `/terminal/` | JupyterLab terminal (DEV_MODE only) | `8888` | Available only when `DEV_MODE=true`; token login uses `JUPYTER_TOKEN` (default `huggingface`) |
|
| 370 |
+
|
| 371 |
+
When enabled, the terminal notebook root is `/home/node`, so you can inspect HuggingClaw files, logs, workspace state, and runtime scripts from the browser.
|
| 372 |
+
|
| 373 |
+
> [!IMPORTANT]
|
| 374 |
+
> For real deployments, set a strong `JUPYTER_TOKEN` secret. The `huggingface` default exists only to match the duplicateable Hugging Face JupyterLab template.
|
| 375 |
+
|
| 376 |
+
## 🔍 Merge Comparison
|
| 377 |
+
|
| 378 |
+
This repository is a merge of two sources:
|
| 379 |
+
|
| 380 |
+
- `anurag162008/HuggingClaw`: OpenClaw gateway, dashboard, Cloudflare proxy/keep-alive, Telegram/WhatsApp helpers, backup sync, key rotation, docs, and security metadata.
|
| 381 |
+
- Hugging Face `SpacesExamples/jupyterlab` template: JupyterLab Docker behavior, token login UX, Hugging Face-branded login template, pinned Jupyter packages, and Git LFS defaults for large model/data artifacts.
|
| 382 |
+
|
| 383 |
+
The main merge-specific change is the single-port router: HF Spaces exposes `7861`, while the router keeps OpenClaw at `/app/` and JupyterLab at `/terminal/` without leaking internal redirects such as `http://127.0.0.1:8888/...`.
|
| 384 |
+
|
| 385 |
## 🏗️ Architecture
|
| 386 |
|
| 387 |
HuggingClaw uses a multi-layered approach to ensure stability and persistence on Hugging Face's ephemeral infrastructure.
|
|
|
|
| 389 |
<details>
|
| 390 |
<summary><b>Click to view technical details</b></summary>
|
| 391 |
|
| 392 |
+
- **Dashboard (`/`)**: Management, monitoring, and keep-alive tools (terminal controls appear only in DEV mode).
|
| 393 |
+
- **Control UI (`/app/`)**: Secure interface for managing agents and channels, proxied to the OpenClaw gateway on internal port `7860`.
|
| 394 |
+
- **JupyterLab Terminal (`/terminal/`)**: Browser terminal/notebook server on internal port `8888` (DEV mode only).
|
| 395 |
- **Health Check (`/health`)**: Endpoint for uptime monitoring and readiness probes.
|
| 396 |
- **Sync Engine**: Python background process managing HF Dataset persistence.
|
| 397 |
- **Transparent Proxy**: Interceptor for requests to blocked domains (Telegram, etc.).
|
|
|
|
| 402 |
2. Resolve backup namespace and restore workspace from HF Dataset.
|
| 403 |
3. Generate `openclaw.json` configuration.
|
| 404 |
4. Launch background tasks (auto-sync, channel helpers).
|
| 405 |
+
5. Start the local dashboard/reverse proxy and OpenClaw gateway (JupyterLab starts only when `DEV_MODE=true`).
|
| 406 |
|
| 407 |
</details>
|
| 408 |
|
| 409 |
## 🐛 Troubleshooting
|
| 410 |
|
| 411 |
+
- **Private Space 404:** If your Space is private, raw `https://<space>.hf.space/app/` or `/terminal/` links can show Hugging Face's own 404 page when opened outside the embedded App session. Open the Space's **App** tab first, then use the in-page dashboard buttons for `/app/` and `/terminal/`.
|
| 412 |
+
- **Terminal 404 or redirect loop:** Open `/terminal/` with the trailing slash from the dashboard/App tab, rebuild after Dockerfile changes, and confirm `JUPYTER_TOKEN` is set correctly if you changed the default.
|
| 413 |
+
- **Control UI 404:** Open `/app/` with the trailing slash from the dashboard/App tab; the reverse proxy rewrites backend redirects into this mount path.
|
| 414 |
- **Missing secrets:** Ensure `LLM_API_KEY`, `LLM_MODEL`, and `GATEWAY_TOKEN` are set in your Space **Settings → Secrets**.
|
| 415 |
- **Telegram bot issues:** Verify your `TELEGRAM_BOT_TOKEN`. Check Space logs for lines like `📱 Enabling Telegram`.
|
| 416 |
- **Backup restore failing:** Make sure `HF_TOKEN` is valid and has write access to your HF account dataset. Set `HF_USERNAME` only if auto-detection is not available in your environment.
|
|
|
|
| 435 |
|
| 436 |
## 📚 Links
|
| 437 |
|
| 438 |
+
- [OpenClaw Docs](https://docs.openclaw.ai)
|
| 439 |
+
- [OpenClaw GitHub](https://github.com/openclaw/openclaw)
|
| 440 |
+
- [HuggingFace Spaces Docs](https://huggingface.co/docs/hub/spaces)
|
| 441 |
|
| 442 |
## ❤️ Support
|
| 443 |
|
|
|
|
| 460 |
|
| 461 |
MIT — see [LICENSE](LICENSE) for details.
|
| 462 |
|
| 463 |
+
*Made with ❤️ by [@somratpro](https://github.com/somratpro) for the OpenClaw community.*
|
SECURITY.md
CHANGED
|
@@ -16,6 +16,7 @@ When deploying HuggingClaw:
|
|
| 16 |
|
| 17 |
- **Set your Space to Private** — prevents unauthorized access to your gateway
|
| 18 |
- **Use a strong `GATEWAY_TOKEN`** — generate with `openssl rand -hex 32`
|
|
|
|
| 19 |
- **Keep your HF token scoped** — use fine-grained tokens with minimum permissions
|
| 20 |
- **Don't commit `.env` files** — the `.gitignore` already excludes them
|
| 21 |
- **Use `TELEGRAM_ALLOWED_USERS`** — restricts bot access to your account only
|
|
|
|
| 16 |
|
| 17 |
- **Set your Space to Private** — prevents unauthorized access to your gateway
|
| 18 |
- **Use a strong `GATEWAY_TOKEN`** — generate with `openssl rand -hex 32`
|
| 19 |
+
- **Set a strong `JUPYTER_TOKEN`** — the `/terminal/` JupyterLab login defaults to `huggingface` only for template convenience
|
| 20 |
- **Keep your HF token scoped** — use fine-grained tokens with minimum permissions
|
| 21 |
- **Don't commit `.env` files** — the `.gitignore` already excludes them
|
| 22 |
- **Use `TELEGRAM_ALLOWED_USERS`** — restricts bot access to your account only
|
cloudflare-proxy.js
CHANGED
|
@@ -32,6 +32,7 @@ const DEFAULT_PROXY_DOMAINS = [
|
|
| 32 |
"open.tiktokapis.com", "oauth.reddit.com",
|
| 33 |
"youtube.com", "www.youtube.com",
|
| 34 |
"api.openai.com",
|
|
|
|
| 35 |
"api.resend.com", "api.sendgrid.com", "api.mailgun.net",
|
| 36 |
"googleapis.com", "google.com", "googleusercontent.com", "gstatic.com",
|
| 37 |
];
|
|
|
|
| 32 |
"open.tiktokapis.com", "oauth.reddit.com",
|
| 33 |
"youtube.com", "www.youtube.com",
|
| 34 |
"api.openai.com",
|
| 35 |
+
"integrate.api.nvidia.com", "api.nvidia.com",
|
| 36 |
"api.resend.com", "api.sendgrid.com", "api.mailgun.net",
|
| 37 |
"googleapis.com", "google.com", "googleusercontent.com", "gstatic.com",
|
| 38 |
];
|
env-builder.html
ADDED
|
@@ -0,0 +1,819 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>HuggingClaw · ENV Builder</title>
|
| 7 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 8 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 9 |
+
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Syne:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
| 10 |
+
|
| 11 |
+
<style>
|
| 12 |
+
/* ── Reset & Tokens ── */
|
| 13 |
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
| 14 |
+
|
| 15 |
+
:root {
|
| 16 |
+
--bg: #0b0c0f;
|
| 17 |
+
--bg2: #111318;
|
| 18 |
+
--bg3: #181c23;
|
| 19 |
+
--bg4: #1e2330;
|
| 20 |
+
--border: #252b38;
|
| 21 |
+
--border2: #2e3648;
|
| 22 |
+
--amber: #f5a623;
|
| 23 |
+
--amber2: #ffbe55;
|
| 24 |
+
--amber-dim: rgba(245,166,35,.12);
|
| 25 |
+
--amber-glow:rgba(245,166,35,.22);
|
| 26 |
+
--green: #3dd68c;
|
| 27 |
+
--red: #f05f5f;
|
| 28 |
+
--blue: #5b8af5;
|
| 29 |
+
--text: #e4e8f0;
|
| 30 |
+
--text2: #8d97ad;
|
| 31 |
+
--text3: #535f76;
|
| 32 |
+
--mono: 'JetBrains Mono', monospace;
|
| 33 |
+
--sans: 'Syne', sans-serif;
|
| 34 |
+
--r: 8px;
|
| 35 |
+
--r2: 12px;
|
| 36 |
+
--sidebar-w: 220px;
|
| 37 |
+
--panel-w: 340px;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
html { scroll-behavior: smooth; }
|
| 41 |
+
|
| 42 |
+
body {
|
| 43 |
+
font-family: var(--sans);
|
| 44 |
+
background: var(--bg);
|
| 45 |
+
color: var(--text);
|
| 46 |
+
min-height: 100vh;
|
| 47 |
+
display: flex;
|
| 48 |
+
flex-direction: column;
|
| 49 |
+
overflow-x: hidden;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
/* ── Topbar ── */
|
| 53 |
+
.topbar {
|
| 54 |
+
position: sticky;
|
| 55 |
+
top: 0;
|
| 56 |
+
z-index: 100;
|
| 57 |
+
height: 52px;
|
| 58 |
+
background: rgba(11,12,15,.9);
|
| 59 |
+
backdrop-filter: blur(14px);
|
| 60 |
+
border-bottom: 1px solid var(--border);
|
| 61 |
+
display: flex;
|
| 62 |
+
align-items: center;
|
| 63 |
+
padding: 0 20px;
|
| 64 |
+
gap: 16px;
|
| 65 |
+
flex-shrink: 0;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
.topbar-logo {
|
| 69 |
+
display: flex;
|
| 70 |
+
align-items: center;
|
| 71 |
+
gap: 10px;
|
| 72 |
+
flex-shrink: 0;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
.topbar-logo svg { width: 26px; height: 26px; }
|
| 76 |
+
|
| 77 |
+
.topbar-wordmark {
|
| 78 |
+
font-weight: 800;
|
| 79 |
+
font-size: 14px;
|
| 80 |
+
letter-spacing: -.2px;
|
| 81 |
+
color: var(--text);
|
| 82 |
+
white-space: nowrap;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.topbar-wordmark em {
|
| 86 |
+
color: var(--amber);
|
| 87 |
+
font-style: normal;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
.topbar-divider {
|
| 91 |
+
width: 1px;
|
| 92 |
+
height: 22px;
|
| 93 |
+
background: var(--border2);
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
.topbar-title {
|
| 97 |
+
font-size: 12px;
|
| 98 |
+
font-weight: 600;
|
| 99 |
+
color: var(--text2);
|
| 100 |
+
letter-spacing: .5px;
|
| 101 |
+
text-transform: uppercase;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
.topbar-spacer { flex: 1; }
|
| 105 |
+
|
| 106 |
+
.topbar-pill {
|
| 107 |
+
font-family: var(--mono);
|
| 108 |
+
font-size: 10px;
|
| 109 |
+
color: var(--amber);
|
| 110 |
+
background: var(--amber-dim);
|
| 111 |
+
border: 1px solid var(--amber-glow);
|
| 112 |
+
border-radius: 20px;
|
| 113 |
+
padding: 3px 10px;
|
| 114 |
+
letter-spacing: .5px;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
/* ── Layout ── */
|
| 118 |
+
.layout {
|
| 119 |
+
display: flex;
|
| 120 |
+
flex: 1;
|
| 121 |
+
min-height: 0;
|
| 122 |
+
height: calc(100vh - 52px);
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
/* ── Sidebar ── */
|
| 126 |
+
.sidebar-wrap {
|
| 127 |
+
width: var(--sidebar-w);
|
| 128 |
+
flex-shrink: 0;
|
| 129 |
+
border-right: 1px solid var(--border);
|
| 130 |
+
background: var(--bg2);
|
| 131 |
+
display: flex;
|
| 132 |
+
flex-direction: column;
|
| 133 |
+
overflow: hidden;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
.sidebar-scroll {
|
| 137 |
+
flex: 1;
|
| 138 |
+
overflow-y: auto;
|
| 139 |
+
padding: 14px 10px;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
.sidebar-scroll::-webkit-scrollbar { width: 4px; }
|
| 143 |
+
.sidebar-scroll::-webkit-scrollbar-track { background: transparent; }
|
| 144 |
+
.sidebar-scroll::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 4px; }
|
| 145 |
+
|
| 146 |
+
/* sb-label rendered by JS */
|
| 147 |
+
.sb-label {
|
| 148 |
+
font-size: 9px;
|
| 149 |
+
font-weight: 700;
|
| 150 |
+
text-transform: uppercase;
|
| 151 |
+
letter-spacing: 1.2px;
|
| 152 |
+
color: var(--text3);
|
| 153 |
+
padding: 0 8px 10px;
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
.nav-btn {
|
| 157 |
+
width: 100%;
|
| 158 |
+
display: flex;
|
| 159 |
+
align-items: center;
|
| 160 |
+
gap: 8px;
|
| 161 |
+
padding: 8px 10px;
|
| 162 |
+
border: none;
|
| 163 |
+
background: transparent;
|
| 164 |
+
cursor: pointer;
|
| 165 |
+
border-radius: var(--r);
|
| 166 |
+
text-align: left;
|
| 167 |
+
color: var(--text2);
|
| 168 |
+
font-family: var(--sans);
|
| 169 |
+
font-size: 12.5px;
|
| 170 |
+
font-weight: 500;
|
| 171 |
+
transition: background .15s, color .15s;
|
| 172 |
+
margin-bottom: 2px;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
.nav-btn:hover { background: var(--bg3); color: var(--text); }
|
| 176 |
+
.nav-btn.active {
|
| 177 |
+
background: var(--amber-dim);
|
| 178 |
+
color: var(--amber);
|
| 179 |
+
border: 1px solid var(--amber-glow);
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
.nav-icon { font-size: 13px; flex-shrink: 0; }
|
| 183 |
+
.nav-label { flex: 1; }
|
| 184 |
+
.nav-count {
|
| 185 |
+
font-family: var(--mono);
|
| 186 |
+
font-size: 10px;
|
| 187 |
+
font-weight: 600;
|
| 188 |
+
color: var(--text3);
|
| 189 |
+
background: var(--bg3);
|
| 190 |
+
border-radius: 10px;
|
| 191 |
+
padding: 1px 6px;
|
| 192 |
+
min-width: 20px;
|
| 193 |
+
text-align: center;
|
| 194 |
+
transition: background .2s, color .2s;
|
| 195 |
+
}
|
| 196 |
+
.nav-btn.active .nav-count {
|
| 197 |
+
background: var(--amber-glow);
|
| 198 |
+
color: var(--amber2);
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
/* ── Main ── */
|
| 202 |
+
.main {
|
| 203 |
+
flex: 1;
|
| 204 |
+
display: flex;
|
| 205 |
+
flex-direction: column;
|
| 206 |
+
min-width: 0;
|
| 207 |
+
overflow: hidden;
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
/* ── Toolbar ── */
|
| 211 |
+
.toolbar {
|
| 212 |
+
display: flex;
|
| 213 |
+
align-items: center;
|
| 214 |
+
gap: 10px;
|
| 215 |
+
padding: 12px 20px;
|
| 216 |
+
border-bottom: 1px solid var(--border);
|
| 217 |
+
background: var(--bg2);
|
| 218 |
+
flex-shrink: 0;
|
| 219 |
+
flex-wrap: wrap;
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
.search-wrap {
|
| 223 |
+
position: relative;
|
| 224 |
+
flex: 1;
|
| 225 |
+
min-width: 160px;
|
| 226 |
+
max-width: 340px;
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
.search-icon {
|
| 230 |
+
position: absolute;
|
| 231 |
+
left: 10px;
|
| 232 |
+
top: 50%;
|
| 233 |
+
transform: translateY(-50%);
|
| 234 |
+
color: var(--text3);
|
| 235 |
+
pointer-events: none;
|
| 236 |
+
font-size: 12px;
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
#search {
|
| 240 |
+
width: 100%;
|
| 241 |
+
background: var(--bg3);
|
| 242 |
+
border: 1px solid var(--border2);
|
| 243 |
+
border-radius: var(--r);
|
| 244 |
+
padding: 7px 10px 7px 30px;
|
| 245 |
+
font-family: var(--mono);
|
| 246 |
+
font-size: 12px;
|
| 247 |
+
color: var(--text);
|
| 248 |
+
outline: none;
|
| 249 |
+
transition: border-color .15s;
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
#search:focus { border-color: var(--amber); }
|
| 253 |
+
#search::placeholder { color: var(--text3); }
|
| 254 |
+
|
| 255 |
+
.tb-sep {
|
| 256 |
+
width: 1px;
|
| 257 |
+
height: 24px;
|
| 258 |
+
background: var(--border2);
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
.btn {
|
| 262 |
+
display: inline-flex;
|
| 263 |
+
align-items: center;
|
| 264 |
+
gap: 5px;
|
| 265 |
+
padding: 6px 13px;
|
| 266 |
+
border-radius: var(--r);
|
| 267 |
+
border: 1px solid var(--border2);
|
| 268 |
+
background: var(--bg3);
|
| 269 |
+
color: var(--text2);
|
| 270 |
+
font-family: var(--sans);
|
| 271 |
+
font-size: 11.5px;
|
| 272 |
+
font-weight: 600;
|
| 273 |
+
cursor: pointer;
|
| 274 |
+
transition: all .15s;
|
| 275 |
+
white-space: nowrap;
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
.btn:hover { background: var(--bg4); color: var(--text); border-color: var(--border2); }
|
| 279 |
+
|
| 280 |
+
.btn-amber {
|
| 281 |
+
background: var(--amber);
|
| 282 |
+
color: #0b0c0f;
|
| 283 |
+
border-color: var(--amber);
|
| 284 |
+
}
|
| 285 |
+
.btn-amber:hover { background: var(--amber2); border-color: var(--amber2); }
|
| 286 |
+
|
| 287 |
+
.btn-ghost {
|
| 288 |
+
background: transparent;
|
| 289 |
+
border-color: transparent;
|
| 290 |
+
color: var(--text3);
|
| 291 |
+
}
|
| 292 |
+
.btn-ghost:hover { background: var(--bg3); color: var(--text2); border-color: var(--border2); }
|
| 293 |
+
|
| 294 |
+
/* ── Content area ── */
|
| 295 |
+
.content-wrap {
|
| 296 |
+
flex: 1;
|
| 297 |
+
display: flex;
|
| 298 |
+
min-height: 0;
|
| 299 |
+
overflow: hidden;
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
/* ── Sections scroll ── */
|
| 303 |
+
.sections-scroll {
|
| 304 |
+
flex: 1;
|
| 305 |
+
overflow-y: auto;
|
| 306 |
+
padding: 16px 20px 80px;
|
| 307 |
+
min-width: 0;
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
.sections-scroll::-webkit-scrollbar { width: 5px; }
|
| 311 |
+
.sections-scroll::-webkit-scrollbar-track { background: transparent; }
|
| 312 |
+
.sections-scroll::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 4px; }
|
| 313 |
+
|
| 314 |
+
/* ── Section ── */
|
| 315 |
+
.sec { margin-bottom: 28px; }
|
| 316 |
+
.sec.sec-hidden { display: none !important; }
|
| 317 |
+
|
| 318 |
+
.sec-header {
|
| 319 |
+
display: flex;
|
| 320 |
+
align-items: center;
|
| 321 |
+
gap: 8px;
|
| 322 |
+
margin-bottom: 12px;
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
.sec-icon { font-size: 14px; }
|
| 326 |
+
.sec-title {
|
| 327 |
+
font-size: 11px;
|
| 328 |
+
font-weight: 700;
|
| 329 |
+
text-transform: uppercase;
|
| 330 |
+
letter-spacing: 1.2px;
|
| 331 |
+
color: var(--text3);
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
.sec-line {
|
| 335 |
+
flex: 1;
|
| 336 |
+
height: 1px;
|
| 337 |
+
background: var(--border);
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
.cards {
|
| 341 |
+
display: grid;
|
| 342 |
+
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
| 343 |
+
gap: 10px;
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
/* ── ENV Card ── */
|
| 347 |
+
.env-card {
|
| 348 |
+
background: var(--bg2);
|
| 349 |
+
border: 1px solid var(--border);
|
| 350 |
+
border-radius: var(--r2);
|
| 351 |
+
padding: 12px;
|
| 352 |
+
transition: border-color .2s, background .2s;
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
.env-card:hover { border-color: var(--border2); }
|
| 356 |
+
.env-card.hidden { display: none; }
|
| 357 |
+
|
| 358 |
+
.env-card.selected {
|
| 359 |
+
border-color: var(--amber-glow);
|
| 360 |
+
background: linear-gradient(135deg, var(--bg2) 80%, rgba(245,166,35,.04));
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
.card-top {
|
| 364 |
+
display: flex;
|
| 365 |
+
align-items: flex-start;
|
| 366 |
+
gap: 9px;
|
| 367 |
+
margin-bottom: 9px;
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
.card-check {
|
| 371 |
+
width: 15px;
|
| 372 |
+
height: 15px;
|
| 373 |
+
accent-color: var(--amber);
|
| 374 |
+
flex-shrink: 0;
|
| 375 |
+
margin-top: 2px;
|
| 376 |
+
cursor: pointer;
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
.card-info { flex: 1; min-width: 0; }
|
| 380 |
+
|
| 381 |
+
.card-key {
|
| 382 |
+
font-family: var(--mono);
|
| 383 |
+
font-size: 11.5px;
|
| 384 |
+
font-weight: 600;
|
| 385 |
+
color: var(--text);
|
| 386 |
+
letter-spacing: .3px;
|
| 387 |
+
white-space: nowrap;
|
| 388 |
+
overflow: hidden;
|
| 389 |
+
text-overflow: ellipsis;
|
| 390 |
+
}
|
| 391 |
+
|
| 392 |
+
.card-lbl {
|
| 393 |
+
font-size: 11px;
|
| 394 |
+
color: var(--text3);
|
| 395 |
+
margin-top: 2px;
|
| 396 |
+
line-height: 1.35;
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
.badge {
|
| 400 |
+
flex-shrink: 0;
|
| 401 |
+
font-family: var(--mono);
|
| 402 |
+
font-size: 9px;
|
| 403 |
+
font-weight: 700;
|
| 404 |
+
text-transform: uppercase;
|
| 405 |
+
letter-spacing: .6px;
|
| 406 |
+
padding: 2px 7px;
|
| 407 |
+
border-radius: 20px;
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
.badge-s {
|
| 411 |
+
background: rgba(240,95,95,.12);
|
| 412 |
+
color: var(--red);
|
| 413 |
+
border: 1px solid rgba(240,95,95,.25);
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
.badge-f {
|
| 417 |
+
background: rgba(61,214,140,.1);
|
| 418 |
+
color: var(--green);
|
| 419 |
+
border: 1px solid rgba(61,214,140,.2);
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
/* ── Card inputs ── */
|
| 423 |
+
.card-input { position: relative; }
|
| 424 |
+
|
| 425 |
+
.card-input input[type="text"],
|
| 426 |
+
.card-input input[type="password"],
|
| 427 |
+
.card-input input[type="number"],
|
| 428 |
+
.card-input textarea,
|
| 429 |
+
.card-input select {
|
| 430 |
+
width: 100%;
|
| 431 |
+
background: var(--bg3);
|
| 432 |
+
border: 1px solid var(--border);
|
| 433 |
+
border-radius: var(--r);
|
| 434 |
+
padding: 7px 10px;
|
| 435 |
+
font-family: var(--mono);
|
| 436 |
+
font-size: 11.5px;
|
| 437 |
+
color: var(--text);
|
| 438 |
+
outline: none;
|
| 439 |
+
transition: border-color .15s;
|
| 440 |
+
resize: vertical;
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
.card-input input[type="text"]:focus,
|
| 444 |
+
.card-input input[type="password"]:focus,
|
| 445 |
+
.card-input input[type="number"]:focus,
|
| 446 |
+
.card-input textarea:focus,
|
| 447 |
+
.card-input select:focus {
|
| 448 |
+
border-color: var(--amber);
|
| 449 |
+
}
|
| 450 |
+
|
| 451 |
+
.card-input textarea { min-height: 64px; }
|
| 452 |
+
.card-input select {
|
| 453 |
+
cursor: pointer;
|
| 454 |
+
appearance: none;
|
| 455 |
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%238d97ad' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
|
| 456 |
+
background-repeat: no-repeat;
|
| 457 |
+
background-position: right 10px center;
|
| 458 |
+
padding-right: 28px;
|
| 459 |
+
}
|
| 460 |
+
|
| 461 |
+
.card-input optgroup { color: var(--text2); font-weight: 600; }
|
| 462 |
+
.card-input option { color: var(--text); background: var(--bg3); }
|
| 463 |
+
|
| 464 |
+
/* ── Toggle ── */
|
| 465 |
+
.toggle-shell { display: flex; align-items: center; gap: 8px; }
|
| 466 |
+
.tog {
|
| 467 |
+
padding: 5px 14px;
|
| 468 |
+
border-radius: 20px;
|
| 469 |
+
border: 1px solid var(--border2);
|
| 470 |
+
background: var(--bg3);
|
| 471 |
+
color: var(--text3);
|
| 472 |
+
font-family: var(--mono);
|
| 473 |
+
font-size: 11px;
|
| 474 |
+
font-weight: 700;
|
| 475 |
+
cursor: pointer;
|
| 476 |
+
transition: all .18s;
|
| 477 |
+
letter-spacing: .5px;
|
| 478 |
+
}
|
| 479 |
+
.tog.on {
|
| 480 |
+
background: rgba(61,214,140,.15);
|
| 481 |
+
border-color: rgba(61,214,140,.4);
|
| 482 |
+
color: var(--green);
|
| 483 |
+
}
|
| 484 |
+
|
| 485 |
+
/* ── Picker shell ── */
|
| 486 |
+
.picker-shell { display: flex; flex-direction: column; gap: 6px; }
|
| 487 |
+
.picker-row { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
|
| 488 |
+
|
| 489 |
+
.picker-select {
|
| 490 |
+
flex: 1;
|
| 491 |
+
min-width: 0;
|
| 492 |
+
padding: 6px 28px 6px 8px !important;
|
| 493 |
+
font-size: 11px !important;
|
| 494 |
+
}
|
| 495 |
+
|
| 496 |
+
.mini-btn {
|
| 497 |
+
padding: 5px 9px;
|
| 498 |
+
border-radius: var(--r);
|
| 499 |
+
border: 1px solid var(--border2);
|
| 500 |
+
background: var(--bg3);
|
| 501 |
+
color: var(--text2);
|
| 502 |
+
font-family: var(--mono);
|
| 503 |
+
font-size: 10px;
|
| 504 |
+
font-weight: 600;
|
| 505 |
+
cursor: pointer;
|
| 506 |
+
transition: all .15s;
|
| 507 |
+
white-space: nowrap;
|
| 508 |
+
}
|
| 509 |
+
.mini-btn:hover { background: var(--bg4); color: var(--text); }
|
| 510 |
+
|
| 511 |
+
/* ── Right panel ── */
|
| 512 |
+
.right-panel {
|
| 513 |
+
width: var(--panel-w);
|
| 514 |
+
flex-shrink: 0;
|
| 515 |
+
border-left: 1px solid var(--border);
|
| 516 |
+
background: var(--bg2);
|
| 517 |
+
display: flex;
|
| 518 |
+
flex-direction: column;
|
| 519 |
+
overflow: hidden;
|
| 520 |
+
}
|
| 521 |
+
|
| 522 |
+
.panel-scroll {
|
| 523 |
+
flex: 1;
|
| 524 |
+
overflow-y: auto;
|
| 525 |
+
padding: 16px;
|
| 526 |
+
display: flex;
|
| 527 |
+
flex-direction: column;
|
| 528 |
+
gap: 16px;
|
| 529 |
+
}
|
| 530 |
+
|
| 531 |
+
.panel-scroll::-webkit-scrollbar { width: 4px; }
|
| 532 |
+
.panel-scroll::-webkit-scrollbar-track { background: transparent; }
|
| 533 |
+
.panel-scroll::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 4px; }
|
| 534 |
+
|
| 535 |
+
/* ── Panel block ── */
|
| 536 |
+
.pblock {
|
| 537 |
+
background: var(--bg3);
|
| 538 |
+
border: 1px solid var(--border);
|
| 539 |
+
border-radius: var(--r2);
|
| 540 |
+
overflow: hidden;
|
| 541 |
+
}
|
| 542 |
+
|
| 543 |
+
.pblock-head {
|
| 544 |
+
display: flex;
|
| 545 |
+
align-items: center;
|
| 546 |
+
justify-content: space-between;
|
| 547 |
+
padding: 10px 14px;
|
| 548 |
+
border-bottom: 1px solid var(--border);
|
| 549 |
+
}
|
| 550 |
+
|
| 551 |
+
.pblock-title {
|
| 552 |
+
font-size: 10.5px;
|
| 553 |
+
font-weight: 700;
|
| 554 |
+
text-transform: uppercase;
|
| 555 |
+
letter-spacing: 1px;
|
| 556 |
+
color: var(--text3);
|
| 557 |
+
display: flex;
|
| 558 |
+
align-items: center;
|
| 559 |
+
gap: 6px;
|
| 560 |
+
}
|
| 561 |
+
|
| 562 |
+
.pblock-body { padding: 12px 14px; display: flex; flex-direction: column; gap: 8px; }
|
| 563 |
+
|
| 564 |
+
.pblock-body textarea,
|
| 565 |
+
.pblock-body input[type="text"] {
|
| 566 |
+
width: 100%;
|
| 567 |
+
background: var(--bg);
|
| 568 |
+
border: 1px solid var(--border);
|
| 569 |
+
border-radius: var(--r);
|
| 570 |
+
padding: 8px 10px;
|
| 571 |
+
font-family: var(--mono);
|
| 572 |
+
font-size: 10.5px;
|
| 573 |
+
color: var(--text2);
|
| 574 |
+
outline: none;
|
| 575 |
+
resize: vertical;
|
| 576 |
+
transition: border-color .15s;
|
| 577 |
+
}
|
| 578 |
+
|
| 579 |
+
.pblock-body textarea:focus,
|
| 580 |
+
.pblock-body input[type="text"]:focus {
|
| 581 |
+
border-color: var(--amber);
|
| 582 |
+
color: var(--text);
|
| 583 |
+
}
|
| 584 |
+
|
| 585 |
+
#importText { min-height: 80px; }
|
| 586 |
+
#bundleOut { min-height: 60px; color: var(--amber2); }
|
| 587 |
+
#envLineOut { font-size: 10px; }
|
| 588 |
+
|
| 589 |
+
.row-btns { display: flex; gap: 6px; flex-wrap: wrap; }
|
| 590 |
+
|
| 591 |
+
/* ── Summary ── */
|
| 592 |
+
#summary {
|
| 593 |
+
font-size: 11.5px;
|
| 594 |
+
color: var(--text2);
|
| 595 |
+
line-height: 1.6;
|
| 596 |
+
}
|
| 597 |
+
|
| 598 |
+
#summary strong {
|
| 599 |
+
font-size: 15px;
|
| 600 |
+
color: var(--amber);
|
| 601 |
+
font-family: var(--mono);
|
| 602 |
+
}
|
| 603 |
+
|
| 604 |
+
.sum-keys {
|
| 605 |
+
margin-top: 8px;
|
| 606 |
+
display: flex;
|
| 607 |
+
flex-wrap: wrap;
|
| 608 |
+
gap: 4px;
|
| 609 |
+
}
|
| 610 |
+
|
| 611 |
+
.sum-key {
|
| 612 |
+
font-family: var(--mono);
|
| 613 |
+
font-size: 9.5px;
|
| 614 |
+
color: var(--text2);
|
| 615 |
+
background: var(--bg4);
|
| 616 |
+
border: 1px solid var(--border2);
|
| 617 |
+
border-radius: 4px;
|
| 618 |
+
padding: 2px 6px;
|
| 619 |
+
}
|
| 620 |
+
|
| 621 |
+
/* ── Custom Env section ── */
|
| 622 |
+
#customSec {
|
| 623 |
+
margin-top: 8px;
|
| 624 |
+
}
|
| 625 |
+
|
| 626 |
+
.custom-row {
|
| 627 |
+
display: flex;
|
| 628 |
+
gap: 8px;
|
| 629 |
+
align-items: center;
|
| 630 |
+
margin-bottom: 8px;
|
| 631 |
+
}
|
| 632 |
+
|
| 633 |
+
.custom-row input {
|
| 634 |
+
flex: 1;
|
| 635 |
+
background: var(--bg3);
|
| 636 |
+
border: 1px solid var(--border);
|
| 637 |
+
border-radius: var(--r);
|
| 638 |
+
padding: 7px 10px;
|
| 639 |
+
font-family: var(--mono);
|
| 640 |
+
font-size: 11px;
|
| 641 |
+
color: var(--text);
|
| 642 |
+
outline: none;
|
| 643 |
+
transition: border-color .15s;
|
| 644 |
+
min-width: 0;
|
| 645 |
+
}
|
| 646 |
+
|
| 647 |
+
.custom-row input:focus { border-color: var(--amber); }
|
| 648 |
+
.custom-row input:first-child { flex: 0 0 40%; }
|
| 649 |
+
|
| 650 |
+
/* ── Toast ── */
|
| 651 |
+
#toast {
|
| 652 |
+
position: fixed;
|
| 653 |
+
bottom: 24px;
|
| 654 |
+
left: 50%;
|
| 655 |
+
transform: translateX(-50%) translateY(20px);
|
| 656 |
+
background: var(--bg4);
|
| 657 |
+
border: 1px solid var(--border2);
|
| 658 |
+
color: var(--amber);
|
| 659 |
+
font-family: var(--mono);
|
| 660 |
+
font-size: 12px;
|
| 661 |
+
font-weight: 600;
|
| 662 |
+
padding: 9px 20px;
|
| 663 |
+
border-radius: 30px;
|
| 664 |
+
z-index: 9999;
|
| 665 |
+
opacity: 0;
|
| 666 |
+
transition: opacity .2s, transform .2s;
|
| 667 |
+
pointer-events: none;
|
| 668 |
+
box-shadow: 0 8px 32px rgba(0,0,0,.5);
|
| 669 |
+
}
|
| 670 |
+
|
| 671 |
+
#toast.show {
|
| 672 |
+
opacity: 1;
|
| 673 |
+
transform: translateX(-50%) translateY(0);
|
| 674 |
+
}
|
| 675 |
+
|
| 676 |
+
/* ── Scrollbar global ── */
|
| 677 |
+
::-webkit-scrollbar { width: 6px; height: 6px; }
|
| 678 |
+
::-webkit-scrollbar-track { background: transparent; }
|
| 679 |
+
::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 4px; }
|
| 680 |
+
|
| 681 |
+
/* ── Responsive ── */
|
| 682 |
+
@media (max-width: 900px) {
|
| 683 |
+
:root { --panel-w: 280px; --sidebar-w: 180px; }
|
| 684 |
+
}
|
| 685 |
+
|
| 686 |
+
@media (max-width: 700px) {
|
| 687 |
+
.right-panel { display: none; }
|
| 688 |
+
:root { --sidebar-w: 160px; }
|
| 689 |
+
}
|
| 690 |
+
|
| 691 |
+
@media (max-width: 520px) {
|
| 692 |
+
.sidebar-wrap { display: none; }
|
| 693 |
+
.topbar-divider, .topbar-title { display: none; }
|
| 694 |
+
}
|
| 695 |
+
</style>
|
| 696 |
+
</head>
|
| 697 |
+
|
| 698 |
+
<body>
|
| 699 |
+
|
| 700 |
+
<!-- ── Topbar ── -->
|
| 701 |
+
<header class="topbar">
|
| 702 |
+
<div class="topbar-logo">
|
| 703 |
+
<svg viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 704 |
+
<rect width="64" height="64" rx="14" fill="#1a1f2e"/>
|
| 705 |
+
<text x="10" y="46" font-size="40" font-family="monospace">🤗</text>
|
| 706 |
+
<circle cx="46" cy="20" r="10" fill="#f5a623"/>
|
| 707 |
+
<text x="40" y="25" font-size="14" font-family="monospace" fill="#0b0c0f">⚡</text>
|
| 708 |
+
</svg>
|
| 709 |
+
<span class="topbar-wordmark">Hugging<em>Claw</em></span>
|
| 710 |
+
</div>
|
| 711 |
+
<div class="topbar-divider"></div>
|
| 712 |
+
<span class="topbar-title">ENV Builder</span>
|
| 713 |
+
<div class="topbar-spacer"></div>
|
| 714 |
+
<span class="topbar-pill">v2025</span>
|
| 715 |
+
</header>
|
| 716 |
+
|
| 717 |
+
<!-- ── Layout ── -->
|
| 718 |
+
<div class="layout">
|
| 719 |
+
|
| 720 |
+
<!-- ── Sidebar ── -->
|
| 721 |
+
<aside class="sidebar-wrap">
|
| 722 |
+
<div class="sidebar-scroll">
|
| 723 |
+
<div id="sidebar"></div>
|
| 724 |
+
</div>
|
| 725 |
+
</aside>
|
| 726 |
+
|
| 727 |
+
<!-- ── Main ── -->
|
| 728 |
+
<main class="main">
|
| 729 |
+
|
| 730 |
+
<!-- toolbar -->
|
| 731 |
+
<div class="toolbar">
|
| 732 |
+
<div class="search-wrap">
|
| 733 |
+
<span class="search-icon">⌕</span>
|
| 734 |
+
<input id="search" type="text" placeholder="Search variables…" autocomplete="off" spellcheck="false">
|
| 735 |
+
</div>
|
| 736 |
+
|
| 737 |
+
<div class="tb-sep"></div>
|
| 738 |
+
|
| 739 |
+
<button id="selectCommon" class="btn">★ Common</button>
|
| 740 |
+
<button id="selectVisible" class="btn">☑ Visible</button>
|
| 741 |
+
<button id="clearAll" class="btn btn-ghost">✕ Clear</button>
|
| 742 |
+
</div>
|
| 743 |
+
|
| 744 |
+
<!-- content row -->
|
| 745 |
+
<div class="content-wrap">
|
| 746 |
+
|
| 747 |
+
<!-- sections -->
|
| 748 |
+
<div class="sections-scroll">
|
| 749 |
+
<div id="sections"></div>
|
| 750 |
+
|
| 751 |
+
<!-- Custom Env section -->
|
| 752 |
+
<div id="customSec" class="sec" data-section="Custom Env">
|
| 753 |
+
<div class="sec-header">
|
| 754 |
+
<span class="sec-icon">🔧</span>
|
| 755 |
+
<span class="sec-title">Custom Env</span>
|
| 756 |
+
<div class="sec-line"></div>
|
| 757 |
+
</div>
|
| 758 |
+
<div id="customRows"></div>
|
| 759 |
+
<button id="addCustom" class="btn" style="margin-top:6px;">+ Add variable</button>
|
| 760 |
+
</div>
|
| 761 |
+
</div>
|
| 762 |
+
|
| 763 |
+
<!-- right panel -->
|
| 764 |
+
<aside class="right-panel">
|
| 765 |
+
<div class="panel-scroll">
|
| 766 |
+
|
| 767 |
+
<!-- Summary -->
|
| 768 |
+
<div class="pblock">
|
| 769 |
+
<div class="pblock-head">
|
| 770 |
+
<span class="pblock-title">📊 Summary</span>
|
| 771 |
+
</div>
|
| 772 |
+
<div class="pblock-body">
|
| 773 |
+
<div id="summary">No variables selected yet.</div>
|
| 774 |
+
</div>
|
| 775 |
+
</div>
|
| 776 |
+
|
| 777 |
+
<!-- Output -->
|
| 778 |
+
<div class="pblock">
|
| 779 |
+
<div class="pblock-head">
|
| 780 |
+
<span class="pblock-title">📦 Bundle Output</span>
|
| 781 |
+
</div>
|
| 782 |
+
<div class="pblock-body">
|
| 783 |
+
<textarea id="bundleOut" placeholder="Your encoded bundle will appear here…" readonly spellcheck="false"></textarea>
|
| 784 |
+
<input type="text" id="envLineOut" placeholder="HUGGINGCLAW_ENV_BUNDLE=…" readonly spellcheck="false">
|
| 785 |
+
<div class="row-btns">
|
| 786 |
+
<button id="copyBundle" class="btn btn-amber">⎘ Bundle</button>
|
| 787 |
+
<button id="copyEnvLine" class="btn">⎘ Env Line</button>
|
| 788 |
+
<button id="copyJson" class="btn">⎘ JSON</button>
|
| 789 |
+
<button id="applyBundle" class="btn btn-ghost">↺ Apply</button>
|
| 790 |
+
</div>
|
| 791 |
+
</div>
|
| 792 |
+
</div>
|
| 793 |
+
|
| 794 |
+
<!-- Import -->
|
| 795 |
+
<div class="pblock">
|
| 796 |
+
<div class="pblock-head">
|
| 797 |
+
<span class="pblock-title">📥 Import</span>
|
| 798 |
+
</div>
|
| 799 |
+
<div class="pblock-body">
|
| 800 |
+
<textarea id="importText" placeholder="Paste .env, JSON, or HUGGINGCLAW_ENV_BUNDLE=… here" spellcheck="false"></textarea>
|
| 801 |
+
<button id="applyImport" class="btn btn-amber" style="width:100%;">↓ Import & Apply</button>
|
| 802 |
+
</div>
|
| 803 |
+
</div>
|
| 804 |
+
|
| 805 |
+
</div>
|
| 806 |
+
</aside>
|
| 807 |
+
|
| 808 |
+
</div><!-- /content-wrap -->
|
| 809 |
+
</main>
|
| 810 |
+
</div><!-- /layout -->
|
| 811 |
+
|
| 812 |
+
<!-- Toast -->
|
| 813 |
+
<div id="toast">Copied ✓</div>
|
| 814 |
+
|
| 815 |
+
<!-- env-builder.js provides MODEL_CATALOGS, FIELDS, ICONS and all logic -->
|
| 816 |
+
<script src="env-builder.js"></script>
|
| 817 |
+
|
| 818 |
+
</body>
|
| 819 |
+
</html>
|
env-builder.js
ADDED
|
@@ -0,0 +1,2341 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const MODEL_CATALOGS = {
|
| 2 |
+
"LLM_MODEL": {
|
| 3 |
+
"Anthropic": [
|
| 4 |
+
"claude-opus-4-7",
|
| 5 |
+
"claude-opus-4-6",
|
| 6 |
+
"claude-opus-4-5",
|
| 7 |
+
"claude-opus-4-1",
|
| 8 |
+
"claude-sonnet-4-7",
|
| 9 |
+
"claude-sonnet-4-6",
|
| 10 |
+
"claude-sonnet-4-5",
|
| 11 |
+
"claude-haiku-4-5",
|
| 12 |
+
"claude-haiku-4-5-20251001",
|
| 13 |
+
"claude-haiku-3-5"
|
| 14 |
+
],
|
| 15 |
+
"OpenAI": [
|
| 16 |
+
"gpt-5.4-pro",
|
| 17 |
+
"gpt-5.4",
|
| 18 |
+
"gpt-5.4-mini",
|
| 19 |
+
"gpt-5.4-nano",
|
| 20 |
+
"gpt-5.1",
|
| 21 |
+
"gpt-5",
|
| 22 |
+
"gpt-4.1",
|
| 23 |
+
"gpt-4.1-mini",
|
| 24 |
+
"gpt-4o",
|
| 25 |
+
"gpt-4o-mini",
|
| 26 |
+
"o3",
|
| 27 |
+
"o4-mini"
|
| 28 |
+
],
|
| 29 |
+
"Gemini": [
|
| 30 |
+
"gemini-3.1-pro-preview",
|
| 31 |
+
"gemini-3.1-flash-preview",
|
| 32 |
+
"gemini-3-flash-preview",
|
| 33 |
+
"gemini-2.5-pro",
|
| 34 |
+
"gemini-2.5-flash",
|
| 35 |
+
"gemini-2.0-flash",
|
| 36 |
+
"gemini-flash-latest",
|
| 37 |
+
"gemini-pro-latest"
|
| 38 |
+
],
|
| 39 |
+
"DeepSeek": [
|
| 40 |
+
"deepseek-v4-pro",
|
| 41 |
+
"deepseek-v4-flash",
|
| 42 |
+
"deepseek-v3.2",
|
| 43 |
+
"deepseek-chat",
|
| 44 |
+
"deepseek-reasoner",
|
| 45 |
+
"deepseek-r1",
|
| 46 |
+
"deepseek-r1-0528"
|
| 47 |
+
],
|
| 48 |
+
"xAI": [
|
| 49 |
+
"grok-4.3",
|
| 50 |
+
"grok-4.1",
|
| 51 |
+
"grok-4",
|
| 52 |
+
"grok-3"
|
| 53 |
+
],
|
| 54 |
+
"Groq": [
|
| 55 |
+
"groq/compound",
|
| 56 |
+
"groq/compound-mini",
|
| 57 |
+
"llama-3.1-8b-instant",
|
| 58 |
+
"llama-3.1-70b-versatile",
|
| 59 |
+
"llama-3.3-70b-versatile",
|
| 60 |
+
"meta-llama/llama-4-scout-17b-16e-instruct",
|
| 61 |
+
"openai/gpt-oss-20b",
|
| 62 |
+
"openai/gpt-oss-120b",
|
| 63 |
+
"qwen/qwen3-32b",
|
| 64 |
+
"mixtral-8x7b-32768"
|
| 65 |
+
],
|
| 66 |
+
"Mistral": [
|
| 67 |
+
"mistral-large-latest",
|
| 68 |
+
"mistral-large-2",
|
| 69 |
+
"mistral-medium-3.5",
|
| 70 |
+
"mistral-small-latest",
|
| 71 |
+
"mistral-small-3.2",
|
| 72 |
+
"devstral-2",
|
| 73 |
+
"ocr-3-premier",
|
| 74 |
+
"voxtral-mini-transcribe-realtime",
|
| 75 |
+
"codestral-latest"
|
| 76 |
+
],
|
| 77 |
+
"Cohere": [
|
| 78 |
+
"command-a",
|
| 79 |
+
"command-a-03-2025",
|
| 80 |
+
"command-a-translate-08-2025",
|
| 81 |
+
"command-a-reasoning-08-2025",
|
| 82 |
+
"command-a-vision-07-2025",
|
| 83 |
+
"command-r7b-12-2024",
|
| 84 |
+
"command-r-08-2024",
|
| 85 |
+
"command-r-plus-08-2024"
|
| 86 |
+
],
|
| 87 |
+
"OpenRouter": [
|
| 88 |
+
"openrouter/free",
|
| 89 |
+
"openrouter/auto",
|
| 90 |
+
"anthropic/claude-opus-4-7",
|
| 91 |
+
"anthropic/claude-sonnet-4-6",
|
| 92 |
+
"anthropic/claude-haiku-4-5",
|
| 93 |
+
"openai/gpt-5.4",
|
| 94 |
+
"openai/gpt-4.1",
|
| 95 |
+
"openai/gpt-4o",
|
| 96 |
+
"openai/gpt-5.1",
|
| 97 |
+
"google/gemini-3.1-pro-preview",
|
| 98 |
+
"google/gemini-2.5-pro",
|
| 99 |
+
"deepseek/deepseek-v3.2",
|
| 100 |
+
"deepseek/deepseek-r1",
|
| 101 |
+
"moonshotai/kimi-k2.5",
|
| 102 |
+
"qwen/qwen3-32b",
|
| 103 |
+
"meta-llama/llama-3.3-70b-instruct"
|
| 104 |
+
],
|
| 105 |
+
"Together": [
|
| 106 |
+
"moonshotai/Kimi-K2.5",
|
| 107 |
+
"deepseek-ai/DeepSeek-R1",
|
| 108 |
+
"Qwen/Qwen3-235B-A22B-Instruct-2507-tput",
|
| 109 |
+
"zai-org/GLM-5.1",
|
| 110 |
+
"google/gemma-4-31B-it",
|
| 111 |
+
"MiniMaxAI/MiniMax-M2.7",
|
| 112 |
+
"meta-llama/Llama-3.3-70B-Instruct-Turbo",
|
| 113 |
+
"openai/gpt-oss-20b",
|
| 114 |
+
"openai/gpt-oss-120b",
|
| 115 |
+
"mistralai/Mistral-Small-3.2-24B-Instruct-2506",
|
| 116 |
+
"moonshotai/Kimi-K2.5-Instruct"
|
| 117 |
+
],
|
| 118 |
+
"OpenCode": [
|
| 119 |
+
"opencode/claude-opus-4-6",
|
| 120 |
+
"opencode/gpt-5.4",
|
| 121 |
+
"opencode-go/kimi-k2.5",
|
| 122 |
+
"opencode-go/qwen3-32b"
|
| 123 |
+
],
|
| 124 |
+
"Cerebras": [
|
| 125 |
+
"cerebras/zai-glm-4.7",
|
| 126 |
+
"cerebras/deepseek-r1",
|
| 127 |
+
"cerebras/llama-4-scout-17b-16e-instruct",
|
| 128 |
+
"cerebras/qwen3-32b"
|
| 129 |
+
],
|
| 130 |
+
"NVIDIA": [
|
| 131 |
+
"nvidia/nemotron-3-super-120b-a12b",
|
| 132 |
+
"nvidia/nemotron-4-340b-instruct",
|
| 133 |
+
"nvidia/llama-3.1-nemotron-70b-instruct"
|
| 134 |
+
],
|
| 135 |
+
"KiloCode": [
|
| 136 |
+
"kilocode/anthropic/claude-opus-4.6",
|
| 137 |
+
"kilocode/anthropic/claude-sonnet-4.6",
|
| 138 |
+
"kilocode/openai/gpt-5.4",
|
| 139 |
+
"kilocode/google/gemini-2.5-pro"
|
| 140 |
+
],
|
| 141 |
+
"Z.AI": [
|
| 142 |
+
"zai-org/GLM-5.1",
|
| 143 |
+
"zai-org/GLM-4.7",
|
| 144 |
+
"zai-org/GLM-4.5"
|
| 145 |
+
],
|
| 146 |
+
"Moonshot": [
|
| 147 |
+
"moonshot/kimi-k2.5",
|
| 148 |
+
"moonshot/kimi-k2.5-thinking",
|
| 149 |
+
"moonshot/kimi-k2.5-coder"
|
| 150 |
+
],
|
| 151 |
+
"MiniMax": [
|
| 152 |
+
"minimax/minimax-m2.7",
|
| 153 |
+
"minimax/minimax-m1.5",
|
| 154 |
+
"minimax/abab6.5s-chat"
|
| 155 |
+
],
|
| 156 |
+
"Xiaomi": [
|
| 157 |
+
"xiaomi/mimo-v1",
|
| 158 |
+
"xiaomi/mimo-v2",
|
| 159 |
+
"xiaomi/mi-mo"
|
| 160 |
+
],
|
| 161 |
+
"Volcano Engine": [
|
| 162 |
+
"volcengine/doubao-seed-1.6",
|
| 163 |
+
"volcengine/doubao-1.5-pro",
|
| 164 |
+
"volcengine/doubao-1.5-lite"
|
| 165 |
+
],
|
| 166 |
+
"BytePlus": [
|
| 167 |
+
"byteplus/seed-1.6",
|
| 168 |
+
"byteplus/deepseek-v3.2",
|
| 169 |
+
"byteplus/doubao-seed-1.6"
|
| 170 |
+
],
|
| 171 |
+
"Qianfan": [
|
| 172 |
+
"qianfan/ernie-4.5",
|
| 173 |
+
"qianfan/ernie-4.5-8k",
|
| 174 |
+
"qianfan/deepseek-v3.2",
|
| 175 |
+
"qianfan/ernie-x1"
|
| 176 |
+
],
|
| 177 |
+
"ModelStudio": [
|
| 178 |
+
"modelstudio/qwen3-max",
|
| 179 |
+
"modelstudio/qwen3-coder",
|
| 180 |
+
"modelstudio/qwen3-32b"
|
| 181 |
+
],
|
| 182 |
+
"Hugging Face": [
|
| 183 |
+
"meta-llama/Llama-3.3-70B-Instruct",
|
| 184 |
+
"Qwen/Qwen3-32B",
|
| 185 |
+
"google/gemma-4-31B-it",
|
| 186 |
+
"deepseek-ai/DeepSeek-V3.2",
|
| 187 |
+
"moonshotai/Kimi-K2.5"
|
| 188 |
+
],
|
| 189 |
+
"Venice": [
|
| 190 |
+
"venice/gpt-5",
|
| 191 |
+
"venice/llama-3.3-70b",
|
| 192 |
+
"venice/deepseek-r1"
|
| 193 |
+
],
|
| 194 |
+
"Synthetic": [
|
| 195 |
+
"synthetic/gpt-5",
|
| 196 |
+
"synthetic/claude-sonnet-4-6"
|
| 197 |
+
],
|
| 198 |
+
"AI Gateway": [
|
| 199 |
+
"openai/gpt-5.4",
|
| 200 |
+
"anthropic/claude-sonnet-4-6",
|
| 201 |
+
"google/gemini-2.5-pro"
|
| 202 |
+
],
|
| 203 |
+
"GitHub Copilot": [
|
| 204 |
+
"github-copilot/gpt-5",
|
| 205 |
+
"github-copilot/gpt-4.1",
|
| 206 |
+
"github-copilot/gpt-4o"
|
| 207 |
+
],
|
| 208 |
+
"ZAI": [
|
| 209 |
+
"zai/glm-5",
|
| 210 |
+
"zai/glm-5-turbo",
|
| 211 |
+
"zai/glm-4.7",
|
| 212 |
+
"zai/glm-4.7-flash"
|
| 213 |
+
],
|
| 214 |
+
"Kimi": [
|
| 215 |
+
"moonshot/kimi-k2.5",
|
| 216 |
+
"moonshot/kimi-k2.5-thinking"
|
| 217 |
+
],
|
| 218 |
+
"HuggingFace": [
|
| 219 |
+
"huggingface/deepseek-ai/DeepSeek-R1",
|
| 220 |
+
"huggingface/meta-llama/Llama-3.3-70B-Instruct",
|
| 221 |
+
"huggingface/Qwen/Qwen3-32B"
|
| 222 |
+
]
|
| 223 |
+
},
|
| 224 |
+
"OPENAI_MODELS": [
|
| 225 |
+
"gpt-5.4-pro",
|
| 226 |
+
"gpt-5.4",
|
| 227 |
+
"gpt-5.4-mini",
|
| 228 |
+
"gpt-5.4-nano",
|
| 229 |
+
"gpt-5.1",
|
| 230 |
+
"gpt-5",
|
| 231 |
+
"gpt-4.1",
|
| 232 |
+
"gpt-4.1-mini",
|
| 233 |
+
"gpt-4o",
|
| 234 |
+
"gpt-4o-mini",
|
| 235 |
+
"o3",
|
| 236 |
+
"o4-mini"
|
| 237 |
+
],
|
| 238 |
+
"ANTHROPIC_MODELS": [
|
| 239 |
+
"anthropic/claude-opus-4-7",
|
| 240 |
+
"anthropic/claude-opus-4-6",
|
| 241 |
+
"anthropic/claude-opus-4-5",
|
| 242 |
+
"anthropic/claude-sonnet-4-6",
|
| 243 |
+
"anthropic/claude-sonnet-4-5",
|
| 244 |
+
"anthropic/claude-haiku-4-5"
|
| 245 |
+
],
|
| 246 |
+
"GEMINI_MODELS": [
|
| 247 |
+
"google/gemini-3.1-pro-preview",
|
| 248 |
+
"google/gemini-3.1-flash-preview",
|
| 249 |
+
"google/gemini-3-flash-preview",
|
| 250 |
+
"google/gemini-2.5-pro",
|
| 251 |
+
"google/gemini-2.5-flash",
|
| 252 |
+
"google/gemini-2.0-flash"
|
| 253 |
+
],
|
| 254 |
+
"DEEPSEEK_MODELS": [
|
| 255 |
+
"deepseek/deepseek-v4-pro",
|
| 256 |
+
"deepseek/deepseek-v4-flash",
|
| 257 |
+
"deepseek/deepseek-v3.2",
|
| 258 |
+
"deepseek/deepseek-chat",
|
| 259 |
+
"deepseek/deepseek-reasoner",
|
| 260 |
+
"deepseek/deepseek-r1",
|
| 261 |
+
"deepseek/deepseek-r1-0528"
|
| 262 |
+
],
|
| 263 |
+
"OPENROUTER_MODELS": [
|
| 264 |
+
"openrouter/free",
|
| 265 |
+
"openrouter/auto",
|
| 266 |
+
"openrouter/anthropic/claude-sonnet-4-6",
|
| 267 |
+
"openrouter/anthropic/claude-opus-4-7",
|
| 268 |
+
"openrouter/anthropic/claude-haiku-4-5",
|
| 269 |
+
"openrouter/openai/gpt-5.4",
|
| 270 |
+
"openrouter/openai/gpt-4.1",
|
| 271 |
+
"openrouter/openai/gpt-4o",
|
| 272 |
+
"openrouter/openai/gpt-5.1",
|
| 273 |
+
"openrouter/google/gemini-3.1-pro-preview",
|
| 274 |
+
"openrouter/google/gemini-2.5-pro",
|
| 275 |
+
"openrouter/deepseek/deepseek-v3.2",
|
| 276 |
+
"openrouter/deepseek/deepseek-r1",
|
| 277 |
+
"openrouter/moonshotai/kimi-k2.5",
|
| 278 |
+
"openrouter/qwen/qwen3-32b"
|
| 279 |
+
],
|
| 280 |
+
"GROQ_MODELS": [
|
| 281 |
+
"groq/compound",
|
| 282 |
+
"groq/compound-mini",
|
| 283 |
+
"llama-3.1-8b-instant",
|
| 284 |
+
"llama-3.1-70b-versatile",
|
| 285 |
+
"llama-3.3-70b-versatile",
|
| 286 |
+
"openai/gpt-oss-20b",
|
| 287 |
+
"openai/gpt-oss-120b",
|
| 288 |
+
"meta-llama/llama-4-scout-17b-16e-instruct",
|
| 289 |
+
"qwen/qwen3-32b",
|
| 290 |
+
"mixtral-8x7b-32768"
|
| 291 |
+
],
|
| 292 |
+
"MISTRAL_MODELS": [
|
| 293 |
+
"mistral-large-latest",
|
| 294 |
+
"mistral-large-2",
|
| 295 |
+
"mistral-medium-3.5",
|
| 296 |
+
"mistral-small-latest",
|
| 297 |
+
"mistral-small-3.2",
|
| 298 |
+
"devstral-2",
|
| 299 |
+
"ocr-3-premier",
|
| 300 |
+
"voxtral-mini-transcribe-realtime",
|
| 301 |
+
"codestral-latest"
|
| 302 |
+
],
|
| 303 |
+
"XAI_MODELS": [
|
| 304 |
+
"grok-4.3",
|
| 305 |
+
"grok-4.1",
|
| 306 |
+
"grok-4",
|
| 307 |
+
"grok-3"
|
| 308 |
+
],
|
| 309 |
+
"COHERE_MODELS": [
|
| 310 |
+
"command-a",
|
| 311 |
+
"command-a-03-2025",
|
| 312 |
+
"command-a-translate-08-2025",
|
| 313 |
+
"command-a-reasoning-08-2025",
|
| 314 |
+
"command-a-vision-07-2025",
|
| 315 |
+
"command-r7b-12-2024",
|
| 316 |
+
"command-r-08-2024",
|
| 317 |
+
"command-r-plus-08-2024"
|
| 318 |
+
],
|
| 319 |
+
"TOGETHER_MODELS": [
|
| 320 |
+
"moonshotai/Kimi-K2.5",
|
| 321 |
+
"deepseek-ai/DeepSeek-R1",
|
| 322 |
+
"Qwen/Qwen3-235B-A22B-Instruct-2507-tput",
|
| 323 |
+
"zai-org/GLM-5.1",
|
| 324 |
+
"google/gemma-4-31B-it",
|
| 325 |
+
"MiniMaxAI/MiniMax-M2.7",
|
| 326 |
+
"meta-llama/Llama-3.3-70B-Instruct-Turbo",
|
| 327 |
+
"openai/gpt-oss-20b",
|
| 328 |
+
"openai/gpt-oss-120b",
|
| 329 |
+
"mistralai/Mistral-Small-3.2-24B-Instruct-2506",
|
| 330 |
+
"moonshotai/Kimi-K2.5-Instruct"
|
| 331 |
+
],
|
| 332 |
+
"CEREBRAS_MODELS": [
|
| 333 |
+
"cerebras/zai-glm-4.7",
|
| 334 |
+
"cerebras/deepseek-r1",
|
| 335 |
+
"cerebras/llama-4-scout-17b-16e-instruct",
|
| 336 |
+
"cerebras/qwen3-32b"
|
| 337 |
+
],
|
| 338 |
+
"NVIDIA_MODELS": [
|
| 339 |
+
"nvidia/nemotron-3-super-120b-a12b",
|
| 340 |
+
"nvidia/nemotron-4-340b-instruct",
|
| 341 |
+
"nvidia/llama-3.1-nemotron-70b-instruct"
|
| 342 |
+
],
|
| 343 |
+
"KILOCODE_MODELS": [
|
| 344 |
+
"kilocode/anthropic/claude-opus-4.6",
|
| 345 |
+
"kilocode/anthropic/claude-sonnet-4.6",
|
| 346 |
+
"kilocode/openai/gpt-5.4",
|
| 347 |
+
"kilocode/google/gemini-2.5-pro"
|
| 348 |
+
],
|
| 349 |
+
"OPENCODE_MODELS": [
|
| 350 |
+
"opencode/claude-opus-4-6",
|
| 351 |
+
"opencode/gpt-5.4",
|
| 352 |
+
"opencode-go/kimi-k2.5",
|
| 353 |
+
"opencode-go/qwen3-32b"
|
| 354 |
+
],
|
| 355 |
+
"ZAI_MODELS": [
|
| 356 |
+
"zai/glm-5",
|
| 357 |
+
"zai/glm-5-turbo",
|
| 358 |
+
"zai/glm-4.7",
|
| 359 |
+
"zai/glm-4.7-flash"
|
| 360 |
+
],
|
| 361 |
+
"MOONSHOT_MODELS": [
|
| 362 |
+
"moonshot/kimi-k2.5",
|
| 363 |
+
"moonshot/kimi-k2.5-thinking",
|
| 364 |
+
"moonshot/kimi-k2.5-coder"
|
| 365 |
+
],
|
| 366 |
+
"MINIMAX_MODELS": [
|
| 367 |
+
"minimax/minimax-m2.7",
|
| 368 |
+
"minimax/minimax-m1.5",
|
| 369 |
+
"minimax/abab6.5s-chat"
|
| 370 |
+
],
|
| 371 |
+
"XIAOMI_MODELS": [
|
| 372 |
+
"xiaomi/mimo-v1",
|
| 373 |
+
"xiaomi/mimo-v2",
|
| 374 |
+
"xiaomi/mi-mo"
|
| 375 |
+
],
|
| 376 |
+
"VOLCANO_ENGINE_MODELS": [
|
| 377 |
+
"volcengine/doubao-seed-1.6",
|
| 378 |
+
"volcengine/doubao-1.5-pro",
|
| 379 |
+
"volcengine/doubao-1.5-lite"
|
| 380 |
+
],
|
| 381 |
+
"BYTEPLUS_MODELS": [
|
| 382 |
+
"byteplus/seed-1.6",
|
| 383 |
+
"byteplus/deepseek-v3.2",
|
| 384 |
+
"byteplus/doubao-seed-1.6"
|
| 385 |
+
],
|
| 386 |
+
"QIANFAN_MODELS": [
|
| 387 |
+
"qianfan/ernie-4.5",
|
| 388 |
+
"qianfan/ernie-4.5-8k",
|
| 389 |
+
"qianfan/deepseek-v3.2",
|
| 390 |
+
"qianfan/ernie-x1"
|
| 391 |
+
],
|
| 392 |
+
"MODELSTUDIO_MODELS": [
|
| 393 |
+
"modelstudio/qwen3-max",
|
| 394 |
+
"modelstudio/qwen3-coder",
|
| 395 |
+
"modelstudio/qwen3-32b"
|
| 396 |
+
],
|
| 397 |
+
"KIMI_MODELS": [
|
| 398 |
+
"moonshot/kimi-k2.5",
|
| 399 |
+
"moonshot/kimi-k2.5-thinking",
|
| 400 |
+
"moonshot/kimi-k2.5-coder"
|
| 401 |
+
],
|
| 402 |
+
"HUGGINGFACE_MODELS": [
|
| 403 |
+
"huggingface/deepseek-ai/DeepSeek-R1",
|
| 404 |
+
"huggingface/meta-llama/Llama-3.3-70B-Instruct",
|
| 405 |
+
"huggingface/Qwen/Qwen3-32B",
|
| 406 |
+
"huggingface/mistralai/Mistral-Small-3.2-24B-Instruct-2506"
|
| 407 |
+
],
|
| 408 |
+
"GITHUB_COPILOT_MODELS": [
|
| 409 |
+
"github-copilot/gpt-5",
|
| 410 |
+
"github-copilot/gpt-4.1",
|
| 411 |
+
"github-copilot/gpt-4o"
|
| 412 |
+
],
|
| 413 |
+
"AI_GATEWAY_MODELS": [],
|
| 414 |
+
"VENICE_MODELS": [],
|
| 415 |
+
"SYNTHETIC_MODELS": []
|
| 416 |
+
};
|
| 417 |
+
|
| 418 |
+
const FIELDS = [
|
| 419 |
+
{
|
| 420 |
+
"g": "Core",
|
| 421 |
+
"icon": "⚡",
|
| 422 |
+
"k": "LLM_MODEL",
|
| 423 |
+
"lbl": "Default model ID",
|
| 424 |
+
"type": "model",
|
| 425 |
+
"options_key": "LLM_MODEL",
|
| 426 |
+
"ph": "choose a provider model",
|
| 427 |
+
"common": 1
|
| 428 |
+
},
|
| 429 |
+
{
|
| 430 |
+
"g": "Core",
|
| 431 |
+
"icon": "⚡",
|
| 432 |
+
"k": "LLM_API_KEY",
|
| 433 |
+
"lbl": "Primary provider API key",
|
| 434 |
+
"type": "password",
|
| 435 |
+
"secret": 1,
|
| 436 |
+
"ph": "sk-...",
|
| 437 |
+
"common": 1
|
| 438 |
+
},
|
| 439 |
+
{
|
| 440 |
+
"g": "Core",
|
| 441 |
+
"icon": "⚡",
|
| 442 |
+
"k": "GATEWAY_TOKEN",
|
| 443 |
+
"lbl": "Control UI gateway token",
|
| 444 |
+
"type": "password",
|
| 445 |
+
"secret": 1,
|
| 446 |
+
"common": 1
|
| 447 |
+
},
|
| 448 |
+
{
|
| 449 |
+
"g": "Core",
|
| 450 |
+
"icon": "⚡",
|
| 451 |
+
"k": "OPENCLAW_PASSWORD",
|
| 452 |
+
"lbl": "Optional password auth",
|
| 453 |
+
"type": "password",
|
| 454 |
+
"secret": 1
|
| 455 |
+
},
|
| 456 |
+
{
|
| 457 |
+
"g": "Core",
|
| 458 |
+
"icon": "⚡",
|
| 459 |
+
"k": "OPENCLAW_VERSION",
|
| 460 |
+
"lbl": "Pin OpenClaw version",
|
| 461 |
+
"type": "text",
|
| 462 |
+
"ph": "latest"
|
| 463 |
+
},
|
| 464 |
+
{
|
| 465 |
+
"g": "Core",
|
| 466 |
+
"icon": "⚡",
|
| 467 |
+
"k": "LLM_API_KEY_FALLBACK_ENABLED",
|
| 468 |
+
"lbl": "Allow global LLM_API_KEY fallback",
|
| 469 |
+
"type": "toggle",
|
| 470 |
+
"ph": "true"
|
| 471 |
+
},
|
| 472 |
+
{
|
| 473 |
+
"g": "Core",
|
| 474 |
+
"icon": "⚡",
|
| 475 |
+
"k": "DEV_MODE",
|
| 476 |
+
"lbl": "Enable dev mode",
|
| 477 |
+
"type": "toggle",
|
| 478 |
+
"ph": "false",
|
| 479 |
+
"common": 1
|
| 480 |
+
},
|
| 481 |
+
{
|
| 482 |
+
"g": "Core",
|
| 483 |
+
"icon": "⚡",
|
| 484 |
+
"k": "HUGGINGCLAW_JUPYTER_ENABLED",
|
| 485 |
+
"lbl": "Enable Jupyter terminal",
|
| 486 |
+
"type": "toggle",
|
| 487 |
+
"ph": "false",
|
| 488 |
+
"common": 1
|
| 489 |
+
},
|
| 490 |
+
{
|
| 491 |
+
"g": "Core",
|
| 492 |
+
"icon": "⚡",
|
| 493 |
+
"k": "DEVDATA",
|
| 494 |
+
"lbl": "DevData switch",
|
| 495 |
+
"type": "toggle",
|
| 496 |
+
"ph": "on",
|
| 497 |
+
"common": 1
|
| 498 |
+
},
|
| 499 |
+
{
|
| 500 |
+
"g": "Core",
|
| 501 |
+
"icon": "⚡",
|
| 502 |
+
"k": "DEVDATA_DATASET_NAME",
|
| 503 |
+
"lbl": "DevData dataset name",
|
| 504 |
+
"type": "text",
|
| 505 |
+
"ph": "huggingclaw-devdata",
|
| 506 |
+
"common": 1
|
| 507 |
+
},
|
| 508 |
+
{
|
| 509 |
+
"g": "Core",
|
| 510 |
+
"icon": "⚡",
|
| 511 |
+
"k": "DEVDATA_SYNC_INTERVAL",
|
| 512 |
+
"lbl": "DevData sync interval (seconds)",
|
| 513 |
+
"type": "number",
|
| 514 |
+
"ph": "180"
|
| 515 |
+
},
|
| 516 |
+
{
|
| 517 |
+
"g": "Core",
|
| 518 |
+
"icon": "⚡",
|
| 519 |
+
"k": "WHATSAPP_ENABLED",
|
| 520 |
+
"lbl": "Enable WhatsApp pairing",
|
| 521 |
+
"type": "toggle",
|
| 522 |
+
"ph": "false",
|
| 523 |
+
"common": 1
|
| 524 |
+
},
|
| 525 |
+
{
|
| 526 |
+
"g": "Core",
|
| 527 |
+
"icon": "⚡",
|
| 528 |
+
"k": "HUGGINGCLAW_CAPTURE_DISABLE",
|
| 529 |
+
"lbl": "Disable capture wrapper",
|
| 530 |
+
"type": "toggle",
|
| 531 |
+
"ph": "false"
|
| 532 |
+
},
|
| 533 |
+
{
|
| 534 |
+
"g": "Core",
|
| 535 |
+
"icon": "⚡",
|
| 536 |
+
"k": "HUGGINGCLAW_STARTUP_STRICT",
|
| 537 |
+
"lbl": "Stop on startup failure",
|
| 538 |
+
"type": "toggle",
|
| 539 |
+
"ph": "false"
|
| 540 |
+
},
|
| 541 |
+
{
|
| 542 |
+
"g": "Core",
|
| 543 |
+
"icon": "⚡",
|
| 544 |
+
"k": "HUGGINGCLAW_RUN",
|
| 545 |
+
"lbl": "Startup command (one-liner)",
|
| 546 |
+
"type": "textarea"
|
| 547 |
+
},
|
| 548 |
+
{
|
| 549 |
+
"g": "Core",
|
| 550 |
+
"icon": "⚡",
|
| 551 |
+
"k": "HUGGINGCLAW_STARTUP_COMMANDS",
|
| 552 |
+
"lbl": "Multiline startup commands",
|
| 553 |
+
"type": "textarea"
|
| 554 |
+
},
|
| 555 |
+
{
|
| 556 |
+
"g": "Core",
|
| 557 |
+
"icon": "⚡",
|
| 558 |
+
"k": "HUGGINGCLAW_STARTUP_SCRIPT",
|
| 559 |
+
"lbl": "Startup shell script",
|
| 560 |
+
"type": "textarea"
|
| 561 |
+
},
|
| 562 |
+
{
|
| 563 |
+
"g": "Core",
|
| 564 |
+
"icon": "⚡",
|
| 565 |
+
"k": "HUGGINGCLAW_STARTUP_SCRIPT_B64",
|
| 566 |
+
"lbl": "Startup script (base64)",
|
| 567 |
+
"type": "textarea"
|
| 568 |
+
},
|
| 569 |
+
{
|
| 570 |
+
"g": "Core",
|
| 571 |
+
"icon": "⚡",
|
| 572 |
+
"k": "HUGGINGCLAW_APT_PACKAGES",
|
| 573 |
+
"lbl": "APT packages to install",
|
| 574 |
+
"type": "textarea"
|
| 575 |
+
},
|
| 576 |
+
{
|
| 577 |
+
"g": "Core",
|
| 578 |
+
"icon": "⚡",
|
| 579 |
+
"k": "HUGGINGCLAW_PIP_PACKAGES",
|
| 580 |
+
"lbl": "Pip packages to install",
|
| 581 |
+
"type": "textarea"
|
| 582 |
+
},
|
| 583 |
+
{
|
| 584 |
+
"g": "Core",
|
| 585 |
+
"icon": "⚡",
|
| 586 |
+
"k": "HUGGINGCLAW_NPM_PACKAGES",
|
| 587 |
+
"lbl": "NPM packages to install",
|
| 588 |
+
"type": "textarea"
|
| 589 |
+
},
|
| 590 |
+
{
|
| 591 |
+
"g": "Core",
|
| 592 |
+
"icon": "⚡",
|
| 593 |
+
"k": "HUGGINGCLAW_OPENCLAW_PLUGINS",
|
| 594 |
+
"lbl": "OpenClaw plugins to load",
|
| 595 |
+
"type": "textarea"
|
| 596 |
+
},
|
| 597 |
+
{
|
| 598 |
+
"g": "Core",
|
| 599 |
+
"icon": "⚡",
|
| 600 |
+
"k": "ALLOWED_ORIGINS",
|
| 601 |
+
"lbl": "Allowed CORS origins",
|
| 602 |
+
"type": "textarea"
|
| 603 |
+
},
|
| 604 |
+
{
|
| 605 |
+
"g": "Core",
|
| 606 |
+
"icon": "⚡",
|
| 607 |
+
"k": "TRUSTED_PROXIES",
|
| 608 |
+
"lbl": "Trusted proxy CIDRs",
|
| 609 |
+
"type": "textarea"
|
| 610 |
+
},
|
| 611 |
+
{
|
| 612 |
+
"g": "Core",
|
| 613 |
+
"icon": "⚡",
|
| 614 |
+
"k": "WEBHOOK_URL",
|
| 615 |
+
"lbl": "Webhook URL",
|
| 616 |
+
"type": "text",
|
| 617 |
+
"ph": "https://..."
|
| 618 |
+
},
|
| 619 |
+
{
|
| 620 |
+
"g": "Core",
|
| 621 |
+
"icon": "⚡",
|
| 622 |
+
"k": "WS_MIN_PROTOCOL",
|
| 623 |
+
"lbl": "Min WebSocket protocol",
|
| 624 |
+
"type": "number",
|
| 625 |
+
"ph": "1"
|
| 626 |
+
},
|
| 627 |
+
{
|
| 628 |
+
"g": "Core",
|
| 629 |
+
"icon": "⚡",
|
| 630 |
+
"k": "WS_MAX_PROTOCOL",
|
| 631 |
+
"lbl": "Max WebSocket protocol",
|
| 632 |
+
"type": "number",
|
| 633 |
+
"ph": "5"
|
| 634 |
+
},
|
| 635 |
+
{
|
| 636 |
+
"g": "Core",
|
| 637 |
+
"icon": "⚡",
|
| 638 |
+
"k": "GATEWAY_MAX_RESTARTS",
|
| 639 |
+
"lbl": "Gateway max restarts",
|
| 640 |
+
"type": "number",
|
| 641 |
+
"ph": "10"
|
| 642 |
+
},
|
| 643 |
+
{
|
| 644 |
+
"g": "Core",
|
| 645 |
+
"icon": "⚡",
|
| 646 |
+
"k": "GATEWAY_READY_TIMEOUT",
|
| 647 |
+
"lbl": "Gateway ready timeout",
|
| 648 |
+
"type": "number",
|
| 649 |
+
"ph": "90"
|
| 650 |
+
},
|
| 651 |
+
{
|
| 652 |
+
"g": "Core",
|
| 653 |
+
"icon": "⚡",
|
| 654 |
+
"k": "GATEWAY_RESTART_DELAY",
|
| 655 |
+
"lbl": "Gateway restart delay",
|
| 656 |
+
"type": "number",
|
| 657 |
+
"ph": "5"
|
| 658 |
+
},
|
| 659 |
+
{
|
| 660 |
+
"g": "Core",
|
| 661 |
+
"icon": "⚡",
|
| 662 |
+
"k": "GATEWAY_VERBOSE",
|
| 663 |
+
"lbl": "Verbose gateway logs",
|
| 664 |
+
"type": "toggle",
|
| 665 |
+
"ph": "false"
|
| 666 |
+
},
|
| 667 |
+
{
|
| 668 |
+
"g": "Core",
|
| 669 |
+
"icon": "⚡",
|
| 670 |
+
"k": "OPENCLAW_CONSOLE_LOG_LEVEL",
|
| 671 |
+
"lbl": "Console log level",
|
| 672 |
+
"type": "select",
|
| 673 |
+
"options": [
|
| 674 |
+
"debug",
|
| 675 |
+
"info",
|
| 676 |
+
"warn",
|
| 677 |
+
"error"
|
| 678 |
+
],
|
| 679 |
+
"ph": "info"
|
| 680 |
+
},
|
| 681 |
+
{
|
| 682 |
+
"g": "Core",
|
| 683 |
+
"icon": "⚡",
|
| 684 |
+
"k": "OPENCLAW_FILE_LOG_LEVEL",
|
| 685 |
+
"lbl": "File log level",
|
| 686 |
+
"type": "select",
|
| 687 |
+
"options": [
|
| 688 |
+
"debug",
|
| 689 |
+
"info",
|
| 690 |
+
"warn",
|
| 691 |
+
"error"
|
| 692 |
+
],
|
| 693 |
+
"ph": "info"
|
| 694 |
+
},
|
| 695 |
+
{
|
| 696 |
+
"g": "Core",
|
| 697 |
+
"icon": "⚡",
|
| 698 |
+
"k": "OPENCLAW_CONSOLE_LOG_STYLE",
|
| 699 |
+
"lbl": "Console log style",
|
| 700 |
+
"type": "select",
|
| 701 |
+
"options": [
|
| 702 |
+
"pretty",
|
| 703 |
+
"json",
|
| 704 |
+
"compact"
|
| 705 |
+
],
|
| 706 |
+
"ph": "pretty"
|
| 707 |
+
},
|
| 708 |
+
{
|
| 709 |
+
"g": "Core",
|
| 710 |
+
"icon": "⚡",
|
| 711 |
+
"k": "BROWSER_PLUGIN_MODE",
|
| 712 |
+
"lbl": "Browser plugin mode",
|
| 713 |
+
"type": "select",
|
| 714 |
+
"options": [
|
| 715 |
+
"auto",
|
| 716 |
+
"enabled",
|
| 717 |
+
"disabled"
|
| 718 |
+
],
|
| 719 |
+
"ph": "auto"
|
| 720 |
+
},
|
| 721 |
+
{
|
| 722 |
+
"g": "Core",
|
| 723 |
+
"icon": "⚡",
|
| 724 |
+
"k": "ACP_PLUGIN_MODE",
|
| 725 |
+
"lbl": "ACP plugin mode",
|
| 726 |
+
"type": "select",
|
| 727 |
+
"options": [
|
| 728 |
+
"auto",
|
| 729 |
+
"enabled",
|
| 730 |
+
"disabled"
|
| 731 |
+
],
|
| 732 |
+
"ph": "auto"
|
| 733 |
+
},
|
| 734 |
+
{
|
| 735 |
+
"g": "Core",
|
| 736 |
+
"icon": "⚡",
|
| 737 |
+
"k": "CLOUDFLARE_PROXY_DEBUG",
|
| 738 |
+
"lbl": "Cloudflare proxy debug",
|
| 739 |
+
"type": "toggle",
|
| 740 |
+
"ph": "false"
|
| 741 |
+
},
|
| 742 |
+
{
|
| 743 |
+
"g": "Core",
|
| 744 |
+
"icon": "⚡",
|
| 745 |
+
"k": "CLOUDFLARE_KEEPALIVE_ENABLED",
|
| 746 |
+
"lbl": "Enable keep-awake worker",
|
| 747 |
+
"type": "toggle",
|
| 748 |
+
"ph": "true"
|
| 749 |
+
},
|
| 750 |
+
{
|
| 751 |
+
"g": "Core",
|
| 752 |
+
"icon": "⚡",
|
| 753 |
+
"k": "CLOUDFLARE_PROXY_URL",
|
| 754 |
+
"lbl": "Proxy worker URL",
|
| 755 |
+
"type": "text",
|
| 756 |
+
"ph": "https://your-proxy.workers.dev",
|
| 757 |
+
"common": 1
|
| 758 |
+
},
|
| 759 |
+
{
|
| 760 |
+
"g": "Core",
|
| 761 |
+
"icon": "⚡",
|
| 762 |
+
"k": "CLOUDFLARE_PROXY_SECRET",
|
| 763 |
+
"lbl": "Proxy shared secret",
|
| 764 |
+
"type": "password",
|
| 765 |
+
"secret": 1
|
| 766 |
+
},
|
| 767 |
+
{
|
| 768 |
+
"g": "Core",
|
| 769 |
+
"icon": "⚡",
|
| 770 |
+
"k": "CLOUDFLARE_PROXY_DOMAINS",
|
| 771 |
+
"lbl": "Extra domains to proxy",
|
| 772 |
+
"type": "textarea",
|
| 773 |
+
"ph": "api.sendgrid.com,slack.com"
|
| 774 |
+
},
|
| 775 |
+
{
|
| 776 |
+
"g": "Core",
|
| 777 |
+
"icon": "⚡",
|
| 778 |
+
"k": "CLOUDFLARE_WORKERS_TOKEN",
|
| 779 |
+
"lbl": "Workers API token",
|
| 780 |
+
"type": "password",
|
| 781 |
+
"secret": 1,
|
| 782 |
+
"common": 1
|
| 783 |
+
},
|
| 784 |
+
{
|
| 785 |
+
"g": "Core",
|
| 786 |
+
"icon": "⚡",
|
| 787 |
+
"k": "HF_USERNAME",
|
| 788 |
+
"lbl": "Hugging Face username",
|
| 789 |
+
"type": "text",
|
| 790 |
+
"common": 1
|
| 791 |
+
},
|
| 792 |
+
{
|
| 793 |
+
"g": "Core",
|
| 794 |
+
"icon": "⚡",
|
| 795 |
+
"k": "HF_TOKEN",
|
| 796 |
+
"lbl": "HF write token",
|
| 797 |
+
"type": "password",
|
| 798 |
+
"secret": 1,
|
| 799 |
+
"common": 1
|
| 800 |
+
},
|
| 801 |
+
{
|
| 802 |
+
"g": "Core",
|
| 803 |
+
"icon": "⚡",
|
| 804 |
+
"k": "BACKUP_DATASET_NAME",
|
| 805 |
+
"lbl": "Backup dataset name",
|
| 806 |
+
"type": "text",
|
| 807 |
+
"ph": "huggingclaw-backup",
|
| 808 |
+
"common": 1
|
| 809 |
+
},
|
| 810 |
+
{
|
| 811 |
+
"g": "Core",
|
| 812 |
+
"icon": "⚡",
|
| 813 |
+
"k": "SYNC_INTERVAL",
|
| 814 |
+
"lbl": "Sync interval (seconds)",
|
| 815 |
+
"type": "number",
|
| 816 |
+
"ph": "180",
|
| 817 |
+
"common": 1
|
| 818 |
+
},
|
| 819 |
+
{
|
| 820 |
+
"g": "Core",
|
| 821 |
+
"icon": "⚡",
|
| 822 |
+
"k": "JUPYTER_TOKEN",
|
| 823 |
+
"lbl": "Jupyter access token",
|
| 824 |
+
"type": "password",
|
| 825 |
+
"secret": 1,
|
| 826 |
+
"ph": "huggingface",
|
| 827 |
+
"common": 1
|
| 828 |
+
},
|
| 829 |
+
{
|
| 830 |
+
"g": "Core",
|
| 831 |
+
"icon": "⚡",
|
| 832 |
+
"k": "KEEP_ALIVE_INTERVAL",
|
| 833 |
+
"lbl": "Keep-alive ping interval (seconds)",
|
| 834 |
+
"type": "number",
|
| 835 |
+
"ph": "300",
|
| 836 |
+
"common": 1
|
| 837 |
+
},
|
| 838 |
+
{
|
| 839 |
+
"g": "Core",
|
| 840 |
+
"icon": "⚡",
|
| 841 |
+
"k": "OPENCLAW_DISABLE_BONJOUR",
|
| 842 |
+
"lbl": "Disable Bonjour/mDNS discovery",
|
| 843 |
+
"type": "toggle",
|
| 844 |
+
"ph": "false"
|
| 845 |
+
},
|
| 846 |
+
{
|
| 847 |
+
"g": "Core",
|
| 848 |
+
"icon": "⚡",
|
| 849 |
+
"k": "OPENCLAW_RUNTIME_VERSION",
|
| 850 |
+
"lbl": "Pin runtime version",
|
| 851 |
+
"type": "text",
|
| 852 |
+
"ph": "latest"
|
| 853 |
+
},
|
| 854 |
+
{
|
| 855 |
+
"g": "Core",
|
| 856 |
+
"icon": "⚡",
|
| 857 |
+
"k": "OPENCLAW_DISPLAY_VERSION",
|
| 858 |
+
"lbl": "Display version label",
|
| 859 |
+
"type": "text",
|
| 860 |
+
"ph": ""
|
| 861 |
+
},
|
| 862 |
+
{
|
| 863 |
+
"g": "Integrations",
|
| 864 |
+
"icon": "🔌",
|
| 865 |
+
"k": "CLOUDFLARE_ACCOUNT_ID",
|
| 866 |
+
"lbl": "Cloudflare account ID",
|
| 867 |
+
"type": "text",
|
| 868 |
+
"ph": "account-id"
|
| 869 |
+
},
|
| 870 |
+
{
|
| 871 |
+
"g": "Integrations",
|
| 872 |
+
"icon": "🔌",
|
| 873 |
+
"k": "CLOUDFLARE_WORKER_NAME",
|
| 874 |
+
"lbl": "Outbound proxy worker name",
|
| 875 |
+
"type": "text",
|
| 876 |
+
"ph": "huggingclaw-proxy"
|
| 877 |
+
},
|
| 878 |
+
{
|
| 879 |
+
"g": "Integrations",
|
| 880 |
+
"icon": "🔌",
|
| 881 |
+
"k": "CLOUDFLARE_KEEPALIVE_URL",
|
| 882 |
+
"lbl": "Keepalive worker URL",
|
| 883 |
+
"type": "text",
|
| 884 |
+
"ph": "https://your-worker.workers.dev"
|
| 885 |
+
},
|
| 886 |
+
{
|
| 887 |
+
"g": "Integrations",
|
| 888 |
+
"icon": "🔌",
|
| 889 |
+
"k": "CLOUDFLARE_KEEPALIVE_WORKER_NAME",
|
| 890 |
+
"lbl": "Keepalive worker name",
|
| 891 |
+
"type": "text",
|
| 892 |
+
"ph": "huggingclaw-keepalive"
|
| 893 |
+
},
|
| 894 |
+
{
|
| 895 |
+
"g": "Integrations",
|
| 896 |
+
"icon": "🔌",
|
| 897 |
+
"k": "CLOUDFLARE_KEEPALIVE_CRON",
|
| 898 |
+
"lbl": "Keepalive cron schedule",
|
| 899 |
+
"type": "text",
|
| 900 |
+
"ph": "*/5 * * * *"
|
| 901 |
+
},
|
| 902 |
+
{
|
| 903 |
+
"g": "Integrations",
|
| 904 |
+
"icon": "🔌",
|
| 905 |
+
"k": "TELEGRAM_API_ROOT",
|
| 906 |
+
"lbl": "Telegram API root override",
|
| 907 |
+
"type": "text",
|
| 908 |
+
"ph": "https://api.telegram.org"
|
| 909 |
+
},
|
| 910 |
+
{
|
| 911 |
+
"g": "Runtime",
|
| 912 |
+
"icon": "⚙️",
|
| 913 |
+
"k": "OPENCLAW_CONFIG_WATCH_INTERVAL",
|
| 914 |
+
"lbl": "Config watch interval (seconds)",
|
| 915 |
+
"type": "number",
|
| 916 |
+
"ph": "1"
|
| 917 |
+
},
|
| 918 |
+
{
|
| 919 |
+
"g": "Runtime",
|
| 920 |
+
"icon": "⚙️",
|
| 921 |
+
"k": "OPENCLAW_CONFIG_SETTLE_SECONDS",
|
| 922 |
+
"lbl": "Config settle window (seconds)",
|
| 923 |
+
"type": "number",
|
| 924 |
+
"ph": "3"
|
| 925 |
+
},
|
| 926 |
+
{
|
| 927 |
+
"g": "Runtime",
|
| 928 |
+
"icon": "⚙️",
|
| 929 |
+
"k": "JUPYTER_ROOT_DIR",
|
| 930 |
+
"lbl": "Jupyter root directory",
|
| 931 |
+
"type": "text",
|
| 932 |
+
"ph": "/home/node"
|
| 933 |
+
},
|
| 934 |
+
{
|
| 935 |
+
"g": "Backup",
|
| 936 |
+
"icon": "💾",
|
| 937 |
+
"k": "WORKSPACE_GIT_USER",
|
| 938 |
+
"lbl": "Workspace git author email",
|
| 939 |
+
"type": "text",
|
| 940 |
+
"ph": "openclaw@example.com"
|
| 941 |
+
},
|
| 942 |
+
{
|
| 943 |
+
"g": "Backup",
|
| 944 |
+
"icon": "💾",
|
| 945 |
+
"k": "WORKSPACE_GIT_NAME",
|
| 946 |
+
"lbl": "Workspace git author name",
|
| 947 |
+
"type": "text",
|
| 948 |
+
"ph": "OpenClaw Bot"
|
| 949 |
+
},
|
| 950 |
+
{
|
| 951 |
+
"g": "Provider Keys",
|
| 952 |
+
"icon": "🔑",
|
| 953 |
+
"k": "ANTHROPIC_API_KEY",
|
| 954 |
+
"lbl": "Anthropic (Claude)",
|
| 955 |
+
"type": "password",
|
| 956 |
+
"secret": 1,
|
| 957 |
+
"common": 0
|
| 958 |
+
},
|
| 959 |
+
{
|
| 960 |
+
"g": "Provider Keys",
|
| 961 |
+
"icon": "🔑",
|
| 962 |
+
"k": "OPENAI_API_KEY",
|
| 963 |
+
"lbl": "OpenAI (GPT)",
|
| 964 |
+
"type": "password",
|
| 965 |
+
"secret": 1,
|
| 966 |
+
"common": 0
|
| 967 |
+
},
|
| 968 |
+
{
|
| 969 |
+
"g": "Provider Keys",
|
| 970 |
+
"icon": "🔑",
|
| 971 |
+
"k": "GOOGLE_API_KEY",
|
| 972 |
+
"lbl": "Google AI Studio",
|
| 973 |
+
"type": "password",
|
| 974 |
+
"secret": 1,
|
| 975 |
+
"common": 0
|
| 976 |
+
},
|
| 977 |
+
{
|
| 978 |
+
"g": "Provider Keys",
|
| 979 |
+
"icon": "🔑",
|
| 980 |
+
"k": "GEMINI_API_KEY",
|
| 981 |
+
"lbl": "Google Gemini",
|
| 982 |
+
"type": "password",
|
| 983 |
+
"secret": 1,
|
| 984 |
+
"common": 0
|
| 985 |
+
},
|
| 986 |
+
{
|
| 987 |
+
"g": "Provider Keys",
|
| 988 |
+
"icon": "🔑",
|
| 989 |
+
"k": "DEEPSEEK_API_KEY",
|
| 990 |
+
"lbl": "DeepSeek",
|
| 991 |
+
"type": "password",
|
| 992 |
+
"secret": 1,
|
| 993 |
+
"common": 0
|
| 994 |
+
},
|
| 995 |
+
{
|
| 996 |
+
"g": "Provider Keys",
|
| 997 |
+
"icon": "🔑",
|
| 998 |
+
"k": "OPENROUTER_API_KEY",
|
| 999 |
+
"lbl": "OpenRouter",
|
| 1000 |
+
"type": "password",
|
| 1001 |
+
"secret": 1,
|
| 1002 |
+
"common": 1
|
| 1003 |
+
},
|
| 1004 |
+
{
|
| 1005 |
+
"g": "Provider Keys",
|
| 1006 |
+
"icon": "🔑",
|
| 1007 |
+
"k": "OPENCODE_API_KEY",
|
| 1008 |
+
"lbl": "OpenCode",
|
| 1009 |
+
"type": "password",
|
| 1010 |
+
"secret": 1,
|
| 1011 |
+
"common": 0
|
| 1012 |
+
},
|
| 1013 |
+
{
|
| 1014 |
+
"g": "Provider Keys",
|
| 1015 |
+
"icon": "🔑",
|
| 1016 |
+
"k": "KILOCODE_API_KEY",
|
| 1017 |
+
"lbl": "KiloCode",
|
| 1018 |
+
"type": "password",
|
| 1019 |
+
"secret": 1,
|
| 1020 |
+
"common": 0
|
| 1021 |
+
},
|
| 1022 |
+
{
|
| 1023 |
+
"g": "Provider Keys",
|
| 1024 |
+
"icon": "🔑",
|
| 1025 |
+
"k": "ZAI_API_KEY",
|
| 1026 |
+
"lbl": "Z.ai / GLM",
|
| 1027 |
+
"type": "password",
|
| 1028 |
+
"secret": 1,
|
| 1029 |
+
"common": 0
|
| 1030 |
+
},
|
| 1031 |
+
{
|
| 1032 |
+
"g": "Provider Keys",
|
| 1033 |
+
"icon": "🔑",
|
| 1034 |
+
"k": "MOONSHOT_API_KEY",
|
| 1035 |
+
"lbl": "Moonshot / Kimi",
|
| 1036 |
+
"type": "password",
|
| 1037 |
+
"secret": 1,
|
| 1038 |
+
"common": 0
|
| 1039 |
+
},
|
| 1040 |
+
{
|
| 1041 |
+
"g": "Provider Keys",
|
| 1042 |
+
"icon": "🔑",
|
| 1043 |
+
"k": "MINIMAX_API_KEY",
|
| 1044 |
+
"lbl": "MiniMax",
|
| 1045 |
+
"type": "password",
|
| 1046 |
+
"secret": 1,
|
| 1047 |
+
"common": 0
|
| 1048 |
+
},
|
| 1049 |
+
{
|
| 1050 |
+
"g": "Provider Keys",
|
| 1051 |
+
"icon": "🔑",
|
| 1052 |
+
"k": "XIAOMI_API_KEY",
|
| 1053 |
+
"lbl": "Xiaomi / MiMo",
|
| 1054 |
+
"type": "password",
|
| 1055 |
+
"secret": 1,
|
| 1056 |
+
"common": 0
|
| 1057 |
+
},
|
| 1058 |
+
{
|
| 1059 |
+
"g": "Provider Keys",
|
| 1060 |
+
"icon": "🔑",
|
| 1061 |
+
"k": "VOLCANO_ENGINE_API_KEY",
|
| 1062 |
+
"lbl": "Volcengine / Doubao",
|
| 1063 |
+
"type": "password",
|
| 1064 |
+
"secret": 1,
|
| 1065 |
+
"common": 0
|
| 1066 |
+
},
|
| 1067 |
+
{
|
| 1068 |
+
"g": "Provider Keys",
|
| 1069 |
+
"icon": "🔑",
|
| 1070 |
+
"k": "BYTEPLUS_API_KEY",
|
| 1071 |
+
"lbl": "BytePlus",
|
| 1072 |
+
"type": "password",
|
| 1073 |
+
"secret": 1,
|
| 1074 |
+
"common": 0
|
| 1075 |
+
},
|
| 1076 |
+
{
|
| 1077 |
+
"g": "Provider Keys",
|
| 1078 |
+
"icon": "🔑",
|
| 1079 |
+
"k": "MISTRAL_API_KEY",
|
| 1080 |
+
"lbl": "Mistral",
|
| 1081 |
+
"type": "password",
|
| 1082 |
+
"secret": 1,
|
| 1083 |
+
"common": 0
|
| 1084 |
+
},
|
| 1085 |
+
{
|
| 1086 |
+
"g": "Provider Keys",
|
| 1087 |
+
"icon": "🔑",
|
| 1088 |
+
"k": "XAI_API_KEY",
|
| 1089 |
+
"lbl": "xAI (Grok)",
|
| 1090 |
+
"type": "password",
|
| 1091 |
+
"secret": 1,
|
| 1092 |
+
"common": 0
|
| 1093 |
+
},
|
| 1094 |
+
{
|
| 1095 |
+
"g": "Provider Keys",
|
| 1096 |
+
"icon": "🔑",
|
| 1097 |
+
"k": "NVIDIA_API_KEY",
|
| 1098 |
+
"lbl": "NVIDIA",
|
| 1099 |
+
"type": "password",
|
| 1100 |
+
"secret": 1,
|
| 1101 |
+
"common": 0
|
| 1102 |
+
},
|
| 1103 |
+
{
|
| 1104 |
+
"g": "Provider Keys",
|
| 1105 |
+
"icon": "🔑",
|
| 1106 |
+
"k": "GROQ_API_KEY",
|
| 1107 |
+
"lbl": "Groq",
|
| 1108 |
+
"type": "password",
|
| 1109 |
+
"secret": 1,
|
| 1110 |
+
"common": 0
|
| 1111 |
+
},
|
| 1112 |
+
{
|
| 1113 |
+
"g": "Provider Keys",
|
| 1114 |
+
"icon": "🔑",
|
| 1115 |
+
"k": "COHERE_API_KEY",
|
| 1116 |
+
"lbl": "Cohere",
|
| 1117 |
+
"type": "password",
|
| 1118 |
+
"secret": 1,
|
| 1119 |
+
"common": 0
|
| 1120 |
+
},
|
| 1121 |
+
{
|
| 1122 |
+
"g": "Provider Keys",
|
| 1123 |
+
"icon": "🔑",
|
| 1124 |
+
"k": "TOGETHER_API_KEY",
|
| 1125 |
+
"lbl": "Together AI",
|
| 1126 |
+
"type": "password",
|
| 1127 |
+
"secret": 1,
|
| 1128 |
+
"common": 0
|
| 1129 |
+
},
|
| 1130 |
+
{
|
| 1131 |
+
"g": "Provider Keys",
|
| 1132 |
+
"icon": "🔑",
|
| 1133 |
+
"k": "CEREBRAS_API_KEY",
|
| 1134 |
+
"lbl": "Cerebras",
|
| 1135 |
+
"type": "password",
|
| 1136 |
+
"secret": 1,
|
| 1137 |
+
"common": 0
|
| 1138 |
+
},
|
| 1139 |
+
{
|
| 1140 |
+
"g": "Provider Keys",
|
| 1141 |
+
"icon": "🔑",
|
| 1142 |
+
"k": "QIANFAN_API_KEY",
|
| 1143 |
+
"lbl": "Qianfan",
|
| 1144 |
+
"type": "password",
|
| 1145 |
+
"secret": 1,
|
| 1146 |
+
"common": 0
|
| 1147 |
+
},
|
| 1148 |
+
{
|
| 1149 |
+
"g": "Provider Keys",
|
| 1150 |
+
"icon": "🔑",
|
| 1151 |
+
"k": "MODELSTUDIO_API_KEY",
|
| 1152 |
+
"lbl": "ModelStudio",
|
| 1153 |
+
"type": "password",
|
| 1154 |
+
"secret": 1,
|
| 1155 |
+
"common": 0
|
| 1156 |
+
},
|
| 1157 |
+
{
|
| 1158 |
+
"g": "Provider Keys",
|
| 1159 |
+
"icon": "🔑",
|
| 1160 |
+
"k": "KIMI_API_KEY",
|
| 1161 |
+
"lbl": "Kimi",
|
| 1162 |
+
"type": "password",
|
| 1163 |
+
"secret": 1,
|
| 1164 |
+
"common": 0
|
| 1165 |
+
},
|
| 1166 |
+
{
|
| 1167 |
+
"g": "Provider Keys",
|
| 1168 |
+
"icon": "🔑",
|
| 1169 |
+
"k": "HUGGINGFACE_HUB_TOKEN",
|
| 1170 |
+
"lbl": "Hugging Face token",
|
| 1171 |
+
"type": "password",
|
| 1172 |
+
"secret": 1,
|
| 1173 |
+
"common": 0
|
| 1174 |
+
},
|
| 1175 |
+
{
|
| 1176 |
+
"g": "Provider Keys",
|
| 1177 |
+
"icon": "🔑",
|
| 1178 |
+
"k": "COPILOT_GITHUB_TOKEN",
|
| 1179 |
+
"lbl": "GitHub Copilot",
|
| 1180 |
+
"type": "password",
|
| 1181 |
+
"secret": 1,
|
| 1182 |
+
"common": 0
|
| 1183 |
+
},
|
| 1184 |
+
{
|
| 1185 |
+
"g": "Provider Keys",
|
| 1186 |
+
"icon": "🔑",
|
| 1187 |
+
"k": "VENICE_API_KEY",
|
| 1188 |
+
"lbl": "Venice",
|
| 1189 |
+
"type": "password",
|
| 1190 |
+
"secret": 1,
|
| 1191 |
+
"common": 0
|
| 1192 |
+
},
|
| 1193 |
+
{
|
| 1194 |
+
"g": "Provider Keys",
|
| 1195 |
+
"icon": "🔑",
|
| 1196 |
+
"k": "SYNTHETIC_API_KEY",
|
| 1197 |
+
"lbl": "Synthetic",
|
| 1198 |
+
"type": "password",
|
| 1199 |
+
"secret": 1,
|
| 1200 |
+
"common": 0
|
| 1201 |
+
},
|
| 1202 |
+
{
|
| 1203 |
+
"g": "Provider Keys",
|
| 1204 |
+
"icon": "🔑",
|
| 1205 |
+
"k": "AI_GATEWAY_API_KEY",
|
| 1206 |
+
"lbl": "AI Gateway",
|
| 1207 |
+
"type": "password",
|
| 1208 |
+
"secret": 1,
|
| 1209 |
+
"common": 0
|
| 1210 |
+
},
|
| 1211 |
+
{
|
| 1212 |
+
"g": "Provider Keys",
|
| 1213 |
+
"icon": "🔑",
|
| 1214 |
+
"k": "CLOUDFLARE_API_TOKEN",
|
| 1215 |
+
"lbl": "Cloudflare API token",
|
| 1216 |
+
"type": "password",
|
| 1217 |
+
"secret": 1,
|
| 1218 |
+
"common": 0
|
| 1219 |
+
},
|
| 1220 |
+
{
|
| 1221 |
+
"g": "Rotation Pools",
|
| 1222 |
+
"icon": "🔄",
|
| 1223 |
+
"k": "ANTHROPIC_API_KEYS",
|
| 1224 |
+
"lbl": "Anthropic pool (comma-sep)",
|
| 1225 |
+
"type": "text"
|
| 1226 |
+
},
|
| 1227 |
+
{
|
| 1228 |
+
"g": "Rotation Pools",
|
| 1229 |
+
"icon": "🔄",
|
| 1230 |
+
"k": "OPENAI_API_KEYS",
|
| 1231 |
+
"lbl": "OpenAI pool",
|
| 1232 |
+
"type": "text"
|
| 1233 |
+
},
|
| 1234 |
+
{
|
| 1235 |
+
"g": "Rotation Pools",
|
| 1236 |
+
"icon": "🔄",
|
| 1237 |
+
"k": "GEMINI_API_KEYS",
|
| 1238 |
+
"lbl": "Gemini pool",
|
| 1239 |
+
"type": "text"
|
| 1240 |
+
},
|
| 1241 |
+
{
|
| 1242 |
+
"g": "Rotation Pools",
|
| 1243 |
+
"icon": "🔄",
|
| 1244 |
+
"k": "GOOGLE_API_KEYS",
|
| 1245 |
+
"lbl": "Google pool",
|
| 1246 |
+
"type": "text"
|
| 1247 |
+
},
|
| 1248 |
+
{
|
| 1249 |
+
"g": "Rotation Pools",
|
| 1250 |
+
"icon": "🔄",
|
| 1251 |
+
"k": "DEEPSEEK_API_KEYS",
|
| 1252 |
+
"lbl": "DeepSeek pool",
|
| 1253 |
+
"type": "text"
|
| 1254 |
+
},
|
| 1255 |
+
{
|
| 1256 |
+
"g": "Rotation Pools",
|
| 1257 |
+
"icon": "🔄",
|
| 1258 |
+
"k": "OPENROUTER_API_KEYS",
|
| 1259 |
+
"lbl": "OpenRouter pool",
|
| 1260 |
+
"type": "text"
|
| 1261 |
+
},
|
| 1262 |
+
{
|
| 1263 |
+
"g": "Rotation Pools",
|
| 1264 |
+
"icon": "🔄",
|
| 1265 |
+
"k": "OPENCODE_API_KEYS",
|
| 1266 |
+
"lbl": "OpenCode pool",
|
| 1267 |
+
"type": "text"
|
| 1268 |
+
},
|
| 1269 |
+
{
|
| 1270 |
+
"g": "Rotation Pools",
|
| 1271 |
+
"icon": "🔄",
|
| 1272 |
+
"k": "KILOCODE_API_KEYS",
|
| 1273 |
+
"lbl": "KiloCode pool",
|
| 1274 |
+
"type": "text"
|
| 1275 |
+
},
|
| 1276 |
+
{
|
| 1277 |
+
"g": "Rotation Pools",
|
| 1278 |
+
"icon": "🔄",
|
| 1279 |
+
"k": "ZAI_API_KEYS",
|
| 1280 |
+
"lbl": "Z.ai / GLM pool",
|
| 1281 |
+
"type": "text"
|
| 1282 |
+
},
|
| 1283 |
+
{
|
| 1284 |
+
"g": "Rotation Pools",
|
| 1285 |
+
"icon": "🔄",
|
| 1286 |
+
"k": "MOONSHOT_API_KEYS",
|
| 1287 |
+
"lbl": "Moonshot / Kimi pool",
|
| 1288 |
+
"type": "text"
|
| 1289 |
+
},
|
| 1290 |
+
{
|
| 1291 |
+
"g": "Rotation Pools",
|
| 1292 |
+
"icon": "🔄",
|
| 1293 |
+
"k": "MINIMAX_API_KEYS",
|
| 1294 |
+
"lbl": "MiniMax pool",
|
| 1295 |
+
"type": "text"
|
| 1296 |
+
},
|
| 1297 |
+
{
|
| 1298 |
+
"g": "Rotation Pools",
|
| 1299 |
+
"icon": "🔄",
|
| 1300 |
+
"k": "XIAOMI_API_KEYS",
|
| 1301 |
+
"lbl": "Xiaomi pool",
|
| 1302 |
+
"type": "text"
|
| 1303 |
+
},
|
| 1304 |
+
{
|
| 1305 |
+
"g": "Rotation Pools",
|
| 1306 |
+
"icon": "🔄",
|
| 1307 |
+
"k": "VOLCANO_ENGINE_API_KEYS",
|
| 1308 |
+
"lbl": "Volcano Engine pool",
|
| 1309 |
+
"type": "text"
|
| 1310 |
+
},
|
| 1311 |
+
{
|
| 1312 |
+
"g": "Rotation Pools",
|
| 1313 |
+
"icon": "🔄",
|
| 1314 |
+
"k": "BYTEPLUS_API_KEYS",
|
| 1315 |
+
"lbl": "BytePlus pool",
|
| 1316 |
+
"type": "text"
|
| 1317 |
+
},
|
| 1318 |
+
{
|
| 1319 |
+
"g": "Rotation Pools",
|
| 1320 |
+
"icon": "🔄",
|
| 1321 |
+
"k": "MISTRAL_API_KEYS",
|
| 1322 |
+
"lbl": "Mistral pool",
|
| 1323 |
+
"type": "text"
|
| 1324 |
+
},
|
| 1325 |
+
{
|
| 1326 |
+
"g": "Rotation Pools",
|
| 1327 |
+
"icon": "🔄",
|
| 1328 |
+
"k": "XAI_API_KEYS",
|
| 1329 |
+
"lbl": "xAI pool",
|
| 1330 |
+
"type": "text"
|
| 1331 |
+
},
|
| 1332 |
+
{
|
| 1333 |
+
"g": "Rotation Pools",
|
| 1334 |
+
"icon": "🔄",
|
| 1335 |
+
"k": "NVIDIA_API_KEYS",
|
| 1336 |
+
"lbl": "NVIDIA pool",
|
| 1337 |
+
"type": "text"
|
| 1338 |
+
},
|
| 1339 |
+
{
|
| 1340 |
+
"g": "Rotation Pools",
|
| 1341 |
+
"icon": "🔄",
|
| 1342 |
+
"k": "GROQ_API_KEYS",
|
| 1343 |
+
"lbl": "Groq pool",
|
| 1344 |
+
"type": "text"
|
| 1345 |
+
},
|
| 1346 |
+
{
|
| 1347 |
+
"g": "Rotation Pools",
|
| 1348 |
+
"icon": "🔄",
|
| 1349 |
+
"k": "COHERE_API_KEYS",
|
| 1350 |
+
"lbl": "Cohere pool",
|
| 1351 |
+
"type": "text"
|
| 1352 |
+
},
|
| 1353 |
+
{
|
| 1354 |
+
"g": "Rotation Pools",
|
| 1355 |
+
"icon": "🔄",
|
| 1356 |
+
"k": "TOGETHER_API_KEYS",
|
| 1357 |
+
"lbl": "Together pool",
|
| 1358 |
+
"type": "text"
|
| 1359 |
+
},
|
| 1360 |
+
{
|
| 1361 |
+
"g": "Rotation Pools",
|
| 1362 |
+
"icon": "🔄",
|
| 1363 |
+
"k": "CEREBRAS_API_KEYS",
|
| 1364 |
+
"lbl": "Cerebras pool",
|
| 1365 |
+
"type": "text"
|
| 1366 |
+
},
|
| 1367 |
+
{
|
| 1368 |
+
"g": "Rotation Pools",
|
| 1369 |
+
"icon": "🔄",
|
| 1370 |
+
"k": "HUGGINGFACE_HUB_TOKENS",
|
| 1371 |
+
"lbl": "HF token pool",
|
| 1372 |
+
"type": "text"
|
| 1373 |
+
},
|
| 1374 |
+
{
|
| 1375 |
+
"g": "Model Lists",
|
| 1376 |
+
"icon": "📋",
|
| 1377 |
+
"k": "OPENAI_MODELS",
|
| 1378 |
+
"lbl": "Visible OpenAI models",
|
| 1379 |
+
"type": "model_list",
|
| 1380 |
+
"options_key": "OPENAI_MODELS",
|
| 1381 |
+
"ph": "Select models to build a comma list"
|
| 1382 |
+
},
|
| 1383 |
+
{
|
| 1384 |
+
"g": "Model Lists",
|
| 1385 |
+
"icon": "📋",
|
| 1386 |
+
"k": "ANTHROPIC_MODELS",
|
| 1387 |
+
"lbl": "Visible Anthropic models",
|
| 1388 |
+
"type": "model_list",
|
| 1389 |
+
"options_key": "ANTHROPIC_MODELS",
|
| 1390 |
+
"ph": "Select models to build a comma list"
|
| 1391 |
+
},
|
| 1392 |
+
{
|
| 1393 |
+
"g": "Model Lists",
|
| 1394 |
+
"icon": "📋",
|
| 1395 |
+
"k": "GEMINI_MODELS",
|
| 1396 |
+
"lbl": "Visible Gemini models",
|
| 1397 |
+
"type": "model_list",
|
| 1398 |
+
"options_key": "GEMINI_MODELS",
|
| 1399 |
+
"ph": "Select models to build a comma list"
|
| 1400 |
+
},
|
| 1401 |
+
{
|
| 1402 |
+
"g": "Model Lists",
|
| 1403 |
+
"icon": "📋",
|
| 1404 |
+
"k": "DEEPSEEK_MODELS",
|
| 1405 |
+
"lbl": "Visible DeepSeek models",
|
| 1406 |
+
"type": "model_list",
|
| 1407 |
+
"options_key": "DEEPSEEK_MODELS",
|
| 1408 |
+
"ph": "Select models to build a comma list"
|
| 1409 |
+
},
|
| 1410 |
+
{
|
| 1411 |
+
"g": "Model Lists",
|
| 1412 |
+
"icon": "📋",
|
| 1413 |
+
"k": "OPENROUTER_MODELS",
|
| 1414 |
+
"lbl": "Visible OpenRouter models",
|
| 1415 |
+
"type": "model_list",
|
| 1416 |
+
"options_key": "OPENROUTER_MODELS",
|
| 1417 |
+
"ph": "Select models to build a comma list"
|
| 1418 |
+
},
|
| 1419 |
+
{
|
| 1420 |
+
"g": "Model Lists",
|
| 1421 |
+
"icon": "📋",
|
| 1422 |
+
"k": "GROQ_MODELS",
|
| 1423 |
+
"lbl": "Visible Groq models",
|
| 1424 |
+
"type": "model_list",
|
| 1425 |
+
"options_key": "GROQ_MODELS",
|
| 1426 |
+
"ph": "Select models to build a comma list"
|
| 1427 |
+
},
|
| 1428 |
+
{
|
| 1429 |
+
"g": "Model Lists",
|
| 1430 |
+
"icon": "📋",
|
| 1431 |
+
"k": "MISTRAL_MODELS",
|
| 1432 |
+
"lbl": "Visible Mistral models",
|
| 1433 |
+
"type": "model_list",
|
| 1434 |
+
"options_key": "MISTRAL_MODELS",
|
| 1435 |
+
"ph": "Select models to build a comma list"
|
| 1436 |
+
},
|
| 1437 |
+
{
|
| 1438 |
+
"g": "Model Lists",
|
| 1439 |
+
"icon": "📋",
|
| 1440 |
+
"k": "XAI_MODELS",
|
| 1441 |
+
"lbl": "Visible xAI models",
|
| 1442 |
+
"type": "model_list",
|
| 1443 |
+
"options_key": "XAI_MODELS",
|
| 1444 |
+
"ph": "Select models to build a comma list"
|
| 1445 |
+
},
|
| 1446 |
+
{
|
| 1447 |
+
"g": "Model Lists",
|
| 1448 |
+
"icon": "📋",
|
| 1449 |
+
"k": "COHERE_MODELS",
|
| 1450 |
+
"lbl": "Visible Cohere models",
|
| 1451 |
+
"type": "model_list",
|
| 1452 |
+
"options_key": "COHERE_MODELS",
|
| 1453 |
+
"ph": "Select models to build a comma list"
|
| 1454 |
+
},
|
| 1455 |
+
{
|
| 1456 |
+
"g": "Model Lists",
|
| 1457 |
+
"icon": "📋",
|
| 1458 |
+
"k": "TOGETHER_MODELS",
|
| 1459 |
+
"lbl": "Visible Together models",
|
| 1460 |
+
"type": "model_list",
|
| 1461 |
+
"options_key": "TOGETHER_MODELS",
|
| 1462 |
+
"ph": "Select models to build a comma list"
|
| 1463 |
+
},
|
| 1464 |
+
{
|
| 1465 |
+
"g": "Model Lists",
|
| 1466 |
+
"icon": "📋",
|
| 1467 |
+
"k": "CEREBRAS_MODELS",
|
| 1468 |
+
"lbl": "Visible Cerebras models",
|
| 1469 |
+
"type": "model_list",
|
| 1470 |
+
"options_key": "CEREBRAS_MODELS",
|
| 1471 |
+
"ph": "Select models to build a comma list"
|
| 1472 |
+
},
|
| 1473 |
+
{
|
| 1474 |
+
"g": "Model Lists",
|
| 1475 |
+
"icon": "📋",
|
| 1476 |
+
"k": "NVIDIA_MODELS",
|
| 1477 |
+
"lbl": "Visible NVIDIA models",
|
| 1478 |
+
"type": "model_list",
|
| 1479 |
+
"options_key": "NVIDIA_MODELS",
|
| 1480 |
+
"ph": "Select models to build a comma list"
|
| 1481 |
+
},
|
| 1482 |
+
{
|
| 1483 |
+
"g": "Model Lists",
|
| 1484 |
+
"icon": "📋",
|
| 1485 |
+
"k": "KILOCODE_MODELS",
|
| 1486 |
+
"lbl": "Visible KiloCode models",
|
| 1487 |
+
"type": "model_list",
|
| 1488 |
+
"options_key": "KILOCODE_MODELS",
|
| 1489 |
+
"ph": "Select models to build a comma list"
|
| 1490 |
+
},
|
| 1491 |
+
{
|
| 1492 |
+
"g": "Model Lists",
|
| 1493 |
+
"icon": "📋",
|
| 1494 |
+
"k": "OPENCODE_MODELS",
|
| 1495 |
+
"lbl": "Visible OpenCode models",
|
| 1496 |
+
"type": "model_list",
|
| 1497 |
+
"options_key": "OPENCODE_MODELS",
|
| 1498 |
+
"ph": "Select models to build a comma list"
|
| 1499 |
+
},
|
| 1500 |
+
{
|
| 1501 |
+
"g": "Model Lists",
|
| 1502 |
+
"icon": "📋",
|
| 1503 |
+
"k": "ZAI_MODELS",
|
| 1504 |
+
"lbl": "Visible Z.ai / GLM models",
|
| 1505 |
+
"type": "model_list",
|
| 1506 |
+
"options_key": "ZAI_MODELS",
|
| 1507 |
+
"ph": "Select models to build a comma list"
|
| 1508 |
+
},
|
| 1509 |
+
{
|
| 1510 |
+
"g": "Model Lists",
|
| 1511 |
+
"icon": "📋",
|
| 1512 |
+
"k": "MOONSHOT_MODELS",
|
| 1513 |
+
"lbl": "Visible Moonshot / Kimi models",
|
| 1514 |
+
"type": "model_list",
|
| 1515 |
+
"options_key": "MOONSHOT_MODELS",
|
| 1516 |
+
"ph": "Select models to build a comma list"
|
| 1517 |
+
},
|
| 1518 |
+
{
|
| 1519 |
+
"g": "Model Lists",
|
| 1520 |
+
"icon": "📋",
|
| 1521 |
+
"k": "MINIMAX_MODELS",
|
| 1522 |
+
"lbl": "Visible MiniMax models",
|
| 1523 |
+
"type": "model_list",
|
| 1524 |
+
"options_key": "MINIMAX_MODELS",
|
| 1525 |
+
"ph": "Select models to build a comma list"
|
| 1526 |
+
},
|
| 1527 |
+
{
|
| 1528 |
+
"g": "Model Lists",
|
| 1529 |
+
"icon": "📋",
|
| 1530 |
+
"k": "XIAOMI_MODELS",
|
| 1531 |
+
"lbl": "Visible Xiaomi models",
|
| 1532 |
+
"type": "model_list",
|
| 1533 |
+
"options_key": "XIAOMI_MODELS",
|
| 1534 |
+
"ph": "Select models to build a comma list"
|
| 1535 |
+
},
|
| 1536 |
+
{
|
| 1537 |
+
"g": "Model Lists",
|
| 1538 |
+
"icon": "📋",
|
| 1539 |
+
"k": "VOLCANO_ENGINE_MODELS",
|
| 1540 |
+
"lbl": "Visible Volcano Engine models",
|
| 1541 |
+
"type": "model_list",
|
| 1542 |
+
"options_key": "VOLCANO_ENGINE_MODELS",
|
| 1543 |
+
"ph": "Select models to build a comma list"
|
| 1544 |
+
},
|
| 1545 |
+
{
|
| 1546 |
+
"g": "Model Lists",
|
| 1547 |
+
"icon": "📋",
|
| 1548 |
+
"k": "BYTEPLUS_MODELS",
|
| 1549 |
+
"lbl": "Visible BytePlus models",
|
| 1550 |
+
"type": "model_list",
|
| 1551 |
+
"options_key": "BYTEPLUS_MODELS",
|
| 1552 |
+
"ph": "Select models to build a comma list"
|
| 1553 |
+
},
|
| 1554 |
+
{
|
| 1555 |
+
"g": "Model Lists",
|
| 1556 |
+
"icon": "📋",
|
| 1557 |
+
"k": "QIANFAN_MODELS",
|
| 1558 |
+
"lbl": "Visible Qianfan models",
|
| 1559 |
+
"type": "model_list",
|
| 1560 |
+
"options_key": "QIANFAN_MODELS",
|
| 1561 |
+
"ph": "Select models to build a comma list"
|
| 1562 |
+
},
|
| 1563 |
+
{
|
| 1564 |
+
"g": "Model Lists",
|
| 1565 |
+
"icon": "📋",
|
| 1566 |
+
"k": "MODELSTUDIO_MODELS",
|
| 1567 |
+
"lbl": "Visible ModelStudio models",
|
| 1568 |
+
"type": "model_list",
|
| 1569 |
+
"options_key": "MODELSTUDIO_MODELS",
|
| 1570 |
+
"ph": "Select models to build a comma list"
|
| 1571 |
+
},
|
| 1572 |
+
{
|
| 1573 |
+
"g": "Model Lists",
|
| 1574 |
+
"icon": "📋",
|
| 1575 |
+
"k": "KIMI_MODELS",
|
| 1576 |
+
"lbl": "Visible Kimi models",
|
| 1577 |
+
"type": "model_list",
|
| 1578 |
+
"options_key": "KIMI_MODELS",
|
| 1579 |
+
"ph": "Select models to build a comma list"
|
| 1580 |
+
},
|
| 1581 |
+
{
|
| 1582 |
+
"g": "Model Lists",
|
| 1583 |
+
"icon": "📋",
|
| 1584 |
+
"k": "HUGGINGFACE_MODELS",
|
| 1585 |
+
"lbl": "Visible Hugging Face models",
|
| 1586 |
+
"type": "model_list",
|
| 1587 |
+
"options_key": "HUGGINGFACE_MODELS",
|
| 1588 |
+
"ph": "Select models to build a comma list"
|
| 1589 |
+
},
|
| 1590 |
+
{
|
| 1591 |
+
"g": "Model Lists",
|
| 1592 |
+
"icon": "📋",
|
| 1593 |
+
"k": "GITHUB_COPILOT_MODELS",
|
| 1594 |
+
"lbl": "Visible GitHub Copilot models",
|
| 1595 |
+
"type": "model_list",
|
| 1596 |
+
"options_key": "GITHUB_COPILOT_MODELS",
|
| 1597 |
+
"ph": "Select models to build a comma list"
|
| 1598 |
+
},
|
| 1599 |
+
{
|
| 1600 |
+
"g": "Custom Provider",
|
| 1601 |
+
"icon": "🔌",
|
| 1602 |
+
"k": "CUSTOM_PROVIDER_NAME",
|
| 1603 |
+
"lbl": "Provider display name",
|
| 1604 |
+
"type": "text"
|
| 1605 |
+
},
|
| 1606 |
+
{
|
| 1607 |
+
"g": "Custom Provider",
|
| 1608 |
+
"icon": "🔌",
|
| 1609 |
+
"k": "CUSTOM_BASE_URL",
|
| 1610 |
+
"lbl": "OpenAI-compatible base URL",
|
| 1611 |
+
"type": "text"
|
| 1612 |
+
},
|
| 1613 |
+
{
|
| 1614 |
+
"g": "Custom Provider",
|
| 1615 |
+
"icon": "🔌",
|
| 1616 |
+
"k": "CUSTOM_MODEL_ID",
|
| 1617 |
+
"lbl": "Model ID",
|
| 1618 |
+
"type": "model",
|
| 1619 |
+
"options_key": "LLM_MODEL",
|
| 1620 |
+
"ph": "custom model id"
|
| 1621 |
+
},
|
| 1622 |
+
{
|
| 1623 |
+
"g": "Custom Provider",
|
| 1624 |
+
"icon": "🔌",
|
| 1625 |
+
"k": "CUSTOM_MODEL_NAME",
|
| 1626 |
+
"lbl": "Friendly model name",
|
| 1627 |
+
"type": "text"
|
| 1628 |
+
},
|
| 1629 |
+
{
|
| 1630 |
+
"g": "Custom Provider",
|
| 1631 |
+
"icon": "🔌",
|
| 1632 |
+
"k": "CUSTOM_API_KEY",
|
| 1633 |
+
"lbl": "Provider API key",
|
| 1634 |
+
"type": "password",
|
| 1635 |
+
"secret": 1
|
| 1636 |
+
},
|
| 1637 |
+
{
|
| 1638 |
+
"g": "Custom Provider",
|
| 1639 |
+
"icon": "🔌",
|
| 1640 |
+
"k": "CUSTOM_API_TYPE",
|
| 1641 |
+
"lbl": "API type",
|
| 1642 |
+
"type": "select",
|
| 1643 |
+
"options": [
|
| 1644 |
+
"openai-completions",
|
| 1645 |
+
"openai-chat-completions",
|
| 1646 |
+
"anthropic",
|
| 1647 |
+
"gemini",
|
| 1648 |
+
"openrouter"
|
| 1649 |
+
],
|
| 1650 |
+
"ph": "openai-completions"
|
| 1651 |
+
},
|
| 1652 |
+
{
|
| 1653 |
+
"g": "Custom Provider",
|
| 1654 |
+
"icon": "🔌",
|
| 1655 |
+
"k": "CUSTOM_CONTEXT_WINDOW",
|
| 1656 |
+
"lbl": "Context window",
|
| 1657 |
+
"type": "number",
|
| 1658 |
+
"ph": "128000"
|
| 1659 |
+
},
|
| 1660 |
+
{
|
| 1661 |
+
"g": "Custom Provider",
|
| 1662 |
+
"icon": "🔌",
|
| 1663 |
+
"k": "CUSTOM_MAX_TOKENS",
|
| 1664 |
+
"lbl": "Max output tokens",
|
| 1665 |
+
"type": "number",
|
| 1666 |
+
"ph": "500"
|
| 1667 |
+
},
|
| 1668 |
+
{
|
| 1669 |
+
"g": "Telegram",
|
| 1670 |
+
"icon": "✈️",
|
| 1671 |
+
"k": "TELEGRAM_BOT_TOKEN",
|
| 1672 |
+
"lbl": "Bot token from BotFather",
|
| 1673 |
+
"type": "password",
|
| 1674 |
+
"secret": 1,
|
| 1675 |
+
"common": 1
|
| 1676 |
+
},
|
| 1677 |
+
{
|
| 1678 |
+
"g": "Telegram",
|
| 1679 |
+
"icon": "✈️",
|
| 1680 |
+
"k": "TELEGRAM_ALLOWED_USERS",
|
| 1681 |
+
"lbl": "Allowed user IDs (comma)",
|
| 1682 |
+
"type": "text",
|
| 1683 |
+
"ph": "123456789,987654321",
|
| 1684 |
+
"common": 1
|
| 1685 |
+
},
|
| 1686 |
+
{
|
| 1687 |
+
"g": "Telegram",
|
| 1688 |
+
"icon": "✈️",
|
| 1689 |
+
"k": "TELEGRAM_USER_ID",
|
| 1690 |
+
"lbl": "Single Telegram user ID",
|
| 1691 |
+
"type": "text"
|
| 1692 |
+
},
|
| 1693 |
+
{
|
| 1694 |
+
"g": "Telegram",
|
| 1695 |
+
"icon": "✈️",
|
| 1696 |
+
"k": "TELEGRAM_USER_IDS",
|
| 1697 |
+
"lbl": "Telegram user IDs (comma)",
|
| 1698 |
+
"type": "text"
|
| 1699 |
+
},
|
| 1700 |
+
{
|
| 1701 |
+
"g": "Deployment",
|
| 1702 |
+
"icon": "🧭",
|
| 1703 |
+
"k": "APP_BASE",
|
| 1704 |
+
"lbl": "Public app base path",
|
| 1705 |
+
"type": "text",
|
| 1706 |
+
"ph": "/app"
|
| 1707 |
+
},
|
| 1708 |
+
{
|
| 1709 |
+
"g": "Deployment",
|
| 1710 |
+
"icon": "🧭",
|
| 1711 |
+
"k": "BACKUP_DATASET",
|
| 1712 |
+
"lbl": "Backup dataset alias",
|
| 1713 |
+
"type": "text",
|
| 1714 |
+
"ph": "huggingclaw-backup"
|
| 1715 |
+
},
|
| 1716 |
+
{
|
| 1717 |
+
"g": "Deployment",
|
| 1718 |
+
"icon": "🧭",
|
| 1719 |
+
"k": "SPACE_AUTHOR_NAME",
|
| 1720 |
+
"lbl": "HF Space author name",
|
| 1721 |
+
"type": "text"
|
| 1722 |
+
},
|
| 1723 |
+
{
|
| 1724 |
+
"g": "Deployment",
|
| 1725 |
+
"icon": "🧭",
|
| 1726 |
+
"k": "SPACE_HOST",
|
| 1727 |
+
"lbl": "HF Space host domain",
|
| 1728 |
+
"type": "text"
|
| 1729 |
+
},
|
| 1730 |
+
{
|
| 1731 |
+
"g": "Deployment",
|
| 1732 |
+
"icon": "🧭",
|
| 1733 |
+
"k": "PORT",
|
| 1734 |
+
"lbl": "Public dashboard port",
|
| 1735 |
+
"type": "number",
|
| 1736 |
+
"ph": "7861"
|
| 1737 |
+
},
|
| 1738 |
+
{
|
| 1739 |
+
"g": "Deployment",
|
| 1740 |
+
"icon": "🧭",
|
| 1741 |
+
"k": "GATEWAY_PORT",
|
| 1742 |
+
"lbl": "OpenClaw internal port",
|
| 1743 |
+
"type": "number",
|
| 1744 |
+
"ph": "7860"
|
| 1745 |
+
},
|
| 1746 |
+
{
|
| 1747 |
+
"g": "Deployment",
|
| 1748 |
+
"icon": "🧭",
|
| 1749 |
+
"k": "JUPYTER_PORT",
|
| 1750 |
+
"lbl": "Jupyter internal port",
|
| 1751 |
+
"type": "number",
|
| 1752 |
+
"ph": "8888"
|
| 1753 |
+
},
|
| 1754 |
+
{
|
| 1755 |
+
"g": "Deployment",
|
| 1756 |
+
"icon": "🧭",
|
| 1757 |
+
"k": "JUPYTER_BASE",
|
| 1758 |
+
"lbl": "Jupyter public base path",
|
| 1759 |
+
"type": "text",
|
| 1760 |
+
"ph": "/terminal"
|
| 1761 |
+
}
|
| 1762 |
+
];
|
| 1763 |
+
|
| 1764 |
+
const ICONS = {
|
| 1765 |
+
All:'🏠', Core:'⚡', Deployment:'🧭', 'Provider Keys':'🔑', 'Rotation Pools':'🔄',
|
| 1766 |
+
'Model Lists':'📋', 'Custom Provider':'🔌', Telegram:'✈️', WhatsApp:'💬',
|
| 1767 |
+
Cloudflare:'☁️', Backup:'💾', DevData:'🧪', Runtime:'⚙️', 'Custom Env':'🔧'
|
| 1768 |
+
};
|
| 1769 |
+
|
| 1770 |
+
const $ = id => document.getElementById(id);
|
| 1771 |
+
const esc = s => String(s ?? '').replace(/[&<>"']/g, c => ({
|
| 1772 |
+
'&': '&',
|
| 1773 |
+
'<': '<',
|
| 1774 |
+
'>': '>',
|
| 1775 |
+
'"': '"',
|
| 1776 |
+
"'": '''
|
| 1777 |
+
}[c]));
|
| 1778 |
+
const safeKey = k => /^[A-Z_][A-Z0-9_]*$/.test(k) && !['HUGGINGCLAW_ENV_BUNDLE', 'ENV_BUNDLE'].includes(k);
|
| 1779 |
+
|
| 1780 |
+
function encodeBundle(obj) {
|
| 1781 |
+
const j = JSON.stringify(obj);
|
| 1782 |
+
let b = '';
|
| 1783 |
+
for (const x of new TextEncoder().encode(j)) b += String.fromCharCode(x);
|
| 1784 |
+
return btoa(b).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
| 1785 |
+
}
|
| 1786 |
+
|
| 1787 |
+
function decodeBundle(raw) {
|
| 1788 |
+
try {
|
| 1789 |
+
raw = String(raw || '').trim();
|
| 1790 |
+
if (!raw) return {};
|
| 1791 |
+
|
| 1792 |
+
if (raw.includes('HUGGINGCLAW_ENV_BUNDLE=')) {
|
| 1793 |
+
raw = raw.split('HUGGINGCLAW_ENV_BUNDLE=').pop().trim();
|
| 1794 |
+
}
|
| 1795 |
+
|
| 1796 |
+
if (
|
| 1797 |
+
(raw.startsWith('"') && raw.endsWith('"')) ||
|
| 1798 |
+
(raw.startsWith("'") && raw.endsWith("'"))
|
| 1799 |
+
) {
|
| 1800 |
+
raw = raw.slice(1, -1);
|
| 1801 |
+
}
|
| 1802 |
+
|
| 1803 |
+
if (raw.startsWith('{')) return JSON.parse(raw);
|
| 1804 |
+
|
| 1805 |
+
const p = raw + '='.repeat((4 - raw.length % 4) % 4);
|
| 1806 |
+
const b = atob(p.replace(/-/g, '+').replace(/_/g, '/'));
|
| 1807 |
+
const bytes = Uint8Array.from(b, c => c.charCodeAt(0));
|
| 1808 |
+
return JSON.parse(new TextDecoder().decode(bytes));
|
| 1809 |
+
} catch {
|
| 1810 |
+
return {};
|
| 1811 |
+
}
|
| 1812 |
+
}
|
| 1813 |
+
|
| 1814 |
+
function parseEnv(text) {
|
| 1815 |
+
text = String(text || '').trim();
|
| 1816 |
+
if (!text) return {};
|
| 1817 |
+
|
| 1818 |
+
if (
|
| 1819 |
+
text.startsWith('{') ||
|
| 1820 |
+
/^[A-Za-z0-9_-]{20,}$/.test(text) ||
|
| 1821 |
+
text.includes('HUGGINGCLAW_ENV_BUNDLE=')
|
| 1822 |
+
) {
|
| 1823 |
+
return decodeBundle(text);
|
| 1824 |
+
}
|
| 1825 |
+
|
| 1826 |
+
const out = {};
|
| 1827 |
+
for (let line of text.split(/\r?\n/)) {
|
| 1828 |
+
line = line.trim();
|
| 1829 |
+
if (!line || line.startsWith('#')) continue;
|
| 1830 |
+
if (line.startsWith('export ')) line = line.slice(7).trim();
|
| 1831 |
+
const i = line.indexOf('=');
|
| 1832 |
+
if (i < 1) continue;
|
| 1833 |
+
|
| 1834 |
+
const key = line.slice(0, i).trim();
|
| 1835 |
+
let val = line.slice(i + 1).trim();
|
| 1836 |
+
|
| 1837 |
+
if (
|
| 1838 |
+
(val.startsWith('"') && val.endsWith('"')) ||
|
| 1839 |
+
(val.startsWith("'") && val.endsWith("'"))
|
| 1840 |
+
) {
|
| 1841 |
+
val = val.slice(1, -1);
|
| 1842 |
+
}
|
| 1843 |
+
|
| 1844 |
+
if (safeKey(key)) out[key] = val;
|
| 1845 |
+
}
|
| 1846 |
+
return out;
|
| 1847 |
+
}
|
| 1848 |
+
|
| 1849 |
+
function showToast(msg = 'Copied!') {
|
| 1850 |
+
const t = $('toast');
|
| 1851 |
+
t.textContent = msg;
|
| 1852 |
+
t.classList.add('show');
|
| 1853 |
+
setTimeout(() => t.classList.remove('show'), 1500);
|
| 1854 |
+
}
|
| 1855 |
+
|
| 1856 |
+
let activeGroup = 'All';
|
| 1857 |
+
let customCount = 0;
|
| 1858 |
+
const GROUPS = ['All', ...[...new Set(FIELDS.map(f => f.g))], 'Custom Env'];
|
| 1859 |
+
|
| 1860 |
+
function renderSidebar() {
|
| 1861 |
+
const sb = $('sidebar');
|
| 1862 |
+
sb.innerHTML = '<div class="sb-label">Groups</div>';
|
| 1863 |
+
GROUPS.forEach(g => {
|
| 1864 |
+
const btn = document.createElement('button');
|
| 1865 |
+
btn.className = 'nav-btn' + (activeGroup === g ? ' active' : '');
|
| 1866 |
+
btn.dataset.group = g;
|
| 1867 |
+
const id = 'nc_' + g.replace(/\W/g, '_');
|
| 1868 |
+
btn.innerHTML = `<span class="nav-icon">${ICONS[g] || '📁'}</span><span class="nav-label">${esc(g)}</span><span class="nav-count" id="${id}">0</span>`;
|
| 1869 |
+
btn.onclick = () => {
|
| 1870 |
+
activeGroup = g;
|
| 1871 |
+
renderSidebar();
|
| 1872 |
+
filter();
|
| 1873 |
+
};
|
| 1874 |
+
sb.appendChild(btn);
|
| 1875 |
+
});
|
| 1876 |
+
}
|
| 1877 |
+
|
| 1878 |
+
function renderOptionsHTML(field) {
|
| 1879 |
+
const src = field.options || MODEL_CATALOGS[field.options_key] || [];
|
| 1880 |
+
|
| 1881 |
+
if (field.options_key === 'LLM_MODEL') {
|
| 1882 |
+
const groups = MODEL_CATALOGS.LLM_MODEL || {};
|
| 1883 |
+
return Object.entries(groups).map(([group, items]) => {
|
| 1884 |
+
const options = items.map(v => `<option value="${esc(v)}">${esc(v)}</option>`).join('');
|
| 1885 |
+
return `<optgroup label="${esc(group)}">${options}</optgroup>`;
|
| 1886 |
+
}).join('');
|
| 1887 |
+
}
|
| 1888 |
+
|
| 1889 |
+
if (Array.isArray(src)) {
|
| 1890 |
+
return src.map(v => `<option value="${esc(v)}">${esc(v)}</option>`).join('');
|
| 1891 |
+
}
|
| 1892 |
+
|
| 1893 |
+
return '';
|
| 1894 |
+
}
|
| 1895 |
+
|
| 1896 |
+
function defaultValueFor(field) {
|
| 1897 |
+
if (field.type === 'toggle') {
|
| 1898 |
+
const on = String(field.ph ?? '').toLowerCase();
|
| 1899 |
+
return ['1', 'true', 'yes', 'on', 'enabled'].includes(on) ? 'true' : 'false';
|
| 1900 |
+
}
|
| 1901 |
+
if (field.type === 'select') return String(field.ph ?? '');
|
| 1902 |
+
return '';
|
| 1903 |
+
}
|
| 1904 |
+
|
| 1905 |
+
function valueControlHTML(field) {
|
| 1906 |
+
const key = esc(field.k);
|
| 1907 |
+
const placeholder = esc(field.ph || field.lbl || '');
|
| 1908 |
+
const isSecret = !!field.secret;
|
| 1909 |
+
const isTextarea = field.type === 'textarea' || field.type === 'model_list';
|
| 1910 |
+
const hasPicker = !!field.options_key || Array.isArray(field.options);
|
| 1911 |
+
const inputType = isSecret ? 'password' : (field.type === 'number' ? 'number' : 'text');
|
| 1912 |
+
|
| 1913 |
+
let control = '';
|
| 1914 |
+
if (field.type === 'toggle') {
|
| 1915 |
+
const initial = defaultValueFor(field);
|
| 1916 |
+
control = `
|
| 1917 |
+
<div class="toggle-shell" data-toggle-row="1" data-field="${key}">
|
| 1918 |
+
<input type="hidden" data-key="${key}" value="${initial}">
|
| 1919 |
+
<button type="button" class="tog ${initial === 'true' ? 'on' : ''}" data-toggle="${key}">${initial === 'true' ? 'On' : 'Off'}</button>
|
| 1920 |
+
</div>`;
|
| 1921 |
+
} else if (isTextarea) {
|
| 1922 |
+
control = `<textarea data-key="${key}" placeholder="${placeholder}" spellcheck="false"></textarea>`;
|
| 1923 |
+
} else {
|
| 1924 |
+
control = `<input type="${inputType}" data-key="${key}" placeholder="${placeholder}" spellcheck="false"/>`;
|
| 1925 |
+
}
|
| 1926 |
+
|
| 1927 |
+
if (!hasPicker) return control;
|
| 1928 |
+
|
| 1929 |
+
const pickerMode = field.type === 'model_list' ? 'multi' : 'single';
|
| 1930 |
+
const pickerLabel = field.type === 'model_list' ? 'Add model…' : 'Choose preset…';
|
| 1931 |
+
return `
|
| 1932 |
+
<div class="picker-shell" data-picker-shell="${key}" data-picker-mode="${pickerMode}">
|
| 1933 |
+
<div class="picker-row">
|
| 1934 |
+
<select class="picker-select" data-pick-for="${key}" aria-label="${esc(field.lbl || field.k)} presets">
|
| 1935 |
+
<option value="">${esc(pickerLabel)}</option>
|
| 1936 |
+
${renderOptionsHTML(field)}
|
| 1937 |
+
<option value="__custom__">Custom…</option>
|
| 1938 |
+
</select>
|
| 1939 |
+
<button type="button" class="mini-btn" data-custom-for="${key}">+ Custom</button>
|
| 1940 |
+
<button type="button" class="mini-btn" data-clear-for="${key}">Clear</button>
|
| 1941 |
+
</div>
|
| 1942 |
+
${control}
|
| 1943 |
+
</div>`;
|
| 1944 |
+
|
| 1945 |
+
return control;
|
| 1946 |
+
}
|
| 1947 |
+
|
| 1948 |
+
function cardHTML(f) {
|
| 1949 |
+
const badge = f.secret
|
| 1950 |
+
? '<span class="badge badge-s">secret</span>'
|
| 1951 |
+
: '<span class="badge badge-f">safe</span>';
|
| 1952 |
+
|
| 1953 |
+
return `<div class="env-card" data-row data-group="${esc(f.g)}" data-search="${esc((f.g + ' ' + f.k + ' ' + (f.lbl || '')).toLowerCase())}">
|
| 1954 |
+
<div class="card-top">
|
| 1955 |
+
<input type="checkbox" class="card-check" data-check="${esc(f.k)}" ${f.common ? 'data-common="1"' : ''}>
|
| 1956 |
+
<div class="card-info">
|
| 1957 |
+
<div class="card-key">${esc(f.k)}</div>
|
| 1958 |
+
<div class="card-lbl">${esc(f.lbl || '')}</div>
|
| 1959 |
+
</div>
|
| 1960 |
+
${badge}
|
| 1961 |
+
</div>
|
| 1962 |
+
<div class="card-input">${valueControlHTML(f)}</div>
|
| 1963 |
+
</div>`;
|
| 1964 |
+
}
|
| 1965 |
+
|
| 1966 |
+
function addCustomRow(key = '', val = '', enabled = false) {
|
| 1967 |
+
const id = customCount++;
|
| 1968 |
+
const row = document.createElement('div');
|
| 1969 |
+
row.className = 'custom-row';
|
| 1970 |
+
row.dataset.customRow = id;
|
| 1971 |
+
row.dataset.enabled = enabled ? '1' : '0';
|
| 1972 |
+
|
| 1973 |
+
row.innerHTML = `
|
| 1974 |
+
<input data-ck="${id}" placeholder="CUSTOM_ENV_NAME" value="${esc(key)}">
|
| 1975 |
+
<input data-cv="${id}" placeholder="value" value="${esc(val)}">
|
| 1976 |
+
<button class="tog${enabled ? ' on' : ''}">${enabled ? 'On' : 'Off'}</button>
|
| 1977 |
+
`;
|
| 1978 |
+
|
| 1979 |
+
$('customRows').appendChild(row);
|
| 1980 |
+
|
| 1981 |
+
row.querySelectorAll('input').forEach(el => el.addEventListener('input', refresh));
|
| 1982 |
+
row.querySelector('button').onclick = () => {
|
| 1983 |
+
const on = row.dataset.enabled !== '1';
|
| 1984 |
+
row.dataset.enabled = on ? '1' : '0';
|
| 1985 |
+
row.querySelector('button').textContent = on ? 'On' : 'Off';
|
| 1986 |
+
row.querySelector('button').classList.toggle('on', on);
|
| 1987 |
+
refresh();
|
| 1988 |
+
};
|
| 1989 |
+
}
|
| 1990 |
+
|
| 1991 |
+
function getFieldValueInput(key) {
|
| 1992 |
+
return document.querySelector(`[data-key="${CSS.escape(key)}"]`);
|
| 1993 |
+
}
|
| 1994 |
+
|
| 1995 |
+
function setFieldValue(key, value) {
|
| 1996 |
+
const el = getFieldValueInput(key);
|
| 1997 |
+
if (!el) return;
|
| 1998 |
+
el.value = value ?? '';
|
| 1999 |
+
}
|
| 2000 |
+
|
| 2001 |
+
function appendCsvValue(existing, next) {
|
| 2002 |
+
const parts = String(existing || '').split(',').map(s => s.trim()).filter(Boolean);
|
| 2003 |
+
const val = String(next || '').trim();
|
| 2004 |
+
if (!val) return parts.join(', ');
|
| 2005 |
+
if (!parts.includes(val)) parts.push(val);
|
| 2006 |
+
return parts.join(', ');
|
| 2007 |
+
}
|
| 2008 |
+
|
| 2009 |
+
function collect() {
|
| 2010 |
+
const obj = {};
|
| 2011 |
+
document.querySelectorAll('[data-key]').forEach(el => {
|
| 2012 |
+
const key = el.dataset.key;
|
| 2013 |
+
if (!key || !safeKey(key)) return;
|
| 2014 |
+
// Only include if the card's checkbox is ticked
|
| 2015 |
+
const chk = document.querySelector(`[data-check="${CSS.escape(key)}"]`);
|
| 2016 |
+
if (!chk || !chk.checked) return;
|
| 2017 |
+
const val = String(el.value ?? '').trim();
|
| 2018 |
+
if (val) obj[key] = val;
|
| 2019 |
+
});
|
| 2020 |
+
|
| 2021 |
+
document.querySelectorAll('[data-custom-row]').forEach(row => {
|
| 2022 |
+
const id = row.dataset.customRow;
|
| 2023 |
+
const key = (row.querySelector(`[data-ck="${id}"]`)?.value || '').trim();
|
| 2024 |
+
const val = (row.querySelector(`[data-cv="${id}"]`)?.value || '').trim();
|
| 2025 |
+
if (row.dataset.enabled === '1' && safeKey(key) && val) obj[key] = val;
|
| 2026 |
+
});
|
| 2027 |
+
|
| 2028 |
+
return obj;
|
| 2029 |
+
}
|
| 2030 |
+
|
| 2031 |
+
function refresh() {
|
| 2032 |
+
const obj = collect();
|
| 2033 |
+
const keys = Object.keys(obj).sort();
|
| 2034 |
+
const bundle = keys.length ? encodeBundle(Object.fromEntries(keys.map(k => [k, obj[k]]))) : '';
|
| 2035 |
+
|
| 2036 |
+
$('bundleOut').value = bundle;
|
| 2037 |
+
$('envLineOut').value = bundle ? `HUGGINGCLAW_ENV_BUNDLE=${bundle}` : '';
|
| 2038 |
+
|
| 2039 |
+
const s = $('summary');
|
| 2040 |
+
if (keys.length) {
|
| 2041 |
+
s.innerHTML = `<strong>${keys.length}</strong> variable${keys.length > 1 ? 's' : ''} selected<div class="sum-keys">${keys.map(k => `<span class="sum-key">${esc(k)}</span>`).join('')}</div>`;
|
| 2042 |
+
} else {
|
| 2043 |
+
s.innerHTML = 'No variables selected yet.';
|
| 2044 |
+
}
|
| 2045 |
+
updateCounts();
|
| 2046 |
+
}
|
| 2047 |
+
|
| 2048 |
+
function markSelected() {
|
| 2049 |
+
document.querySelectorAll('[data-row]').forEach(r => r.classList.toggle('selected', !!r.querySelector('[data-check]')?.checked));
|
| 2050 |
+
}
|
| 2051 |
+
|
| 2052 |
+
function updateCounts() {
|
| 2053 |
+
document.querySelectorAll('[id^="nc_"]').forEach(el => el.textContent = '0');
|
| 2054 |
+
const byGrp = {};
|
| 2055 |
+
document.querySelectorAll('[data-check]:checked').forEach(ch => {
|
| 2056 |
+
const g = ch.closest('[data-row]')?.dataset.group;
|
| 2057 |
+
if (g) byGrp[g] = (byGrp[g] || 0) + 1;
|
| 2058 |
+
});
|
| 2059 |
+
const custOn = document.querySelectorAll('[data-custom-row][data-enabled="1"]').length;
|
| 2060 |
+
const total = Object.values(byGrp).reduce((a, b) => a + b, 0) + custOn;
|
| 2061 |
+
const allEl = document.getElementById('nc_All'); if (allEl) allEl.textContent = total;
|
| 2062 |
+
Object.entries(byGrp).forEach(([g, c]) => {
|
| 2063 |
+
const el = document.getElementById('nc_' + g.replace(/\W/g, '_'));
|
| 2064 |
+
if (el) el.textContent = c;
|
| 2065 |
+
});
|
| 2066 |
+
const custEl = document.getElementById('nc_Custom_Env'); if (custEl) custEl.textContent = custOn;
|
| 2067 |
+
}
|
| 2068 |
+
|
| 2069 |
+
function filter() {
|
| 2070 |
+
const q = $('search').value.trim().toLowerCase();
|
| 2071 |
+
document.querySelectorAll('.sec[data-section]').forEach(sec => {
|
| 2072 |
+
const grp = sec.dataset.section;
|
| 2073 |
+
const gMatch = activeGroup === 'All' || activeGroup === grp;
|
| 2074 |
+
if (!gMatch) { sec.classList.add('sec-hidden'); return; }
|
| 2075 |
+
let any = false;
|
| 2076 |
+
sec.querySelectorAll('[data-row]').forEach(card => {
|
| 2077 |
+
const m = !q || card.dataset.search.includes(q);
|
| 2078 |
+
card.classList.toggle('hidden', !m);
|
| 2079 |
+
if (m) any = true;
|
| 2080 |
+
});
|
| 2081 |
+
sec.classList.toggle('sec-hidden', !any);
|
| 2082 |
+
});
|
| 2083 |
+
const cs = $('customSec');
|
| 2084 |
+
if (cs) cs.style.display = (activeGroup === 'All' || activeGroup === 'Custom Env') ? '' : 'none';
|
| 2085 |
+
document.querySelectorAll('.nav-btn').forEach(b => b.classList.toggle('active', b.dataset.group === activeGroup));
|
| 2086 |
+
}
|
| 2087 |
+
|
| 2088 |
+
function clearForm() {
|
| 2089 |
+
document.querySelectorAll('[data-check]').forEach(c => c.checked = false);
|
| 2090 |
+
document.querySelectorAll('[data-key]').forEach(el => {
|
| 2091 |
+
if (el.closest('[data-toggle-row]')) {
|
| 2092 |
+
el.value = 'false';
|
| 2093 |
+
const btn = el.closest('.toggle-shell')?.querySelector('[data-toggle]');
|
| 2094 |
+
if (btn) {
|
| 2095 |
+
btn.textContent = 'Off';
|
| 2096 |
+
btn.classList.remove('on');
|
| 2097 |
+
}
|
| 2098 |
+
return;
|
| 2099 |
+
}
|
| 2100 |
+
el.value = '';
|
| 2101 |
+
});
|
| 2102 |
+
$('customRows').innerHTML = '';
|
| 2103 |
+
customCount = 0;
|
| 2104 |
+
addCustomRow();
|
| 2105 |
+
}
|
| 2106 |
+
|
| 2107 |
+
function applyObj(obj, replace = false) {
|
| 2108 |
+
if (replace) clearForm();
|
| 2109 |
+
for (const [key, val] of Object.entries(obj || {})) {
|
| 2110 |
+
if (!safeKey(key)) continue;
|
| 2111 |
+
const inp = getFieldValueInput(key);
|
| 2112 |
+
const chk = document.querySelector(`[data-check="${CSS.escape(key)}"]`);
|
| 2113 |
+
if (inp && chk) {
|
| 2114 |
+
inp.value = val;
|
| 2115 |
+
chk.checked = true;
|
| 2116 |
+
const btn = inp.closest('[data-toggle-row]')?.querySelector('[data-toggle]');
|
| 2117 |
+
if (btn) {
|
| 2118 |
+
const on = String(val).trim().toLowerCase() === 'true';
|
| 2119 |
+
btn.textContent = on ? 'On' : 'Off';
|
| 2120 |
+
btn.classList.toggle('on', on);
|
| 2121 |
+
inp.value = on ? 'true' : 'false';
|
| 2122 |
+
}
|
| 2123 |
+
} else {
|
| 2124 |
+
addCustomRow(key, val, true);
|
| 2125 |
+
}
|
| 2126 |
+
}
|
| 2127 |
+
markSelected(); filter(); refresh();
|
| 2128 |
+
}
|
| 2129 |
+
|
| 2130 |
+
function autoCheck(key) {
|
| 2131 |
+
const chk = document.querySelector(`[data-check="${CSS.escape(key)}"]`);
|
| 2132 |
+
if (chk && !chk.checked) {
|
| 2133 |
+
chk.checked = true;
|
| 2134 |
+
markSelected();
|
| 2135 |
+
}
|
| 2136 |
+
}
|
| 2137 |
+
|
| 2138 |
+
function handlePickerChange(sel) {
|
| 2139 |
+
const key = sel.dataset.pickFor;
|
| 2140 |
+
const mode = sel.closest('[data-picker-shell]')?.dataset.pickerMode || 'single';
|
| 2141 |
+
const value = sel.value;
|
| 2142 |
+
if (!key || !value) return;
|
| 2143 |
+
if (value === '__custom__') {
|
| 2144 |
+
sel.value = '';
|
| 2145 |
+
return;
|
| 2146 |
+
}
|
| 2147 |
+
const inp = getFieldValueInput(key);
|
| 2148 |
+
if (!inp) return;
|
| 2149 |
+
|
| 2150 |
+
if (mode === 'multi') {
|
| 2151 |
+
inp.value = appendCsvValue(inp.value, value);
|
| 2152 |
+
} else {
|
| 2153 |
+
inp.value = value;
|
| 2154 |
+
}
|
| 2155 |
+
sel.value = '';
|
| 2156 |
+
autoCheck(key);
|
| 2157 |
+
refresh();
|
| 2158 |
+
}
|
| 2159 |
+
|
| 2160 |
+
function promptCustomModel(btn) {
|
| 2161 |
+
const key = btn.dataset.customFor;
|
| 2162 |
+
const mode = btn.closest('[data-picker-shell]')?.dataset.pickerMode || 'single';
|
| 2163 |
+
const inp = getFieldValueInput(key);
|
| 2164 |
+
if (!inp) return;
|
| 2165 |
+
const message = mode === 'multi'
|
| 2166 |
+
? 'Enter one or more custom model IDs separated by commas'
|
| 2167 |
+
: 'Enter a custom model ID';
|
| 2168 |
+
const initial = '';
|
| 2169 |
+
const text = prompt(message, initial);
|
| 2170 |
+
if (text === null) return;
|
| 2171 |
+
const val = String(text).trim();
|
| 2172 |
+
if (!val) return;
|
| 2173 |
+
if (mode === 'multi') {
|
| 2174 |
+
const vals = val.split(',').map(s => s.trim()).filter(Boolean);
|
| 2175 |
+
let out = inp.value || '';
|
| 2176 |
+
for (const v of vals) out = appendCsvValue(out, v);
|
| 2177 |
+
inp.value = out;
|
| 2178 |
+
} else {
|
| 2179 |
+
inp.value = val;
|
| 2180 |
+
}
|
| 2181 |
+
autoCheck(key);
|
| 2182 |
+
refresh();
|
| 2183 |
+
}
|
| 2184 |
+
|
| 2185 |
+
function resetPickerField(btn) {
|
| 2186 |
+
const key = btn.dataset.clearFor;
|
| 2187 |
+
const inp = getFieldValueInput(key);
|
| 2188 |
+
if (!inp) return;
|
| 2189 |
+
if (inp.closest('[data-toggle-row]')) {
|
| 2190 |
+
inp.value = 'false';
|
| 2191 |
+
const toggleBtn = inp.closest('.toggle-shell')?.querySelector('[data-toggle]');
|
| 2192 |
+
if (toggleBtn) {
|
| 2193 |
+
toggleBtn.textContent = 'Off';
|
| 2194 |
+
toggleBtn.classList.remove('on');
|
| 2195 |
+
}
|
| 2196 |
+
} else {
|
| 2197 |
+
inp.value = '';
|
| 2198 |
+
}
|
| 2199 |
+
refresh();
|
| 2200 |
+
}
|
| 2201 |
+
|
| 2202 |
+
function toggleField(key) {
|
| 2203 |
+
const inp = getFieldValueInput(key);
|
| 2204 |
+
if (!inp) return;
|
| 2205 |
+
const on = String(inp.value || '').trim().toLowerCase() !== 'true';
|
| 2206 |
+
inp.value = on ? 'true' : 'false';
|
| 2207 |
+
const btn = inp.closest('.toggle-shell')?.querySelector('[data-toggle]');
|
| 2208 |
+
if (btn) {
|
| 2209 |
+
btn.textContent = on ? 'On' : 'Off';
|
| 2210 |
+
btn.classList.toggle('on', on);
|
| 2211 |
+
}
|
| 2212 |
+
// Auto-check when turned on; uncheck when turned off
|
| 2213 |
+
const chk = document.querySelector(`[data-check="${CSS.escape(key)}"]`);
|
| 2214 |
+
if (chk) {
|
| 2215 |
+
chk.checked = on;
|
| 2216 |
+
markSelected();
|
| 2217 |
+
}
|
| 2218 |
+
refresh();
|
| 2219 |
+
}
|
| 2220 |
+
|
| 2221 |
+
function bindFieldEvents() {
|
| 2222 |
+
document.querySelectorAll('[data-check]').forEach(el => el.addEventListener('change', () => { markSelected(); refresh(); }));
|
| 2223 |
+
document.querySelectorAll('[data-key]').forEach(el => el.addEventListener('input', refresh));
|
| 2224 |
+
document.querySelectorAll('[data-toggle]').forEach(btn => btn.addEventListener('click', () => toggleField(btn.dataset.toggle)));
|
| 2225 |
+
document.querySelectorAll('[data-pick-for]').forEach(sel => sel.addEventListener('change', () => handlePickerChange(sel)));
|
| 2226 |
+
document.querySelectorAll('[data-custom-for]').forEach(btn => btn.addEventListener('click', () => promptCustomModel(btn)));
|
| 2227 |
+
document.querySelectorAll('[data-clear-for]').forEach(btn => btn.addEventListener('click', () => resetPickerField(btn)));
|
| 2228 |
+
}
|
| 2229 |
+
|
| 2230 |
+
function renderSections() {
|
| 2231 |
+
const grouped = {};
|
| 2232 |
+
FIELDS.forEach(f => { (grouped[f.g] ||= []).push(f); });
|
| 2233 |
+
|
| 2234 |
+
const wrap = $('sections');
|
| 2235 |
+
wrap.innerHTML = '';
|
| 2236 |
+
Object.entries(grouped).forEach(([grp, items]) => {
|
| 2237 |
+
const sec = document.createElement('div');
|
| 2238 |
+
sec.className = 'sec';
|
| 2239 |
+
sec.dataset.section = grp;
|
| 2240 |
+
sec.innerHTML = `
|
| 2241 |
+
<div class="sec-header">
|
| 2242 |
+
<span class="sec-icon">${ICONS[grp] || '📁'}</span>
|
| 2243 |
+
<span class="sec-title">${esc(grp)}</span>
|
| 2244 |
+
<div class="sec-line"></div>
|
| 2245 |
+
</div>
|
| 2246 |
+
<div class="cards">${items.map(cardHTML).join('')}</div>`;
|
| 2247 |
+
wrap.appendChild(sec);
|
| 2248 |
+
});
|
| 2249 |
+
bindFieldEvents();
|
| 2250 |
+
}
|
| 2251 |
+
|
| 2252 |
+
function copyText(text) {
|
| 2253 |
+
return navigator.clipboard.writeText(text).then(
|
| 2254 |
+
() => showToast('Copied ✓'),
|
| 2255 |
+
() => {
|
| 2256 |
+
const ta = document.createElement('textarea');
|
| 2257 |
+
ta.value = text;
|
| 2258 |
+
ta.style.position = 'fixed';
|
| 2259 |
+
ta.style.left = '-9999px';
|
| 2260 |
+
document.body.appendChild(ta);
|
| 2261 |
+
ta.select();
|
| 2262 |
+
document.execCommand('copy');
|
| 2263 |
+
ta.remove();
|
| 2264 |
+
showToast('Copied ✓');
|
| 2265 |
+
}
|
| 2266 |
+
);
|
| 2267 |
+
}
|
| 2268 |
+
|
| 2269 |
+
// ── Init ──
|
| 2270 |
+
renderSidebar();
|
| 2271 |
+
renderSections();
|
| 2272 |
+
addCustomRow();
|
| 2273 |
+
filter();
|
| 2274 |
+
refresh();
|
| 2275 |
+
|
| 2276 |
+
// ── Events ──
|
| 2277 |
+
$('search').oninput = filter;
|
| 2278 |
+
$('selectCommon').onclick = () => {
|
| 2279 |
+
document.querySelectorAll('[data-common="1"]').forEach(c => c.checked = true);
|
| 2280 |
+
markSelected();
|
| 2281 |
+
refresh();
|
| 2282 |
+
};
|
| 2283 |
+
$('selectVisible').onclick = () => {
|
| 2284 |
+
document.querySelectorAll('.sec:not(.sec-hidden) [data-row]:not(.hidden) [data-check]').forEach(c => c.checked = true);
|
| 2285 |
+
markSelected();
|
| 2286 |
+
refresh();
|
| 2287 |
+
};
|
| 2288 |
+
$('clearAll').onclick = () => {
|
| 2289 |
+
clearForm();
|
| 2290 |
+
markSelected();
|
| 2291 |
+
filter();
|
| 2292 |
+
refresh();
|
| 2293 |
+
};
|
| 2294 |
+
$('applyImport').onclick = () => {
|
| 2295 |
+
try {
|
| 2296 |
+
applyObj(parseEnv($('importText').value), true);
|
| 2297 |
+
showToast('Imported ✓');
|
| 2298 |
+
} catch (e) {
|
| 2299 |
+
showToast('Import failed');
|
| 2300 |
+
alert(e.message);
|
| 2301 |
+
}
|
| 2302 |
+
};
|
| 2303 |
+
|
| 2304 |
+
// Auto-import: paste karo aur turant parse + apply ho jaata hai
|
| 2305 |
+
$('importText').addEventListener('paste', () => {
|
| 2306 |
+
setTimeout(() => {
|
| 2307 |
+
try {
|
| 2308 |
+
const val = $('importText').value.trim();
|
| 2309 |
+
if (!val) return;
|
| 2310 |
+
applyObj(parseEnv(val), true);
|
| 2311 |
+
showToast('Auto-imported ✓');
|
| 2312 |
+
} catch (e) {
|
| 2313 |
+
showToast('Import failed');
|
| 2314 |
+
}
|
| 2315 |
+
}, 0);
|
| 2316 |
+
});
|
| 2317 |
+
|
| 2318 |
+
// Live typing: jaise jaise type karo env format mein, bundle banta jaata hai
|
| 2319 |
+
$('importText').addEventListener('input', () => {
|
| 2320 |
+
const val = $('importText').value.trim();
|
| 2321 |
+
if (!val) return;
|
| 2322 |
+
// Sirf agar valid env/bundle format lag raha ho tabhi auto-apply
|
| 2323 |
+
const looksLikeEnv = val.includes('=') || val.startsWith('{') || /^[A-Za-z0-9_\-]{20,}$/.test(val);
|
| 2324 |
+
if (looksLikeEnv) {
|
| 2325 |
+
try {
|
| 2326 |
+
applyObj(parseEnv(val), true);
|
| 2327 |
+
} catch (e) { /* silent — user abhi type kar raha hai */ }
|
| 2328 |
+
}
|
| 2329 |
+
});
|
| 2330 |
+
$('addCustom').onclick = () => addCustomRow();
|
| 2331 |
+
$('applyBundle').onclick = () => {
|
| 2332 |
+
try {
|
| 2333 |
+
applyObj(decodeBundle($('bundleOut').value), true);
|
| 2334 |
+
showToast('Bundle applied ✓');
|
| 2335 |
+
} catch (e) {
|
| 2336 |
+
showToast('Invalid bundle');
|
| 2337 |
+
}
|
| 2338 |
+
};
|
| 2339 |
+
$('copyBundle').onclick = () => copyText($('bundleOut').value);
|
| 2340 |
+
$('copyEnvLine').onclick = () => copyText($('envLineOut').value);
|
| 2341 |
+
$('copyJson').onclick = () => copyText(JSON.stringify(collect(), null, 2));
|
health-server.js
CHANGED
|
@@ -1,58 +1,126 @@
|
|
| 1 |
-
// Single public entrypoint for HF Spaces:
|
| 2 |
const http = require("http");
|
|
|
|
| 3 |
const fs = require("fs");
|
| 4 |
const net = require("net");
|
| 5 |
|
| 6 |
-
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
const GATEWAY_HOST = "127.0.0.1";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
const startTime = Date.now();
|
| 10 |
const LLM_MODEL = process.env.LLM_MODEL || "Not Set";
|
| 11 |
const TELEGRAM_ENABLED = !!process.env.TELEGRAM_BOT_TOKEN;
|
| 12 |
-
const WHATSAPP_ENABLED =
|
| 13 |
const WHATSAPP_STATUS_FILE = "/tmp/huggingclaw-wa-status.json";
|
| 14 |
const HF_BACKUP_ENABLED = !!process.env.HF_TOKEN;
|
| 15 |
-
const SYNC_INTERVAL = process.env.SYNC_INTERVAL || "180";
|
| 16 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
const SYNC_STATUS_FILE = "/tmp/sync-status.json";
|
| 18 |
-
const CLOUDFLARE_KEEPALIVE_STATUS_FILE =
|
| 19 |
-
"/tmp/huggingclaw-cloudflare-keepalive-status.json";
|
| 20 |
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
try {
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
} catch {
|
| 25 |
-
|
| 26 |
}
|
| 27 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
function getSyncStatus() {
|
| 30 |
try {
|
| 31 |
-
if (fs.existsSync(SYNC_STATUS_FILE))
|
| 32 |
return JSON.parse(fs.readFileSync(SYNC_STATUS_FILE, "utf8"));
|
| 33 |
-
}
|
| 34 |
} catch {}
|
| 35 |
-
if (HF_BACKUP_ENABLED)
|
| 36 |
-
return {
|
| 37 |
-
status: "configured",
|
| 38 |
-
message: `Backup is enabled. Waiting for sync window (${SYNC_INTERVAL}s).`,
|
| 39 |
-
};
|
| 40 |
-
}
|
| 41 |
return { status: "unknown", message: "No sync data yet" };
|
| 42 |
}
|
| 43 |
|
| 44 |
function readGuardianStatus() {
|
| 45 |
-
if (!WHATSAPP_ENABLED) {
|
| 46 |
-
return { configured: false, connected: false, pairing: false };
|
| 47 |
-
}
|
| 48 |
try {
|
| 49 |
if (fs.existsSync(WHATSAPP_STATUS_FILE)) {
|
| 50 |
-
const
|
| 51 |
-
return {
|
| 52 |
-
configured: parsed.configured !== false,
|
| 53 |
-
connected: parsed.connected === true,
|
| 54 |
-
pairing: parsed.pairing === true,
|
| 55 |
-
};
|
| 56 |
}
|
| 57 |
} catch {}
|
| 58 |
return { configured: true, connected: false, pairing: false };
|
|
@@ -60,71 +128,42 @@ function readGuardianStatus() {
|
|
| 60 |
|
| 61 |
function getKeepaliveStatus() {
|
| 62 |
try {
|
| 63 |
-
if (fs.existsSync(CLOUDFLARE_KEEPALIVE_STATUS_FILE))
|
| 64 |
-
return JSON.parse(
|
| 65 |
-
fs.readFileSync(CLOUDFLARE_KEEPALIVE_STATUS_FILE, "utf8"),
|
| 66 |
-
);
|
| 67 |
-
}
|
| 68 |
} catch {}
|
| 69 |
return null;
|
| 70 |
}
|
| 71 |
|
| 72 |
-
function
|
| 73 |
return new Promise((resolve) => {
|
| 74 |
-
const
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
port: GATEWAY_PORT,
|
| 78 |
-
path: "/health",
|
| 79 |
-
timeout: timeoutMs,
|
| 80 |
-
},
|
| 81 |
-
(response) => {
|
| 82 |
-
response.resume();
|
| 83 |
-
resolve(response.statusCode >= 200 && response.statusCode < 400);
|
| 84 |
-
},
|
| 85 |
-
);
|
| 86 |
-
request.on("timeout", () => {
|
| 87 |
-
request.destroy();
|
| 88 |
-
resolve(false);
|
| 89 |
});
|
| 90 |
-
|
|
|
|
| 91 |
});
|
| 92 |
}
|
| 93 |
|
| 94 |
function formatUptime(ms) {
|
| 95 |
-
const
|
| 96 |
-
const
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
if (hours) return `${hours}h ${minutes}m`;
|
| 101 |
-
return `${minutes}m`;
|
| 102 |
}
|
| 103 |
|
| 104 |
-
function escapeHtml(
|
| 105 |
-
return String(
|
| 106 |
-
.replace(/&/g, "&")
|
| 107 |
-
.replace(/</g, "<")
|
| 108 |
-
.replace(/>/g, ">")
|
| 109 |
-
.replace(/"/g, """);
|
| 110 |
}
|
| 111 |
|
| 112 |
-
function
|
| 113 |
return `<span class="badge ${tone}">${escapeHtml(label)}</span>`;
|
| 114 |
}
|
| 115 |
|
| 116 |
-
function
|
| 117 |
-
title,
|
| 118 |
-
value,
|
| 119 |
-
detail = "",
|
| 120 |
-
tone = "neutral",
|
| 121 |
-
meta = "",
|
| 122 |
-
}) {
|
| 123 |
return `<article class="tile ${tone}">
|
| 124 |
-
<div class="tile-head">
|
| 125 |
-
<span class="tile-title">${escapeHtml(title)}</span>
|
| 126 |
-
<span class="tile-dot"></span>
|
| 127 |
-
</div>
|
| 128 |
<div class="tile-value">${value}</div>
|
| 129 |
${detail ? `<div class="tile-detail">${detail}</div>` : ""}
|
| 130 |
${meta ? `<div class="tile-meta">${meta}</div>` : ""}
|
|
@@ -133,276 +172,391 @@ function renderTile({
|
|
| 133 |
|
| 134 |
function renderDashboard(data) {
|
| 135 |
const syncStatus = String(data.sync?.status || "unknown");
|
| 136 |
-
const syncTone = ["success",
|
| 137 |
-
|
| 138 |
-
)
|
| 139 |
-
|
| 140 |
-
: syncStatus === "disabled"
|
| 141 |
-
? "warn"
|
| 142 |
-
: "neutral";
|
| 143 |
-
const backupDetail = data.sync?.message
|
| 144 |
-
? escapeHtml(data.sync.message)
|
| 145 |
-
: "No status yet";
|
| 146 |
-
|
| 147 |
-
const keepaliveConfigured = data.keepalive?.configured === true;
|
| 148 |
-
const keepaliveStatus = String(
|
| 149 |
-
data.keepalive?.status ||
|
| 150 |
-
(process.env.CLOUDFLARE_WORKERS_TOKEN ? "pending" : "not configured"),
|
| 151 |
-
);
|
| 152 |
-
const keepAliveTone = keepaliveConfigured
|
| 153 |
-
? "ok"
|
| 154 |
-
: process.env.CLOUDFLARE_WORKERS_TOKEN
|
| 155 |
-
? "warn"
|
| 156 |
-
: "neutral";
|
| 157 |
-
const keepAliveDetail = keepaliveConfigured
|
| 158 |
-
? `Pinging <code>${escapeHtml(data.keepalive.targetUrl || "/health")}</code>`
|
| 159 |
-
: process.env.CLOUDFLARE_WORKERS_TOKEN
|
| 160 |
-
? "Worker pending or failed"
|
| 161 |
-
: "Not configured";
|
| 162 |
|
| 163 |
const tiles = [
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
}),
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
})
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
detail: TELEGRAM_ENABLED ? "Bot Channel active" : "Not configured",
|
| 192 |
-
tone: TELEGRAM_ENABLED ? "ok" : "neutral",
|
| 193 |
-
}),
|
| 194 |
-
renderTile({
|
| 195 |
-
title: "Backup",
|
| 196 |
-
value: toneBadge(syncStatus.toUpperCase(), syncTone),
|
| 197 |
-
detail: backupDetail,
|
| 198 |
-
tone: syncTone,
|
| 199 |
-
meta: data.sync?.timestamp
|
| 200 |
-
? `<span class="local-time" data-iso="${data.sync.timestamp}"></span>`
|
| 201 |
-
: "",
|
| 202 |
-
}),
|
| 203 |
-
renderTile({
|
| 204 |
-
title: "Keep Awake",
|
| 205 |
-
value: toneBadge(
|
| 206 |
-
keepaliveConfigured ? "CF Cron" : keepaliveStatus.toUpperCase(),
|
| 207 |
-
keepAliveTone,
|
| 208 |
-
),
|
| 209 |
-
detail: keepAliveDetail,
|
| 210 |
-
tone: keepAliveTone,
|
| 211 |
-
}),
|
| 212 |
-
].join("");
|
| 213 |
-
|
| 214 |
-
return `<!doctype html>
|
| 215 |
-
<html lang="en">
|
| 216 |
-
<head>
|
| 217 |
-
<meta charset="utf-8" />
|
| 218 |
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 219 |
<title>HuggingClaw</title>
|
| 220 |
<style>
|
| 221 |
-
:root
|
| 222 |
-
*
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
.
|
| 228 |
-
.hero-action
|
| 229 |
-
.
|
| 230 |
-
.
|
| 231 |
-
.tile
|
| 232 |
-
.tile
|
| 233 |
-
.tile
|
| 234 |
-
.tile
|
| 235 |
-
.tile
|
| 236 |
-
.tile-
|
| 237 |
-
.tile-
|
| 238 |
-
|
| 239 |
-
.
|
| 240 |
-
.
|
| 241 |
-
.
|
| 242 |
-
.
|
| 243 |
-
.
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
</style>
|
| 255 |
-
</head>
|
| 256 |
-
<body>
|
| 257 |
-
<main>
|
| 258 |
-
<header>
|
| 259 |
-
<h1>🦞 HuggingClaw</h1>
|
| 260 |
-
<div class="subtitle">OpenClaw Gateway Dashboard</div>
|
| 261 |
-
</header>
|
| 262 |
-
<a class="hero-action" href="${APP_BASE}/" target="_blank" rel="noopener noreferrer">Open Control UI -></a>
|
| 263 |
-
<section class="overview">
|
| 264 |
-
${tiles}
|
| 265 |
-
</section>
|
| 266 |
-
<footer>Built by <a href="https://github.com/somratpro" target="_blank" rel="noopener noreferrer" style="color: var(--accent); text-decoration: none;">@somratpro</a></footer>
|
| 267 |
</main>
|
| 268 |
<script>
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 275 |
</script>
|
| 276 |
-
</body>
|
| 277 |
-
</html>`;
|
| 278 |
}
|
| 279 |
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 283 |
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
return res.end(
|
| 291 |
-
JSON.stringify({
|
| 292 |
-
status: gatewayReady ? "ok" : "degraded",
|
| 293 |
-
gatewayReady,
|
| 294 |
-
uptime: formatUptime(Date.now() - startTime),
|
| 295 |
-
sync: getSyncStatus(),
|
| 296 |
-
keepalive: getKeepaliveStatus(),
|
| 297 |
-
}),
|
| 298 |
-
);
|
| 299 |
}
|
|
|
|
|
|
|
| 300 |
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 314 |
}
|
| 315 |
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
keepalive: getKeepaliveStatus(),
|
| 326 |
-
}),
|
| 327 |
-
);
|
| 328 |
}
|
|
|
|
| 329 |
|
| 330 |
-
|
| 331 |
-
const
|
|
|
|
| 332 |
...req.headers,
|
| 333 |
-
host: `${
|
| 334 |
"x-forwarded-for": req.socket.remoteAddress,
|
| 335 |
"x-forwarded-host": req.headers.host,
|
| 336 |
"x-forwarded-proto": "https",
|
|
|
|
| 337 |
};
|
| 338 |
|
| 339 |
-
const
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
);
|
| 356 |
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
|
|
|
|
|
|
| 361 |
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
});
|
| 366 |
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
)
|
| 377 |
-
|
| 378 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 379 |
}
|
| 380 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
|
| 382 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 383 |
});
|
| 384 |
|
|
|
|
| 385 |
server.on("upgrade", (req, socket, head) => {
|
| 386 |
-
const
|
| 387 |
-
const
|
| 388 |
-
const
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 392 |
for (let i = 0; i < req.rawHeaders.length; i += 2) {
|
| 393 |
-
|
|
|
|
|
|
|
|
|
|
| 394 |
}
|
| 395 |
-
|
| 396 |
-
if (head && head.length)
|
| 397 |
-
|
| 398 |
});
|
| 399 |
-
|
|
|
|
|
|
|
|
|
|
| 400 |
});
|
| 401 |
|
| 402 |
server.timeout = 0;
|
| 403 |
server.keepAliveTimeout = 65000;
|
|
|
|
| 404 |
server.listen(PORT, "0.0.0.0", () =>
|
| 405 |
-
console.log(
|
| 406 |
-
`🦞 HuggingClaw Dashboard on ${PORT} -> Gateway on ${GATEWAY_PORT}`,
|
| 407 |
-
),
|
| 408 |
);
|
|
|
|
| 1 |
+
// Single public entrypoint for HF Spaces: dashboard + reverse proxy to OpenClaw + JupyterLab.
|
| 2 |
const http = require("http");
|
| 3 |
+
const https = require("https");
|
| 4 |
const fs = require("fs");
|
| 5 |
const net = require("net");
|
| 6 |
|
| 7 |
+
function isTrue(value) {
|
| 8 |
+
return /^(true|1|yes|on)$/i.test(String(value || "").trim());
|
| 9 |
+
}
|
| 10 |
+
function normalizeBase(value, fallback) {
|
| 11 |
+
const raw = String(value || fallback || "").trim() || fallback;
|
| 12 |
+
if (!raw) return fallback;
|
| 13 |
+
const base = raw.startsWith("/") ? raw : `/${raw}`;
|
| 14 |
+
return base.replace(/\/+$/, "") || fallback;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
const PORT = Number.parseInt(process.env.PORT || "7861", 10);
|
| 18 |
+
const GATEWAY_PORT = Number.parseInt(process.env.GATEWAY_PORT || "7860", 10);
|
| 19 |
const GATEWAY_HOST = "127.0.0.1";
|
| 20 |
+
const JUPYTER_PORT = Number.parseInt(process.env.JUPYTER_PORT || "8888", 10);
|
| 21 |
+
const JUPYTER_HOST = "127.0.0.1";
|
| 22 |
+
const JUPYTER_BASE = normalizeBase(process.env.JUPYTER_BASE, "/terminal");
|
| 23 |
+
const DEV_MODE_ENABLED = isTrue(process.env.DEV_MODE);
|
| 24 |
+
const JUPYTER_ENABLED = /^(true|1|yes|on)$/i.test(
|
| 25 |
+
process.env.HUGGINGCLAW_JUPYTER_ENABLED || (DEV_MODE_ENABLED ? "true" : "false")
|
| 26 |
+
);
|
| 27 |
const startTime = Date.now();
|
| 28 |
const LLM_MODEL = process.env.LLM_MODEL || "Not Set";
|
| 29 |
const TELEGRAM_ENABLED = !!process.env.TELEGRAM_BOT_TOKEN;
|
| 30 |
+
const WHATSAPP_ENABLED = isTrue(process.env.WHATSAPP_ENABLED);
|
| 31 |
const WHATSAPP_STATUS_FILE = "/tmp/huggingclaw-wa-status.json";
|
| 32 |
const HF_BACKUP_ENABLED = !!process.env.HF_TOKEN;
|
| 33 |
+
const SYNC_INTERVAL = (process.env.SYNC_INTERVAL || "180").trim() || "180";
|
| 34 |
+
const BACKUP_DATASET_NAME = (process.env.BACKUP_DATASET_NAME || process.env.BACKUP_DATASET || "huggingclaw-backup").trim() || "huggingclaw-backup";
|
| 35 |
+
const DEVDATA_DATASET_NAME = (process.env.DEVDATA_DATASET_NAME || "huggingclaw-devdata").trim() || "huggingclaw-devdata";
|
| 36 |
+
const DEVDATA_SYNC_INTERVAL = (process.env.DEVDATA_SYNC_INTERVAL || "180").trim() || "180";
|
| 37 |
+
const DEVDATA_SEPARATE_DATASET = DEVDATA_DATASET_NAME !== BACKUP_DATASET_NAME;
|
| 38 |
+
const DEVDATA_ENABLED = JUPYTER_ENABLED && HF_BACKUP_ENABLED && DEVDATA_SEPARATE_DATASET && !/^(off|false|0|no)$/i.test((process.env.DEVDATA || "on").trim());
|
| 39 |
+
const APP_BASE = normalizeBase(process.env.APP_BASE, "/app");
|
| 40 |
const SYNC_STATUS_FILE = "/tmp/sync-status.json";
|
|
|
|
|
|
|
| 41 |
|
| 42 |
+
// ── Private Space redirect support ──
|
| 43 |
+
// HF automatically sets SPACE_ID as "username/spacename" in every Space container.
|
| 44 |
+
const SPACE_ID = (process.env.SPACE_ID || "").trim();
|
| 45 |
+
function deriveHfSpaceUrl() {
|
| 46 |
+
if (SPACE_ID) return `https://huggingface.co/spaces/${SPACE_ID}`;
|
| 47 |
+
const host = (process.env.SPACE_HOST || "").replace(/\.hf\.space$/i, "");
|
| 48 |
+
const author = (process.env.SPACE_AUTHOR_NAME || "").trim().toLowerCase();
|
| 49 |
+
if (author && host.toLowerCase().startsWith(author + "-")) {
|
| 50 |
+
const spaceName = host.slice(author.length + 1);
|
| 51 |
+
return `https://huggingface.co/spaces/${process.env.SPACE_AUTHOR_NAME}/${spaceName}`;
|
| 52 |
+
}
|
| 53 |
+
return "";
|
| 54 |
+
}
|
| 55 |
+
const HF_SPACE_URL = deriveHfSpaceUrl();
|
| 56 |
+
|
| 57 |
+
// Auto-detect space privacy via HF API at startup.
|
| 58 |
+
// Caches result so every request doesn't hit the API.
|
| 59 |
+
let SPACE_IS_PRIVATE = false;
|
| 60 |
+
async function detectSpacePrivacy() {
|
| 61 |
+
if (!SPACE_ID) return;
|
| 62 |
try {
|
| 63 |
+
const token = (process.env.HF_TOKEN || "").trim();
|
| 64 |
+
const reqOptions = {
|
| 65 |
+
hostname: "huggingface.co",
|
| 66 |
+
path: `/api/spaces/${SPACE_ID}`,
|
| 67 |
+
method: "GET",
|
| 68 |
+
headers: Object.assign(
|
| 69 |
+
{ "User-Agent": "HuggingClaw/health-server" },
|
| 70 |
+
token ? { Authorization: `Bearer ${token}` } : {}
|
| 71 |
+
),
|
| 72 |
+
};
|
| 73 |
+
await new Promise((resolve) => {
|
| 74 |
+
const r = https.request(reqOptions, (res) => {
|
| 75 |
+
let body = "";
|
| 76 |
+
res.on("data", (chunk) => { body += chunk; });
|
| 77 |
+
res.on("end", () => {
|
| 78 |
+
try {
|
| 79 |
+
if (res.statusCode === 200) {
|
| 80 |
+
const data = JSON.parse(body);
|
| 81 |
+
SPACE_IS_PRIVATE = data.private === true;
|
| 82 |
+
} else if (res.statusCode === 404 && !token) {
|
| 83 |
+
// 404 with no token usually means private space
|
| 84 |
+
SPACE_IS_PRIVATE = true;
|
| 85 |
+
}
|
| 86 |
+
} catch {}
|
| 87 |
+
resolve();
|
| 88 |
+
});
|
| 89 |
+
});
|
| 90 |
+
r.on("error", resolve);
|
| 91 |
+
r.setTimeout(5000, () => { r.destroy(); resolve(); });
|
| 92 |
+
r.end();
|
| 93 |
+
});
|
| 94 |
+
console.log(`[health-server] Space privacy detected: ${SPACE_IS_PRIVATE ? "private" : "public"}`);
|
| 95 |
} catch {
|
| 96 |
+
// Network error — default to false (safe)
|
| 97 |
}
|
| 98 |
}
|
| 99 |
+
detectSpacePrivacy();
|
| 100 |
+
const CLOUDFLARE_KEEPALIVE_STATUS_FILE =
|
| 101 |
+
"/tmp/huggingclaw-cloudflare-keepalive-status.json";
|
| 102 |
+
|
| 103 |
+
function parseRequestUrl(url) {
|
| 104 |
+
try { return new URL(url, "http://localhost"); }
|
| 105 |
+
catch { return new URL("http://localhost/"); }
|
| 106 |
+
}
|
| 107 |
|
| 108 |
function getSyncStatus() {
|
| 109 |
try {
|
| 110 |
+
if (fs.existsSync(SYNC_STATUS_FILE))
|
| 111 |
return JSON.parse(fs.readFileSync(SYNC_STATUS_FILE, "utf8"));
|
|
|
|
| 112 |
} catch {}
|
| 113 |
+
if (HF_BACKUP_ENABLED)
|
| 114 |
+
return { status: "configured", message: `Backup enabled. Waiting for sync window (${SYNC_INTERVAL}s).` };
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
return { status: "unknown", message: "No sync data yet" };
|
| 116 |
}
|
| 117 |
|
| 118 |
function readGuardianStatus() {
|
| 119 |
+
if (!WHATSAPP_ENABLED) return { configured: false, connected: false, pairing: false };
|
|
|
|
|
|
|
| 120 |
try {
|
| 121 |
if (fs.existsSync(WHATSAPP_STATUS_FILE)) {
|
| 122 |
+
const p = JSON.parse(fs.readFileSync(WHATSAPP_STATUS_FILE, "utf8"));
|
| 123 |
+
return { configured: p.configured !== false, connected: p.connected === true, pairing: p.pairing === true };
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
}
|
| 125 |
} catch {}
|
| 126 |
return { configured: true, connected: false, pairing: false };
|
|
|
|
| 128 |
|
| 129 |
function getKeepaliveStatus() {
|
| 130 |
try {
|
| 131 |
+
if (fs.existsSync(CLOUDFLARE_KEEPALIVE_STATUS_FILE))
|
| 132 |
+
return JSON.parse(fs.readFileSync(CLOUDFLARE_KEEPALIVE_STATUS_FILE, "utf8"));
|
|
|
|
|
|
|
|
|
|
| 133 |
} catch {}
|
| 134 |
return null;
|
| 135 |
}
|
| 136 |
|
| 137 |
+
function probePort(host, port, path, timeoutMs = 1500) {
|
| 138 |
return new Promise((resolve) => {
|
| 139 |
+
const req = http.get({ hostname: host, port, path, timeout: timeoutMs }, (res) => {
|
| 140 |
+
res.resume();
|
| 141 |
+
resolve(res.statusCode >= 200 && res.statusCode < 400);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
});
|
| 143 |
+
req.on("timeout", () => { req.destroy(); resolve(false); });
|
| 144 |
+
req.on("error", () => resolve(false));
|
| 145 |
});
|
| 146 |
}
|
| 147 |
|
| 148 |
function formatUptime(ms) {
|
| 149 |
+
const t = Math.floor(ms / 1000);
|
| 150 |
+
const d = Math.floor(t / 86400), h = Math.floor((t % 86400) / 3600), m = Math.floor((t % 3600) / 60);
|
| 151 |
+
if (d) return `${d}d ${h}h ${m}m`;
|
| 152 |
+
if (h) return `${h}h ${m}m`;
|
| 153 |
+
return `${m}m`;
|
|
|
|
|
|
|
| 154 |
}
|
| 155 |
|
| 156 |
+
function escapeHtml(v) {
|
| 157 |
+
return String(v).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
}
|
| 159 |
|
| 160 |
+
function badge(label, tone = "neutral") {
|
| 161 |
return `<span class="badge ${tone}">${escapeHtml(label)}</span>`;
|
| 162 |
}
|
| 163 |
|
| 164 |
+
function tile({ title, value, detail = "", tone = "neutral", meta = "" }) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
return `<article class="tile ${tone}">
|
| 166 |
+
<div class="tile-head"><span class="tile-title">${escapeHtml(title)}</span><span class="tile-dot"></span></div>
|
|
|
|
|
|
|
|
|
|
| 167 |
<div class="tile-value">${value}</div>
|
| 168 |
${detail ? `<div class="tile-detail">${detail}</div>` : ""}
|
| 169 |
${meta ? `<div class="tile-meta">${meta}</div>` : ""}
|
|
|
|
| 172 |
|
| 173 |
function renderDashboard(data) {
|
| 174 |
const syncStatus = String(data.sync?.status || "unknown");
|
| 175 |
+
const syncTone = ["success","restored","synced","configured"].includes(syncStatus) ? "ok" : syncStatus === "disabled" ? "warn" : "neutral";
|
| 176 |
+
const kaConf = data.keepalive?.configured === true;
|
| 177 |
+
const kaStatus = String(data.keepalive?.status || (process.env.CLOUDFLARE_WORKERS_TOKEN ? "pending" : "not configured"));
|
| 178 |
+
const kaTone = kaConf ? "ok" : process.env.CLOUDFLARE_WORKERS_TOKEN ? "warn" : "neutral";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
|
| 180 |
const tiles = [
|
| 181 |
+
tile({ title: "Gateway", value: badge(data.gatewayReady ? "Online" : "Offline", data.gatewayReady ? "ok" : "off"), detail: `OpenClaw on internal port ${GATEWAY_PORT}`, tone: data.gatewayReady ? "ok" : "off" }),
|
| 182 |
+
tile({ title: "Model", value: `<code>${escapeHtml(LLM_MODEL)}</code>`, detail: "Primary LLM configured", tone: "neutral" }),
|
| 183 |
+
tile({ title: "Runtime", value: escapeHtml(data.uptimeHuman), detail: `Public port ${PORT}`, tone: "neutral" }),
|
| 184 |
+
tile({ title: "Telegram", value: badge(TELEGRAM_ENABLED ? "Enabled" : "Disabled", TELEGRAM_ENABLED ? "ok" : "neutral"), detail: TELEGRAM_ENABLED ? "Bot channel active" : "Not configured", tone: TELEGRAM_ENABLED ? "ok" : "neutral" }),
|
| 185 |
+
];
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
tiles.push(
|
| 189 |
+
tile({ title: "Backup", value: badge(syncStatus.toUpperCase(), syncTone), detail: escapeHtml(data.sync?.message || "No status yet"), tone: syncTone, meta: data.sync?.timestamp ? `<span class="local-time" data-iso="${data.sync.timestamp}"></span>` : "" }),
|
| 190 |
+
tile({ title: "Keep Awake", value: badge(kaConf ? "CF Cron" : kaStatus.toUpperCase(), kaTone), detail: kaConf ? `Pinging <code>${escapeHtml(data.keepalive?.targetUrl || "/health")}</code>` : process.env.CLOUDFLARE_WORKERS_TOKEN ? "Worker pending or failed" : "Not configured", tone: kaTone }),
|
| 191 |
+
);
|
| 192 |
+
|
| 193 |
+
if (JUPYTER_ENABLED) {
|
| 194 |
+
tiles.push(tile({ title: "Terminal", value: badge(data.jupyterReady ? "Online" : "Starting…", data.jupyterReady ? "ok" : "warn"), detail: `JupyterLab at <a href="${JUPYTER_BASE}/" style="color:inherit">${JUPYTER_BASE}/</a>`, tone: data.jupyterReady ? "ok" : "warn" }));
|
| 195 |
+
tiles.push(tile({
|
| 196 |
+
title: "DevData",
|
| 197 |
+
value: badge(DEVDATA_ENABLED ? "Enabled" : "Disabled", DEVDATA_ENABLED ? "ok" : "neutral"),
|
| 198 |
+
detail: DEVDATA_ENABLED ? `Separate dataset <code>${escapeHtml(DEVDATA_DATASET_NAME)}</code>` : DEVDATA_SEPARATE_DATASET ? "Separate Jupyter dataset backup inactive" : "DevData dataset must be separate from main backup dataset",
|
| 199 |
+
tone: DEVDATA_ENABLED ? "ok" : "neutral",
|
| 200 |
+
meta: `Sync interval ${escapeHtml(DEVDATA_SYNC_INTERVAL)}s`,
|
| 201 |
+
}));
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
const tilesHtml = tiles.join("");
|
| 205 |
+
|
| 206 |
+
return `<!doctype html><html lang="en"><head>
|
| 207 |
+
<meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
<title>HuggingClaw</title>
|
| 209 |
<style>
|
| 210 |
+
:root{color-scheme:dark;--bg:#08080f;--panel:#12111b;--line:#26243a;--text:#f6f4ff;--muted:#7f7a9e;--soft:#b8b3d7;--good:#22c55e;--warn:#f5c542;--bad:#fb7185}
|
| 211 |
+
*{box-sizing:border-box}body{margin:0;min-height:100vh;font-family:Inter,ui-sans-serif,system-ui,-apple-system,sans-serif;background:var(--bg);color:var(--text);font-size:13px}
|
| 212 |
+
main{width:min(720px,calc(100% - 32px));margin:0 auto;padding:36px 0 44px}
|
| 213 |
+
header{text-align:center;margin-bottom:22px}h1{margin:0;font-size:1.65rem;line-height:1}
|
| 214 |
+
.subtitle{margin-top:12px;color:var(--muted);font-size:.72rem;text-transform:uppercase;letter-spacing:.14em;font-weight:800}
|
| 215 |
+
.btn-row{display:flex;gap:12px;margin:24px 0 20px}
|
| 216 |
+
.hero-action{display:flex;flex:1;min-height:46px;align-items:center;justify-content:center;border-radius:8px;background:#fff;color:#000;text-decoration:none;font-weight:850;font-size:.98rem;transition:opacity .15s}
|
| 217 |
+
.hero-action:hover{opacity:.9}.hero-action.terminal{background:#1e1e2e;color:#cdd6f4;border:1px solid #45475a}.hero-action.env{background:#312e81;color:#eef2ff;border:1px solid #6366f1}
|
| 218 |
+
.overview{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin-bottom:10px}
|
| 219 |
+
.tile{border:1px solid var(--line);background:var(--panel);border-radius:11px;padding:18px;min-height:124px;display:flex;flex-direction:column;gap:10px}
|
| 220 |
+
.tile.ok{border-color:rgba(34,197,94,.22)}.tile.warn{border-color:rgba(245,197,66,.24)}.tile.off{border-color:rgba(251,113,133,.28)}
|
| 221 |
+
.tile-head{display:flex;align-items:center;justify-content:space-between;gap:12px}
|
| 222 |
+
.tile-title{color:var(--muted);font-size:.67rem;letter-spacing:.18em;text-transform:uppercase;font-weight:850}
|
| 223 |
+
.tile-dot{width:7px;height:7px;border-radius:50%;background:var(--line)}
|
| 224 |
+
.tile.ok .tile-dot{background:var(--good)}.tile.warn .tile-dot{background:var(--warn)}.tile.off .tile-dot{background:var(--bad)}
|
| 225 |
+
.tile-value{font-size:1.12rem;font-weight:850;overflow-wrap:anywhere}.tile-detail{color:var(--soft);line-height:1.45;font-size:.83rem}
|
| 226 |
+
.tile-meta{color:var(--muted);line-height:1.4;font-size:.75rem;margin-top:auto;overflow-wrap:anywhere}
|
| 227 |
+
code{background:#232234;border:1px solid #34324c;border-radius:6px;padding:2px 6px;color:var(--text);font-size:.9em}
|
| 228 |
+
.badge{display:inline-flex;align-items:center;width:max-content;border:1px solid var(--line);border-radius:999px;padding:5px 10px;font-size:.72rem;font-weight:850;line-height:1;text-transform:uppercase}
|
| 229 |
+
.badge.ok{color:var(--good);border-color:rgba(34,197,94,.34);background:rgba(34,197,94,.11)}
|
| 230 |
+
.badge.warn{color:var(--warn);border-color:rgba(245,197,66,.34);background:rgba(245,197,66,.11)}
|
| 231 |
+
.badge.off{color:var(--bad);border-color:rgba(251,113,133,.34);background:rgba(251,113,133,.11)}
|
| 232 |
+
.badge.neutral{color:var(--soft)}
|
| 233 |
+
footer{color:var(--muted);text-align:center;font-size:.74rem;margin-top:18px}
|
| 234 |
+
@media(max-width:700px){.overview{grid-template-columns:1fr}main{width:min(100% - 22px,720px);padding-top:28px}.btn-row{flex-direction:column}}
|
| 235 |
+
</style></head><body><main>
|
| 236 |
+
<header><h1>🦞 HuggingClaw</h1><div class="subtitle">OpenClaw Gateway</div></header>
|
| 237 |
+
<div class="btn-row">
|
| 238 |
+
<a class="hero-action" data-space-link="app" href="${APP_BASE}/">Open Control UI →</a>
|
| 239 |
+
${JUPYTER_ENABLED ? `<a class="hero-action terminal" data-space-link="terminal" href="${JUPYTER_BASE}/">💻 Open Terminal →</a>` : ""}
|
| 240 |
+
<a class="hero-action env" data-space-link="env-builder" href="/env-builder">⚙️ Env Builder →</a>
|
| 241 |
+
</div>
|
| 242 |
+
<section class="overview">${tilesHtml}</section>
|
| 243 |
+
<footer>Built by <a href="https://github.com/somratpro" target="_blank" rel="noopener noreferrer" style="color:inherit;text-decoration:none">@somratpro</a>${JUPYTER_ENABLED ? " · Terminal by JupyterLab" : ""}<br><span>Public Spaces open via <code>.hf.space</code> directly. Private Spaces require the <a href="${HF_SPACE_URL || "#"}" target="_blank" rel="noopener noreferrer" style="color:inherit">Hugging Face App tab</a> for the authenticated session${HF_SPACE_URL ? ` — or share <code>huggingface.co/spaces/${SPACE_ID}</code>` : ""}.</span></footer>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
</main>
|
| 245 |
<script>
|
| 246 |
+
document.querySelectorAll('.local-time').forEach(el=>{const d=new Date(el.getAttribute('data-iso'));if(!isNaN(d))el.textContent='At '+d.toLocaleTimeString()});
|
| 247 |
+
const inEmbeddedApp = (() => { try { return window.top !== window.self; } catch { return true; } })();
|
| 248 |
+
const isDirectHfSpaceHost = /\.hf\.space$/i.test(window.location.hostname);
|
| 249 |
+
const HF_SPACE_URL = ${JSON.stringify(HF_SPACE_URL)};
|
| 250 |
+
const SPACE_IS_PRIVATE = ${JSON.stringify(SPACE_IS_PRIVATE)};
|
| 251 |
+
// ── Private Space Guard ──
|
| 252 |
+
// Direct .hf.space access outside the HF App iframe has no valid session cookie
|
| 253 |
+
// for private spaces — HF CDN returns 404 before the request reaches the container.
|
| 254 |
+
// Redirect users to huggingface.co/spaces/... which authenticates them properly.
|
| 255 |
+
if (SPACE_IS_PRIVATE && isDirectHfSpaceHost && !inEmbeddedApp && HF_SPACE_URL) {
|
| 256 |
+
const notice = document.createElement('div');
|
| 257 |
+
notice.style.cssText = 'position:fixed;inset:0;display:flex;align-items:center;justify-content:center;background:#08080f;color:#f6f4ff;font-family:sans-serif;flex-direction:column;gap:16px;z-index:9999';
|
| 258 |
+
notice.innerHTML = '<span style="font-size:1.1rem">🔒 Private Space — Redirecting…</span><a href="' + HF_SPACE_URL + '" style="color:#a5b4fc;font-size:.85rem">Click here if not redirected</a>';
|
| 259 |
+
document.body.appendChild(notice);
|
| 260 |
+
setTimeout(() => { window.location.replace(HF_SPACE_URL); }, 300);
|
| 261 |
+
}
|
| 262 |
+
// If inside the HF App iframe, force new-tab navigation so users can break out
|
| 263 |
+
// to the standalone Space host. Also keep direct .hf.space behavior opening new tabs.
|
| 264 |
+
const openInNewTab = inEmbeddedApp || isDirectHfSpaceHost;
|
| 265 |
+
document.querySelectorAll('a[data-space-link]').forEach((a) => {
|
| 266 |
+
if (openInNewTab) {
|
| 267 |
+
a.setAttribute('target', '_blank');
|
| 268 |
+
a.setAttribute('rel', 'noopener noreferrer');
|
| 269 |
+
} else {
|
| 270 |
+
a.removeAttribute('target');
|
| 271 |
+
a.removeAttribute('rel');
|
| 272 |
+
}
|
| 273 |
+
});
|
| 274 |
+
</script>
|
| 275 |
+
</body></html>`;
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
function renderPrivateRedirect(targetUrl) {
|
| 279 |
+
const safeUrl = escapeHtml(targetUrl);
|
| 280 |
+
return `<!doctype html><html lang="en"><head>
|
| 281 |
+
<meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/>
|
| 282 |
+
<meta http-equiv="refresh" content="3;url=${safeUrl}"/>
|
| 283 |
+
<title>HuggingClaw — Private Space</title>
|
| 284 |
+
<style>
|
| 285 |
+
:root{color-scheme:dark}
|
| 286 |
+
body{margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;
|
| 287 |
+
font-family:Inter,ui-sans-serif,system-ui,-apple-system,sans-serif;
|
| 288 |
+
background:#08080f;color:#f6f4ff;text-align:center;padding:24px}
|
| 289 |
+
.card{border:1px solid #26243a;background:#12111b;border-radius:14px;padding:36px 32px;max-width:440px}
|
| 290 |
+
h1{margin:0 0 12px;font-size:1.5rem}
|
| 291 |
+
p{color:#b8b3d7;line-height:1.6;margin:0 0 24px}
|
| 292 |
+
.btn{display:inline-flex;align-items:center;justify-content:center;
|
| 293 |
+
background:#fff;color:#000;font-weight:850;font-size:.95rem;
|
| 294 |
+
border-radius:8px;padding:12px 28px;text-decoration:none;transition:opacity .15s}
|
| 295 |
+
.btn:hover{opacity:.85}
|
| 296 |
+
.sub{color:#7f7a9e;font-size:.78rem;margin-top:16px}
|
| 297 |
+
</style></head><body>
|
| 298 |
+
<div class="card">
|
| 299 |
+
<h1>🔒 Private Space</h1>
|
| 300 |
+
<p>This HuggingFace Space is private. You need to be logged in to <strong>huggingface.co</strong> to access it.<br><br>Redirecting you now…</p>
|
| 301 |
+
<a class="btn" href="${safeUrl}">Open on Hugging Face →</a>
|
| 302 |
+
<div class="sub">Redirecting in 3 seconds…</div>
|
| 303 |
+
</div>
|
| 304 |
+
<script>
|
| 305 |
+
// Immediate redirect if JS available — don't wait for meta refresh
|
| 306 |
+
setTimeout(() => { window.location.replace(${JSON.stringify(targetUrl)}); }, 100);
|
| 307 |
</script>
|
| 308 |
+
</body></html>`;
|
|
|
|
| 309 |
}
|
| 310 |
|
| 311 |
+
function renderEnvBuilder() {
|
| 312 |
+
try {
|
| 313 |
+
return fs.readFileSync(require("path").join(__dirname, "env-builder.html"), "utf8");
|
| 314 |
+
} catch (exc) {
|
| 315 |
+
return `<!doctype html><title>Env Builder unavailable</title><pre>${escapeHtml(exc.message)}</pre>`;
|
| 316 |
+
}
|
| 317 |
+
}
|
| 318 |
|
| 319 |
+
// ── Generic proxy ──
|
| 320 |
+
function proxiedPath(url, { stripPrefix = "" } = {}) {
|
| 321 |
+
if (!stripPrefix) return url.pathname + url.search;
|
| 322 |
+
if (url.pathname === stripPrefix) return "/" + url.search;
|
| 323 |
+
if (url.pathname.startsWith(stripPrefix + "/")) {
|
| 324 |
+
return url.pathname.slice(stripPrefix.length) + url.search;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 325 |
}
|
| 326 |
+
return url.pathname + url.search;
|
| 327 |
+
}
|
| 328 |
|
| 329 |
+
function rewriteProxyHeaders(headers, { publicPrefix = "", targetHost = "", targetPort = "" } = {}) {
|
| 330 |
+
const next = { ...headers };
|
| 331 |
+
|
| 332 |
+
// Keep browser redirects inside the public HF Space path. Backends may emit
|
| 333 |
+
// root-relative redirects ("/login") or absolute redirects pointing at their
|
| 334 |
+
// internal listener ("http://127.0.0.1:8888/..."). Both break from a browser
|
| 335 |
+
// if we do not normalize them back to the public mount path.
|
| 336 |
+
if (publicPrefix && typeof next.location === "string") {
|
| 337 |
+
try {
|
| 338 |
+
const internalOrigins = new Set([
|
| 339 |
+
"http://huggingclaw.local",
|
| 340 |
+
`http://${targetHost}:${targetPort}`,
|
| 341 |
+
`http://localhost:${targetPort}`,
|
| 342 |
+
`http://127.0.0.1:${targetPort}`,
|
| 343 |
+
]);
|
| 344 |
+
const location = new URL(next.location, "http://huggingclaw.local");
|
| 345 |
+
if (internalOrigins.has(location.origin)) {
|
| 346 |
+
let path = location.pathname;
|
| 347 |
+
if (path !== publicPrefix && !path.startsWith(publicPrefix + "/")) {
|
| 348 |
+
path = publicPrefix + (path.startsWith("/") ? path : `/${path}`);
|
| 349 |
+
}
|
| 350 |
+
next.location = path + location.search + location.hash;
|
| 351 |
+
}
|
| 352 |
+
} catch {}
|
| 353 |
}
|
| 354 |
|
| 355 |
+
return next;
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
function sendServiceUnavailable(res) {
|
| 359 |
+
if (!res.headersSent) {
|
| 360 |
+
res.writeHead(503, { "Content-Type": "application/json" });
|
| 361 |
+
res.end(JSON.stringify({ status: "starting", message: "Service is initializing… please wait." }));
|
| 362 |
+
} else {
|
| 363 |
+
res.end();
|
|
|
|
|
|
|
|
|
|
| 364 |
}
|
| 365 |
+
}
|
| 366 |
|
| 367 |
+
function proxyHTTP(req, res, targetHost, targetPort, options = {}) {
|
| 368 |
+
const url = parseRequestUrl(req.url);
|
| 369 |
+
const headers = {
|
| 370 |
...req.headers,
|
| 371 |
+
host: `${targetHost}:${targetPort}`,
|
| 372 |
"x-forwarded-for": req.socket.remoteAddress,
|
| 373 |
"x-forwarded-host": req.headers.host,
|
| 374 |
"x-forwarded-proto": "https",
|
| 375 |
+
"x-forwarded-prefix": options.publicPrefix || "",
|
| 376 |
};
|
| 377 |
|
| 378 |
+
const canReplayRequest = req.method === "GET" || req.method === "HEAD";
|
| 379 |
+
const proxyOnce = (path, retryOn404) => {
|
| 380 |
+
const pr = http.request({ hostname: targetHost, port: targetPort, path, method: req.method, headers }, (pres) => {
|
| 381 |
+
if (canReplayRequest && retryOn404 && pres.statusCode === 404 && options.stripPrefix) {
|
| 382 |
+
pres.resume();
|
| 383 |
+
return proxyOnce(proxiedPath(url, { stripPrefix: options.stripPrefix }), false);
|
| 384 |
+
}
|
| 385 |
+
res.writeHead(pres.statusCode, rewriteProxyHeaders(pres.headers, { ...options, targetHost, targetPort }));
|
| 386 |
+
pres.pipe(res);
|
| 387 |
+
pres.on("error", () => res.end());
|
| 388 |
+
});
|
| 389 |
+
req.on("error", () => pr.destroy());
|
| 390 |
+
res.on("error", () => pr.destroy());
|
| 391 |
+
pr.on("error", () => sendServiceUnavailable(res));
|
| 392 |
+
req.pipe(pr);
|
| 393 |
+
};
|
|
|
|
| 394 |
|
| 395 |
+
// First try the public path as-is because OpenClaw and JupyterLab are both
|
| 396 |
+
// configured with base paths. If a backend still returns 404, retry with the
|
| 397 |
+
// mount prefix stripped; that covers images built before the base-path config
|
| 398 |
+
// took effect and avoids the common HF Spaces "404 at /app or /terminal" trap.
|
| 399 |
+
proxyOnce(url.pathname + url.search, !!options.retryWithoutPrefixOn404);
|
| 400 |
+
}
|
| 401 |
|
| 402 |
+
// ── HTTP server ──
|
| 403 |
+
const server = http.createServer(async (req, res) => {
|
| 404 |
+
const { pathname } = parseRequestUrl(req.url);
|
|
|
|
| 405 |
|
| 406 |
+
if (pathname === "/health") {
|
| 407 |
+
const gatewayReady = await probePort(GATEWAY_HOST, GATEWAY_PORT, "/health");
|
| 408 |
+
res.writeHead(gatewayReady ? 200 : 503, { "Content-Type": "application/json" });
|
| 409 |
+
return res.end(JSON.stringify({ status: gatewayReady ? "ok" : "degraded", gatewayReady, uptime: formatUptime(Date.now() - startTime), sync: getSyncStatus(), keepalive: getKeepaliveStatus() }));
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
if (pathname === "/status") {
|
| 413 |
+
const [gatewayReady, jupyterReady] = await Promise.all([
|
| 414 |
+
probePort(GATEWAY_HOST, GATEWAY_PORT, "/health"),
|
| 415 |
+
JUPYTER_ENABLED ? probePort(JUPYTER_HOST, JUPYTER_PORT, `${JUPYTER_BASE}/login`) : Promise.resolve(false),
|
| 416 |
+
]);
|
| 417 |
+
res.writeHead(200, { "Content-Type": "application/json" });
|
| 418 |
+
return res.end(JSON.stringify({ model: LLM_MODEL, uptime: formatUptime(Date.now() - startTime), gatewayReady, jupyterReady, sync: getSyncStatus(), whatsapp: readGuardianStatus(), keepalive: getKeepaliveStatus() }));
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
// Private space redirect — send users to the authenticated HF Spaces page.
|
| 422 |
+
// Works for both direct .hf.space links AND programmatic shares.
|
| 423 |
+
if (pathname === "/hf-redirect" || pathname === "/hf-redirect/") {
|
| 424 |
+
if (HF_SPACE_URL) {
|
| 425 |
+
res.writeHead(302, { Location: HF_SPACE_URL, "Cache-Control": "no-store" });
|
| 426 |
+
return res.end();
|
| 427 |
}
|
| 428 |
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
| 429 |
+
return res.end("SPACE_ID not configured.");
|
| 430 |
+
}
|
| 431 |
+
|
| 432 |
+
// ── Private Space Guard (server-side) ──
|
| 433 |
+
// Triggers automatically when SPACE_IS_PRIVATE=true (detected via HF API at startup).
|
| 434 |
+
// Only intercepts browser navigation (Accept: text/html) — API calls, assets,
|
| 435 |
+
// and WebSocket upgrades pass through untouched.
|
| 436 |
+
// /health and /status are always exempt so uptime monitors keep working.
|
| 437 |
+
const isHtmlRequest = (req.headers.accept || "").includes("text/html");
|
| 438 |
+
const isDirectHfSpaceRequest = SPACE_IS_PRIVATE &&
|
| 439 |
+
HF_SPACE_URL &&
|
| 440 |
+
isHtmlRequest &&
|
| 441 |
+
typeof req.headers.host === "string" &&
|
| 442 |
+
req.headers.host.endsWith(".hf.space");
|
| 443 |
|
| 444 |
+
if (pathname === "/env-builder" || pathname === "/env-builder/") {
|
| 445 |
+
if (isDirectHfSpaceRequest) {
|
| 446 |
+
res.writeHead(200, { "Content-Type": "text/html" });
|
| 447 |
+
return res.end(renderPrivateRedirect(HF_SPACE_URL));
|
| 448 |
+
}
|
| 449 |
+
res.writeHead(200, { "Content-Type": "text/html" });
|
| 450 |
+
return res.end(renderEnvBuilder());
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
if (pathname === "/env-builder.js") {
|
| 454 |
+
try {
|
| 455 |
+
const js = fs.readFileSync(require("path").join(__dirname, "env-builder.js"), "utf8");
|
| 456 |
+
res.writeHead(200, { "Content-Type": "application/javascript" });
|
| 457 |
+
return res.end(js);
|
| 458 |
+
} catch (exc) {
|
| 459 |
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
| 460 |
+
return res.end(`env-builder.js not found: ${exc.message}`);
|
| 461 |
+
}
|
| 462 |
+
}
|
| 463 |
+
|
| 464 |
+
if (pathname === "/" || pathname === "/dashboard") {
|
| 465 |
+
if (isDirectHfSpaceRequest) {
|
| 466 |
+
res.writeHead(200, { "Content-Type": "text/html" });
|
| 467 |
+
return res.end(renderPrivateRedirect(HF_SPACE_URL));
|
| 468 |
+
}
|
| 469 |
+
const [gatewayReady, jupyterReady] = await Promise.all([
|
| 470 |
+
probePort(GATEWAY_HOST, GATEWAY_PORT, "/health"),
|
| 471 |
+
JUPYTER_ENABLED ? probePort(JUPYTER_HOST, JUPYTER_PORT, `${JUPYTER_BASE}/login`) : Promise.resolve(false),
|
| 472 |
+
]);
|
| 473 |
+
res.writeHead(200, { "Content-Type": "text/html" });
|
| 474 |
+
return res.end(renderDashboard({ uptimeHuman: formatUptime(Date.now() - startTime), gatewayReady, jupyterReady, sync: getSyncStatus(), whatsapp: readGuardianStatus(), keepalive: getKeepaliveStatus() }));
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
+
// JupyterLab terminal
|
| 478 |
+
if (pathname === JUPYTER_BASE || pathname.startsWith(JUPYTER_BASE + "/")) {
|
| 479 |
+
if (!JUPYTER_ENABLED) {
|
| 480 |
+
res.writeHead(404, { "Content-Type": "application/json" });
|
| 481 |
+
return res.end(JSON.stringify({ status: "disabled", message: "JupyterLab terminal is disabled. Set DEV_MODE=true to enable /terminal/." }));
|
| 482 |
+
}
|
| 483 |
+
if (isDirectHfSpaceRequest) {
|
| 484 |
+
res.writeHead(200, { "Content-Type": "text/html" });
|
| 485 |
+
return res.end(renderPrivateRedirect(HF_SPACE_URL));
|
| 486 |
+
}
|
| 487 |
+
return proxyHTTP(req, res, JUPYTER_HOST, JUPYTER_PORT, {
|
| 488 |
+
publicPrefix: JUPYTER_BASE,
|
| 489 |
+
// Jupyter is started with --ServerApp.base_url=/terminal/, so keep the
|
| 490 |
+
// /terminal prefix when proxying. Stripping it breaks static/theme URLs.
|
| 491 |
+
stripPrefix: "",
|
| 492 |
+
retryWithoutPrefixOn404: false,
|
| 493 |
+
});
|
| 494 |
+
}
|
| 495 |
+
|
| 496 |
+
// OpenClaw Control UI mounted under /app. Retry without the mount prefix on
|
| 497 |
+
// 404 so deployments keep working across OpenClaw basePath behavior changes.
|
| 498 |
+
if (pathname === APP_BASE || pathname.startsWith(APP_BASE + "/")) {
|
| 499 |
+
if (isDirectHfSpaceRequest) {
|
| 500 |
+
res.writeHead(200, { "Content-Type": "text/html" });
|
| 501 |
+
return res.end(renderPrivateRedirect(HF_SPACE_URL));
|
| 502 |
+
}
|
| 503 |
+
return proxyHTTP(req, res, GATEWAY_HOST, GATEWAY_PORT, {
|
| 504 |
+
publicPrefix: APP_BASE,
|
| 505 |
+
stripPrefix: APP_BASE,
|
| 506 |
+
retryWithoutPrefixOn404: true,
|
| 507 |
+
});
|
| 508 |
+
}
|
| 509 |
+
|
| 510 |
+
// Favicon — serve a minimal inline SVG so browsers don't proxy to the gateway
|
| 511 |
+
if (pathname === "/favicon.ico" || pathname === "/favicon.svg") {
|
| 512 |
+
const svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><text y=".9em" font-size="90">🦞</text></svg>';
|
| 513 |
+
res.writeHead(200, { "Content-Type": "image/svg+xml", "Cache-Control": "public, max-age=86400" });
|
| 514 |
+
return res.end(svg);
|
| 515 |
+
}
|
| 516 |
+
|
| 517 |
+
// OpenClaw gateway API/static fallback (everything else)
|
| 518 |
+
if (isDirectHfSpaceRequest) {
|
| 519 |
+
res.writeHead(200, { "Content-Type": "text/html" });
|
| 520 |
+
return res.end(renderPrivateRedirect(HF_SPACE_URL));
|
| 521 |
+
}
|
| 522 |
+
proxyHTTP(req, res, GATEWAY_HOST, GATEWAY_PORT);
|
| 523 |
});
|
| 524 |
|
| 525 |
+
// ── WebSocket upgrade (JupyterLab kernels + terminals need this) ──
|
| 526 |
server.on("upgrade", (req, socket, head) => {
|
| 527 |
+
const { pathname, search } = parseRequestUrl(req.url);
|
| 528 |
+
const isJupyter = JUPYTER_ENABLED && (pathname === JUPYTER_BASE || pathname.startsWith(JUPYTER_BASE + "/"));
|
| 529 |
+
const isApp = pathname === APP_BASE || pathname.startsWith(APP_BASE + "/");
|
| 530 |
+
const [targetHost, targetPort] = isJupyter ? [JUPYTER_HOST, JUPYTER_PORT] : [GATEWAY_HOST, GATEWAY_PORT];
|
| 531 |
+
const publicPrefix = isJupyter ? JUPYTER_BASE : isApp ? APP_BASE : "";
|
| 532 |
+
const targetPath = pathname + search;
|
| 533 |
+
|
| 534 |
+
const ps = net.connect(targetPort, targetHost, () => {
|
| 535 |
+
ps.write(`${req.method} ${targetPath} HTTP/${req.httpVersion}\r\n`);
|
| 536 |
+
ps.write(`Host: ${targetHost}:${targetPort}\r\n`);
|
| 537 |
+
ps.write(`X-Forwarded-For: ${req.socket.remoteAddress || ""}\r\n`);
|
| 538 |
+
ps.write(`X-Forwarded-Host: ${req.headers.host || ""}\r\n`);
|
| 539 |
+
ps.write("X-Forwarded-Proto: https\r\n");
|
| 540 |
+
if (publicPrefix) ps.write(`X-Forwarded-Prefix: ${publicPrefix}\r\n`);
|
| 541 |
for (let i = 0; i < req.rawHeaders.length; i += 2) {
|
| 542 |
+
const header = req.rawHeaders[i];
|
| 543 |
+
const lower = header.toLowerCase();
|
| 544 |
+
if (["host", "x-forwarded-for", "x-forwarded-host", "x-forwarded-proto", "x-forwarded-prefix"].includes(lower)) continue;
|
| 545 |
+
ps.write(`${header}: ${req.rawHeaders[i + 1]}\r\n`);
|
| 546 |
}
|
| 547 |
+
ps.write("\r\n");
|
| 548 |
+
if (head && head.length) ps.write(head);
|
| 549 |
+
ps.pipe(socket).pipe(ps);
|
| 550 |
});
|
| 551 |
+
ps.on("error", () => socket.destroy());
|
| 552 |
+
ps.on("close", () => socket.destroy());
|
| 553 |
+
socket.on("error", () => ps.destroy());
|
| 554 |
+
socket.on("close", () => ps.destroy());
|
| 555 |
});
|
| 556 |
|
| 557 |
server.timeout = 0;
|
| 558 |
server.keepAliveTimeout = 65000;
|
| 559 |
+
server.on("error", (err) => console.error(`[health-server] Server error:`, err));
|
| 560 |
server.listen(PORT, "0.0.0.0", () =>
|
| 561 |
+
console.log(`🦞 HuggingClaw :${PORT} → Gateway :${GATEWAY_PORT}${JUPYTER_ENABLED ? ` | Terminal :${JUPYTER_PORT} at ${JUPYTER_BASE}/` : " | Terminal disabled"}`),
|
|
|
|
|
|
|
| 562 |
);
|
iframe-fix.cjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
process.on('uncaughtException', function(err) {
|
| 2 |
if (err.code === 'EPIPE') return;
|
|
|
|
| 3 |
throw err;
|
| 4 |
});
|
| 5 |
/**
|
|
|
|
| 1 |
process.on('uncaughtException', function(err) {
|
| 2 |
if (err.code === 'EPIPE') return;
|
| 3 |
+
if (err.code === 'ECONNRESET' || err.code === 'ENOTCONN' || err.code === 'ERR_STREAM_DESTROYED') return;
|
| 4 |
throw err;
|
| 5 |
});
|
| 6 |
/**
|
jupyter-devdata-sync.py
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import os, shutil, socket, sys, tempfile, time
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
HF_TOKEN = os.environ.get("HF_TOKEN", "").strip()
|
| 8 |
+
HF_USERNAME = os.environ.get("HF_USERNAME", "").strip() or os.environ.get("SPACE_AUTHOR_NAME", "").strip()
|
| 9 |
+
DATASET_NAME = os.environ.get("DEVDATA_DATASET_NAME", "").strip() or "huggingclaw-devdata"
|
| 10 |
+
BACKUP_DATASET_NAME = os.environ.get("BACKUP_DATASET_NAME", "").strip() or os.environ.get("BACKUP_DATASET", "").strip() or "huggingclaw-backup"
|
| 11 |
+
JUPYTER_ROOT = Path(os.environ.get("JUPYTER_ROOT_DIR", "/home/node")).resolve()
|
| 12 |
+
INTERVAL = int((os.environ.get("DEVDATA_SYNC_INTERVAL", "").strip() or "180"))
|
| 13 |
+
# BUG FIX #5: Respect max file size so giant files don't stall uploads.
|
| 14 |
+
# Matches the 50 MB ceiling in openclaw-sync.py; override with DEVDATA_MAX_FILE_BYTES.
|
| 15 |
+
MAX_FILE_SIZE_BYTES = int(
|
| 16 |
+
(os.environ.get("DEVDATA_MAX_FILE_BYTES", "").strip() or str(50 * 1024 * 1024))
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
def is_true(value):
|
| 20 |
+
return str(value).strip().lower() in {"1", "true", "yes", "on"}
|
| 21 |
+
|
| 22 |
+
ENABLE = is_true(os.environ.get("DEVDATA", "on"))
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def classify_error(exc: Exception) -> str:
|
| 26 |
+
msg = str(exc).lower()
|
| 27 |
+
if isinstance(exc, PermissionError) or "permission denied" in msg:
|
| 28 |
+
return "filesystem-permission"
|
| 29 |
+
if any(k in msg for k in ("connection error", "fetch failed", "timeout", "temporarily unavailable", "network")):
|
| 30 |
+
return "network-provider"
|
| 31 |
+
if "unsafe" in msg or "malware" in msg or "security" in msg:
|
| 32 |
+
return "safety-scan"
|
| 33 |
+
return "general"
|
| 34 |
+
|
| 35 |
+
# BUG FIX #4: ".local/share/Trash" in the original EXCLUDE set was a
|
| 36 |
+
# multi-component path string that was never matched because parts-based
|
| 37 |
+
# lookup compares individual directory names. Added "Trash" as a standalone
|
| 38 |
+
# component so any path with a "Trash" segment (e.g. .local/share/Trash/*)
|
| 39 |
+
# is correctly skipped during snapshot and restore.
|
| 40 |
+
EXCLUDE = {
|
| 41 |
+
".cache",
|
| 42 |
+
"node_modules",
|
| 43 |
+
".npm",
|
| 44 |
+
".yarn",
|
| 45 |
+
"Trash", # BUG FIX #4: covers .local/share/Trash (was ".local/share/Trash" — never matched)
|
| 46 |
+
".ipynb_checkpoints",
|
| 47 |
+
".openclaw",
|
| 48 |
+
"app",
|
| 49 |
+
"HuggingClaw",
|
| 50 |
+
"HuggingClaw-Workspace",
|
| 51 |
+
"browser-deps",
|
| 52 |
+
# Exclude Python/system package directories — these contain thousands of files
|
| 53 |
+
# (e.g. .local/lib/python3.11/site-packages/) and must not be synced to the
|
| 54 |
+
# HF Dataset. Syncing them causes 10,000+ file fetches on every restore and
|
| 55 |
+
# can restore a broken jsonschema that crashes JupyterLab on boot.
|
| 56 |
+
".local",
|
| 57 |
+
"lib",
|
| 58 |
+
"site-packages",
|
| 59 |
+
"__pycache__",
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def enabled():
|
| 64 |
+
dev = is_true(os.environ.get("DEV_MODE", ""))
|
| 65 |
+
separate_dataset = DATASET_NAME != BACKUP_DATASET_NAME
|
| 66 |
+
if ENABLE and dev and HF_TOKEN and not separate_dataset:
|
| 67 |
+
print("DevData sync disabled: DEVDATA_DATASET_NAME must be separate from BACKUP_DATASET_NAME.")
|
| 68 |
+
return ENABLE and dev and bool(HF_TOKEN) and separate_dataset
|
| 69 |
+
|
| 70 |
+
def validate_jupyter_paths() -> None:
|
| 71 |
+
# JupyterLab theme/settings live under ~/.jupyter and ~/.local/share/jupyter.
|
| 72 |
+
# If these are not writable, settings can appear to "reset" every restart.
|
| 73 |
+
for required in (JUPYTER_ROOT, Path("/home/node/.jupyter"), Path("/home/node/.local/share/jupyter")):
|
| 74 |
+
try:
|
| 75 |
+
required.mkdir(parents=True, exist_ok=True)
|
| 76 |
+
probe = required / ".devdata-write-check"
|
| 77 |
+
probe.write_text("ok", encoding="utf-8")
|
| 78 |
+
probe.unlink(missing_ok=True)
|
| 79 |
+
except Exception as exc:
|
| 80 |
+
kind = classify_error(exc)
|
| 81 |
+
print(f"DevData warning [{kind}]: {required} is not writable; Jupyter settings may not persist ({exc})")
|
| 82 |
+
|
| 83 |
+
def repo_id(api) -> str:
|
| 84 |
+
ns = HF_USERNAME
|
| 85 |
+
if not ns:
|
| 86 |
+
who = api.whoami()
|
| 87 |
+
ns = who.get("name") or who.get("user") or ""
|
| 88 |
+
if not ns:
|
| 89 |
+
raise RuntimeError("Cannot resolve HF namespace for devdata sync")
|
| 90 |
+
return f"{ns}/{DATASET_NAME}"
|
| 91 |
+
|
| 92 |
+
# Filename patterns that must never be synced to a public/private HF Dataset.
|
| 93 |
+
# These are matched against the *name* of each path component (not the full path),
|
| 94 |
+
# so ".env" matches /home/node/.env and /home/node/subdir/.env alike.
|
| 95 |
+
import fnmatch as _fnmatch
|
| 96 |
+
|
| 97 |
+
SECRET_FILENAME_PATTERNS = {
|
| 98 |
+
".env", # dotenv files — almost always contain API keys
|
| 99 |
+
".env.*", # .env.local, .env.production, etc.
|
| 100 |
+
"*secret*", # any file/dir whose name contains "secret"
|
| 101 |
+
"*secrets*",
|
| 102 |
+
"*_secret*",
|
| 103 |
+
"*-secret*",
|
| 104 |
+
"*key*", # private keys, API key files
|
| 105 |
+
"*_key*",
|
| 106 |
+
"*-key*",
|
| 107 |
+
"*token*", # token files
|
| 108 |
+
"*_token*",
|
| 109 |
+
"*-token*",
|
| 110 |
+
"*.pem", # TLS/SSH private keys
|
| 111 |
+
"*.key", # generic key files
|
| 112 |
+
"*.p12", # PKCS#12 bundles
|
| 113 |
+
"*.pfx",
|
| 114 |
+
"credentials", # common credential file names
|
| 115 |
+
"credentials.*",
|
| 116 |
+
".netrc", # stores plaintext passwords
|
| 117 |
+
".htpasswd",
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def _name_is_secret(name: str) -> bool:
|
| 122 |
+
"""Return True if *name* matches any secret-exclusion pattern."""
|
| 123 |
+
name_lower = name.lower()
|
| 124 |
+
return any(_fnmatch.fnmatch(name_lower, pat) for pat in SECRET_FILENAME_PATTERNS)
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def should_skip(p: Path):
|
| 128 |
+
# Skip directories/files in the hard-coded exclude set.
|
| 129 |
+
parts = p.parts
|
| 130 |
+
if any(x in parts for x in EXCLUDE):
|
| 131 |
+
return True
|
| 132 |
+
# Skip any component whose name looks like a secret file/dir.
|
| 133 |
+
return any(_name_is_secret(part) for part in parts)
|
| 134 |
+
|
| 135 |
+
def snapshot(src: Path, dst: Path):
|
| 136 |
+
for p in src.rglob("*"):
|
| 137 |
+
rel = p.relative_to(src)
|
| 138 |
+
if should_skip(rel):
|
| 139 |
+
continue
|
| 140 |
+
if p.is_symlink():
|
| 141 |
+
continue
|
| 142 |
+
target = dst / rel
|
| 143 |
+
if p.is_dir():
|
| 144 |
+
target.mkdir(parents=True, exist_ok=True)
|
| 145 |
+
elif p.is_file():
|
| 146 |
+
# BUG FIX #5: Skip files that exceed the size limit.
|
| 147 |
+
try:
|
| 148 |
+
if p.stat().st_size > MAX_FILE_SIZE_BYTES:
|
| 149 |
+
continue
|
| 150 |
+
except OSError:
|
| 151 |
+
continue
|
| 152 |
+
target.parent.mkdir(parents=True, exist_ok=True)
|
| 153 |
+
try:
|
| 154 |
+
shutil.copy2(p, target)
|
| 155 |
+
except OSError:
|
| 156 |
+
pass
|
| 157 |
+
|
| 158 |
+
def is_jupyter_running(port: int = 8888) -> bool:
|
| 159 |
+
"""Return True if JupyterLab is already listening on *port*.
|
| 160 |
+
|
| 161 |
+
BUG FIX #2 (safety net): restore_once() must never run while JupyterLab
|
| 162 |
+
is active. Overwriting files under JUPYTER_ROOT (runtime/ sockets, lab/
|
| 163 |
+
settings, kernel connection files) while JupyterLab is live corrupts its
|
| 164 |
+
state and causes it to exit within seconds.
|
| 165 |
+
|
| 166 |
+
The primary guard is the --restore / sync separation introduced in
|
| 167 |
+
BUG FIX #3, but this TCP probe stays as a hard backstop for any future
|
| 168 |
+
code path that might call restore_once() unexpectedly.
|
| 169 |
+
"""
|
| 170 |
+
try:
|
| 171 |
+
with socket.create_connection(("127.0.0.1", port), timeout=2):
|
| 172 |
+
return True
|
| 173 |
+
except OSError:
|
| 174 |
+
return False
|
| 175 |
+
|
| 176 |
+
def restore_once(api, rid: str):
|
| 177 |
+
from huggingface_hub.errors import RepositoryNotFoundError
|
| 178 |
+
tmp = Path(tempfile.mkdtemp(prefix="devdata-restore-"))
|
| 179 |
+
try:
|
| 180 |
+
snapshot_download(repo_id=rid, repo_type="dataset", local_dir=str(tmp), local_dir_use_symlinks=False, token=HF_TOKEN)
|
| 181 |
+
for p in tmp.rglob("*"):
|
| 182 |
+
rel = p.relative_to(tmp)
|
| 183 |
+
if should_skip(rel):
|
| 184 |
+
continue
|
| 185 |
+
if str(rel) == ".gitattributes":
|
| 186 |
+
continue
|
| 187 |
+
target = JUPYTER_ROOT / rel
|
| 188 |
+
if p.is_dir():
|
| 189 |
+
target.mkdir(parents=True, exist_ok=True)
|
| 190 |
+
elif p.is_file():
|
| 191 |
+
target.parent.mkdir(parents=True, exist_ok=True)
|
| 192 |
+
try:
|
| 193 |
+
shutil.copy2(p, target)
|
| 194 |
+
except OSError as exc:
|
| 195 |
+
kind = classify_error(exc)
|
| 196 |
+
print(f"DevData restore skip [{kind}] (cannot write {target}): {exc}")
|
| 197 |
+
print(f"DevData restored from {rid}")
|
| 198 |
+
except RepositoryNotFoundError:
|
| 199 |
+
print(f"DevData dataset not found yet: {rid}")
|
| 200 |
+
except Exception as exc:
|
| 201 |
+
kind = classify_error(exc)
|
| 202 |
+
print(f"DevData restore warning [{kind}]: {exc}")
|
| 203 |
+
finally:
|
| 204 |
+
shutil.rmtree(tmp, ignore_errors=True)
|
| 205 |
+
|
| 206 |
+
def prune_remote_deleted_files(api, rid: str, snapshot_dir: Path) -> None:
|
| 207 |
+
"""BUG FIX #6: Delete from the HF dataset any files the user deleted
|
| 208 |
+
locally. Without this, deleted files re-appear on the next Space restart
|
| 209 |
+
because restore_once() copies everything in the dataset back to disk.
|
| 210 |
+
Mirrors the prune_remote_deleted_files() logic in openclaw-sync.py.
|
| 211 |
+
"""
|
| 212 |
+
try:
|
| 213 |
+
local_files = {
|
| 214 |
+
p.relative_to(snapshot_dir).as_posix()
|
| 215 |
+
for p in snapshot_dir.rglob("*")
|
| 216 |
+
if p.is_file()
|
| 217 |
+
}
|
| 218 |
+
remote_files = list(api.list_repo_files(repo_id=rid, repo_type="dataset"))
|
| 219 |
+
stale = [f for f in remote_files if f not in local_files and f != ".gitattributes"]
|
| 220 |
+
if stale:
|
| 221 |
+
api.delete_files(
|
| 222 |
+
delete_patterns=stale,
|
| 223 |
+
repo_id=rid,
|
| 224 |
+
repo_type="dataset",
|
| 225 |
+
commit_message=f"DevData prune {len(stale)} deleted file(s) {time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())}",
|
| 226 |
+
)
|
| 227 |
+
print(f"DevData pruned {len(stale)} deleted file(s) from {rid}")
|
| 228 |
+
except Exception as exc:
|
| 229 |
+
kind = classify_error(exc)
|
| 230 |
+
print(f"DevData prune warning [{kind}]: {exc}")
|
| 231 |
+
|
| 232 |
+
def sync_loop(api, rid: str):
|
| 233 |
+
while True:
|
| 234 |
+
tmp = Path(tempfile.mkdtemp(prefix="devdata-snap-"))
|
| 235 |
+
try:
|
| 236 |
+
snapshot(JUPYTER_ROOT, tmp)
|
| 237 |
+
upload_folder(
|
| 238 |
+
folder_path=str(tmp),
|
| 239 |
+
repo_id=rid,
|
| 240 |
+
repo_type="dataset",
|
| 241 |
+
token=HF_TOKEN,
|
| 242 |
+
commit_message=f"DevData sync {time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())}",
|
| 243 |
+
ignore_patterns=[".git/*", ".git"],
|
| 244 |
+
)
|
| 245 |
+
print(f"DevData synced to {rid}")
|
| 246 |
+
# BUG FIX #6: Prune files deleted locally so they don't reappear on restore.
|
| 247 |
+
prune_remote_deleted_files(api, rid, tmp)
|
| 248 |
+
except Exception as exc:
|
| 249 |
+
kind = classify_error(exc)
|
| 250 |
+
print(f"DevData sync warning [{kind}]: {exc}")
|
| 251 |
+
finally:
|
| 252 |
+
shutil.rmtree(tmp, ignore_errors=True)
|
| 253 |
+
time.sleep(INTERVAL)
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
if __name__ == "__main__":
|
| 257 |
+
if not enabled():
|
| 258 |
+
print("DevData sync disabled.")
|
| 259 |
+
raise SystemExit(0)
|
| 260 |
+
|
| 261 |
+
from huggingface_hub import HfApi, upload_folder, snapshot_download
|
| 262 |
+
from huggingface_hub.errors import RepositoryNotFoundError
|
| 263 |
+
|
| 264 |
+
api = HfApi(token=HF_TOKEN)
|
| 265 |
+
rid = repo_id(api)
|
| 266 |
+
try:
|
| 267 |
+
api.repo_info(repo_id=rid, repo_type="dataset")
|
| 268 |
+
except RepositoryNotFoundError:
|
| 269 |
+
api.create_repo(repo_id=rid, repo_type="dataset", private=True)
|
| 270 |
+
|
| 271 |
+
# ── BUG FIX #3: Restore must happen BEFORE JupyterLab starts ──────────
|
| 272 |
+
# The original code always called restore_once() here, but start.sh starts
|
| 273 |
+
# JupyterLab long before the gateway is ready and this script is launched.
|
| 274 |
+
# That made restore_once() ALWAYS run while JupyterLab was live, which
|
| 275 |
+
# overwrote its runtime/ sockets and settings → JupyterLab died.
|
| 276 |
+
#
|
| 277 |
+
# Fix: start.sh now calls `python3 jupyter-devdata-sync.py --restore`
|
| 278 |
+
# BEFORE starting JupyterLab. That --restore invocation does the restore
|
| 279 |
+
# and exits. This background invocation (no --restore flag) skips straight
|
| 280 |
+
# to sync_loop so it never touches files while JupyterLab is running.
|
| 281 |
+
#
|
| 282 |
+
# BUG FIX #2 (safety net): If JupyterLab is somehow already running when
|
| 283 |
+
# this code path is reached, abort restore to avoid corrupting its state.
|
| 284 |
+
if "--restore" in sys.argv:
|
| 285 |
+
# Synchronous restore mode — called by start.sh before JupyterLab.
|
| 286 |
+
validate_jupyter_paths()
|
| 287 |
+
restore_once(api, rid)
|
| 288 |
+
raise SystemExit(0)
|
| 289 |
+
|
| 290 |
+
# Normal background sync mode — no restore; go straight to upload loop.
|
| 291 |
+
validate_jupyter_paths()
|
| 292 |
+
if is_jupyter_running():
|
| 293 |
+
print("DevData: background sync started (JupyterLab is live, restore already done by --restore).")
|
| 294 |
+
else:
|
| 295 |
+
# Fallback: JupyterLab not detected. Should not normally happen
|
| 296 |
+
# because start.sh calls --restore before starting JupyterLab and then
|
| 297 |
+
# waits for the gateway before launching this background process.
|
| 298 |
+
# Log a warning and proceed to sync; do NOT restore to avoid racing
|
| 299 |
+
# with a JupyterLab that may be in the middle of starting up.
|
| 300 |
+
print("DevData: WARNING — JupyterLab not detected on port 8888. Skipping restore to be safe; starting sync loop.")
|
| 301 |
+
|
| 302 |
+
sync_loop(api, rid)
|
login.html
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "page.html" %}
|
| 2 |
+
|
| 3 |
+
{% block stylesheet %}
|
| 4 |
+
{% endblock %}
|
| 5 |
+
|
| 6 |
+
{% block site %}
|
| 7 |
+
<div id="jupyter-main-app" class="container" style="text-align:center; max-width: 760px; margin-top: 40px;">
|
| 8 |
+
<img src="https://huggingface.co/front/assets/huggingface_logo-noborder.svg" alt="Hugging Face Logo" style="max-width: 120px; margin-bottom: 24px;">
|
| 9 |
+
<h3>HuggingClaw Terminal</h3>
|
| 10 |
+
<h4>Welcome to JupyterLab</h4>
|
| 11 |
+
<h5>The default token is <span style="color:orange;">huggingface</span></h5>
|
| 12 |
+
<p style="color:#666;">This terminal is mounted at <code>/terminal/</code> inside the same Hugging Face Space as the OpenClaw UI.</p>
|
| 13 |
+
|
| 14 |
+
{% if login_available %}
|
| 15 |
+
<div class="row" style="display:flex; justify-content:center; margin-top:24px;">
|
| 16 |
+
<div class="navbar col-sm-8">
|
| 17 |
+
<div class="navbar-inner">
|
| 18 |
+
<div class="container">
|
| 19 |
+
<div class="center-nav">
|
| 20 |
+
<form action="{{base_url}}login?next={{next}}" method="post" class="navbar-form pull-left">
|
| 21 |
+
{{ xsrf_form_html() | safe }}
|
| 22 |
+
{% if token_available %}
|
| 23 |
+
<label for="password_input"><strong>{% trans %}Jupyter token <span title="This is the secret you set up when deploying your JupyterLab terminal">ⓘ</span> {% endtrans %}</strong></label>
|
| 24 |
+
{% else %}
|
| 25 |
+
<label for="password_input"><strong>{% trans %}Jupyter password:{% endtrans %}</strong></label>
|
| 26 |
+
{% endif %}
|
| 27 |
+
<input type="password" name="password" id="password_input" class="form-control">
|
| 28 |
+
<button type="submit" class="btn btn-default" id="login_submit">{% trans %}Log in{% endtrans %}</button>
|
| 29 |
+
</form>
|
| 30 |
+
</div>
|
| 31 |
+
</div>
|
| 32 |
+
</div>
|
| 33 |
+
</div>
|
| 34 |
+
</div>
|
| 35 |
+
{% else %}
|
| 36 |
+
<p>{% trans %}No login available, you shouldn't be seeing this page.{% endtrans %}</p>
|
| 37 |
+
{% endif %}
|
| 38 |
+
|
| 39 |
+
<h5 style="margin-top:28px;"><a href="/dashboard">Back to HuggingClaw dashboard</a></h5>
|
| 40 |
+
<p>This login page is based on the Hugging Face JupyterLab Space template.</p>
|
| 41 |
+
|
| 42 |
+
{% if message %}
|
| 43 |
+
<div class="row">
|
| 44 |
+
{% for key in message %}
|
| 45 |
+
<div class="message {{key}}">
|
| 46 |
+
{{message[key]}}
|
| 47 |
+
</div>
|
| 48 |
+
{% endfor %}
|
| 49 |
+
</div>
|
| 50 |
+
{% endif %}
|
| 51 |
+
{% if token_available %}
|
| 52 |
+
{% block token_message %}
|
| 53 |
+
{% endblock token_message %}
|
| 54 |
+
{% endif %}
|
| 55 |
+
</div>
|
| 56 |
+
{% endblock %}
|
| 57 |
+
|
| 58 |
+
{% block script %}
|
| 59 |
+
{% endblock %}
|
multi-provider-key-rotator.cjs
CHANGED
|
@@ -166,6 +166,33 @@ const PROVIDERS = [
|
|
| 166 |
envPlural: 'HUGGINGFACE_HUB_TOKENS', // plural variant
|
| 167 |
envSingular:'HUGGINGFACE_HUB_TOKEN',
|
| 168 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
];
|
| 170 |
|
| 171 |
// ─── Key loading ─────────────────────────────────────────────────────────────
|
|
@@ -298,10 +325,9 @@ function patchFetch() {
|
|
| 298 |
const headers = init.headers || (input && input.headers) || undefined;
|
| 299 |
const patchedHeaders = setAuthHeader(headers, key);
|
| 300 |
init = { ...init, headers: patchedHeaders };
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
}
|
| 305 |
}
|
| 306 |
}
|
| 307 |
}
|
|
@@ -335,13 +361,17 @@ function patchHttpModule(mod) {
|
|
| 335 |
? { ...options, path: `${u.pathname}${u.search}` }
|
| 336 |
: u.toString();
|
| 337 |
} else if (typeof options === 'string' || options instanceof URL) {
|
|
|
|
|
|
|
| 338 |
const u = new URL(String(options));
|
|
|
|
| 339 |
args[0] = {
|
| 340 |
protocol: u.protocol,
|
| 341 |
hostname: u.hostname,
|
| 342 |
port: u.port,
|
| 343 |
path: `${u.pathname}${u.search}`,
|
| 344 |
-
|
|
|
|
| 345 |
};
|
| 346 |
} else if (options && typeof options === 'object') {
|
| 347 |
args[0] = { ...options, headers: setAuthHeader(options.headers, key) };
|
|
|
|
| 166 |
envPlural: 'HUGGINGFACE_HUB_TOKENS', // plural variant
|
| 167 |
envSingular:'HUGGINGFACE_HUB_TOKEN',
|
| 168 |
},
|
| 169 |
+
{
|
| 170 |
+
name: 'venice',
|
| 171 |
+
hostname: /(?:^|\.)api\.venice\.ai$/i,
|
| 172 |
+
envPlural: 'VENICE_API_KEYS',
|
| 173 |
+
envSingular:'VENICE_API_KEY',
|
| 174 |
+
},
|
| 175 |
+
{
|
| 176 |
+
name: 'github-copilot',
|
| 177 |
+
hostname: /(?:^|\.)api\.githubcopilot\.com$/i,
|
| 178 |
+
envPlural: 'COPILOT_GITHUB_TOKENS',
|
| 179 |
+
envSingular:'COPILOT_GITHUB_TOKEN',
|
| 180 |
+
},
|
| 181 |
+
{
|
| 182 |
+
name: 'qianfan',
|
| 183 |
+
// Baidu Qianfan / ERNIE
|
| 184 |
+
hostname: /(?:^|\.)(?:aip|qianfan)\.baidubce\.com$/i,
|
| 185 |
+
envPlural: 'QIANFAN_API_KEYS',
|
| 186 |
+
envSingular:'QIANFAN_API_KEY',
|
| 187 |
+
},
|
| 188 |
+
{
|
| 189 |
+
name: 'modelstudio',
|
| 190 |
+
// Aliyun DashScope / Qwen (both qwen/* and modelstudio/* prefixes)
|
| 191 |
+
hostname: /(?:^|\.)dashscope\.aliyuncs\.com$/i,
|
| 192 |
+
envPlural: 'MODELSTUDIO_API_KEYS',
|
| 193 |
+
envSingular:'MODELSTUDIO_API_KEY',
|
| 194 |
+
},
|
| 195 |
+
|
| 196 |
];
|
| 197 |
|
| 198 |
// ─── Key loading ─────────────────────────────────────────────────────────────
|
|
|
|
| 325 |
const headers = init.headers || (input && input.headers) || undefined;
|
| 326 |
const patchedHeaders = setAuthHeader(headers, key);
|
| 327 |
init = { ...init, headers: patchedHeaders };
|
| 328 |
+
// NOTE: new Request(input, {headers}) yahan nahi karte — Request clone karna
|
| 329 |
+
// body stream ko disturb kar deta hai → UND_ERR_INVALID_ARG on POST requests.
|
| 330 |
+
// init.headers fetch spec ke mutabiq Request ke headers ko override kar deta hai.
|
|
|
|
| 331 |
}
|
| 332 |
}
|
| 333 |
}
|
|
|
|
| 361 |
? { ...options, path: `${u.pathname}${u.search}` }
|
| 362 |
: u.toString();
|
| 363 |
} else if (typeof options === 'string' || options instanceof URL) {
|
| 364 |
+
// Convert string/URL to options object and inject auth header.
|
| 365 |
+
// Also preserve any extra options passed as args[1] (3-arg form of http.request).
|
| 366 |
const u = new URL(String(options));
|
| 367 |
+
const extraOpts = (args[1] && typeof args[1] === 'object' && typeof args[1].on !== 'function') ? args[1] : {};
|
| 368 |
args[0] = {
|
| 369 |
protocol: u.protocol,
|
| 370 |
hostname: u.hostname,
|
| 371 |
port: u.port,
|
| 372 |
path: `${u.pathname}${u.search}`,
|
| 373 |
+
...extraOpts,
|
| 374 |
+
headers: setAuthHeader(extraOpts.headers, key),
|
| 375 |
};
|
| 376 |
} else if (options && typeof options === 'object') {
|
| 377 |
args[0] = { ...options, headers: setAuthHeader(options.headers, key) };
|
openclaw-sync.py
CHANGED
|
@@ -53,8 +53,11 @@ CONFIG_SETTLE_SECONDS = max(
|
|
| 53 |
HF_TOKEN = os.environ.get("HF_TOKEN", "").strip()
|
| 54 |
HF_USERNAME = os.environ.get("HF_USERNAME", "").strip()
|
| 55 |
SPACE_AUTHOR_NAME = os.environ.get("SPACE_AUTHOR_NAME", "").strip()
|
| 56 |
-
BACKUP_DATASET_NAME = os.environ.get("BACKUP_DATASET_NAME", "
|
| 57 |
-
|
|
|
|
|
|
|
|
|
|
| 58 |
|
| 59 |
EXCLUDED_SYNC_DIRS = {
|
| 60 |
"node_modules", ".git", "__pycache__", ".venv", "venv",
|
|
|
|
| 53 |
HF_TOKEN = os.environ.get("HF_TOKEN", "").strip()
|
| 54 |
HF_USERNAME = os.environ.get("HF_USERNAME", "").strip()
|
| 55 |
SPACE_AUTHOR_NAME = os.environ.get("SPACE_AUTHOR_NAME", "").strip()
|
| 56 |
+
BACKUP_DATASET_NAME = os.environ.get("BACKUP_DATASET_NAME", "").strip() or os.environ.get("BACKUP_DATASET", "").strip() or "huggingclaw-backup"
|
| 57 |
+
def is_true(value):
|
| 58 |
+
return str(value).strip().lower() in {"1", "true", "yes", "on"}
|
| 59 |
+
|
| 60 |
+
WHATSAPP_ENABLED = is_true(os.environ.get("WHATSAPP_ENABLED", ""))
|
| 61 |
|
| 62 |
EXCLUDED_SYNC_DIRS = {
|
| 63 |
"node_modules", ".git", "__pycache__", ".venv", "venv",
|
start.sh
CHANGED
|
@@ -8,7 +8,70 @@ umask 0077
|
|
| 8 |
# ════════════════════════════════════════════════════════════════
|
| 9 |
|
| 10 |
# ── Startup Banner ──
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
@@ -21,7 +84,21 @@ WHATSAPP_ENABLED_CONFIGURED=false
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
if [ -n "${SPACE_HOST:-}" ]; then
|
| 26 |
OPENCLAW_CONSOLE_LOG_LEVEL="${OPENCLAW_CONSOLE_LOG_LEVEL:-warn}"
|
| 27 |
OPENCLAW_FILE_LOG_LEVEL="${OPENCLAW_FILE_LOG_LEVEL:-info}"
|
|
@@ -39,7 +116,7 @@ else
|
|
| 39 |
fi
|
| 40 |
echo ""
|
| 41 |
echo " ╔══════════════════════════════════════════╗"
|
| 42 |
-
echo " ║
|
| 43 |
echo " ╚══════════════════════════════════════════╝"
|
| 44 |
echo ""
|
| 45 |
|
|
@@ -57,7 +134,7 @@ fi
|
|
| 57 |
if [ -n "$ERRORS" ]; then
|
| 58 |
echo "Missing required secrets:"
|
| 59 |
echo -e "$ERRORS"
|
| 60 |
-
echo "Add them in HF Spaces → Settings → Secrets"
|
| 61 |
exit 1
|
| 62 |
fi
|
| 63 |
|
|
@@ -175,6 +252,14 @@ promote_first_pool_key "CEREBRAS_API_KEY" "CEREBRAS_API_KEYS"
|
|
| 175 |
promote_first_pool_key "VENICE_API_KEY" "VENICE_API_KEYS"
|
| 176 |
promote_first_pool_key "SYNTHETIC_API_KEY" "SYNTHETIC_API_KEYS"
|
| 177 |
promote_first_pool_key "COPILOT_GITHUB_TOKEN" "COPILOT_GITHUB_TOKENS"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
promote_first_pool_key "HUGGINGFACE_HUB_TOKEN" "HUGGINGFACE_HUB_TOKENS"
|
| 179 |
|
| 180 |
# Compatibility aliases for Google provider secrets some users already have.
|
|
@@ -200,6 +285,11 @@ export NPM_CONFIG_PREFIX="${NPM_CONFIG_PREFIX:-/home/node/.local}"
|
|
| 200 |
export npm_config_prefix="$NPM_CONFIG_PREFIX"
|
| 201 |
export PYTHONUSERBASE="${PYTHONUSERBASE:-/home/node/.local}"
|
| 202 |
export DEBIAN_FRONTEND="${DEBIAN_FRONTEND:-noninteractive}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
STARTUP_FILE="/home/node/.openclaw/workspace/startup.sh"
|
| 204 |
|
| 205 |
# ── Restore workspace/state from HF Dataset ──
|
|
@@ -433,6 +523,8 @@ fi
|
|
| 433 |
# plugins that the Control UI/dashboard needs to render correctly
|
| 434 |
# on HF Spaces. Without these the UI shows blank panels.
|
| 435 |
# telegram/whatsapp/browser/acpx are added conditionally below.
|
|
|
|
|
|
|
| 436 |
# DENY: lmstudio crashes on boot when no local server is reachable;
|
| 437 |
# xai PLUGIN (separate from the xai model PROVIDER) is broken in
|
| 438 |
# current OpenClaw releases and prevents gateway start. Disabling
|
|
@@ -452,20 +544,17 @@ if [ "$WHATSAPP_ENABLED_NORMALIZED" = "true" ]; then
|
|
| 452 |
fi
|
| 453 |
|
| 454 |
# Apply plugin allow/deny + per-entry toggles in one jq pass.
|
| 455 |
-
ACPX_DISABLED=false
|
| 456 |
-
if [ "$ACP_PLUGIN_MODE" = "disabled" ]; then ACPX_DISABLED=true; fi
|
| 457 |
BROWSER_DISABLED=true
|
| 458 |
if [ "$BROWSER_SHOULD_ENABLE" = "true" ]; then BROWSER_DISABLED=false; fi
|
| 459 |
|
| 460 |
CONFIG_JSON=$(jq \
|
| 461 |
--argjson allow "$PLUGIN_ALLOW_JSON" \
|
| 462 |
-
--argjson acpxDisabled "$ACPX_DISABLED" \
|
| 463 |
--argjson browserDisabled "$BROWSER_DISABLED" \
|
| 464 |
'.plugins.allow = $allow
|
| 465 |
| .plugins.deny = ["lmstudio","xai"]
|
| 466 |
| .plugins.entries.lmstudio.enabled = false
|
| 467 |
| .plugins.entries.xai.enabled = false
|
| 468 |
-
| (
|
| 469 |
| (if $browserDisabled then
|
| 470 |
.plugins.entries.browser.enabled = false | .browser.enabled = false
|
| 471 |
else . end)' <<<"$CONFIG_JSON")
|
|
@@ -513,6 +602,24 @@ if [ -n "${ALLOWED_ORIGINS:-}" ]; then
|
|
| 513 |
CONFIG_JSON=$(echo "$CONFIG_JSON" | jq ".gateway.controlUi.allowedOrigins += $ORIGINS_JSON | .gateway.controlUi.allowedOrigins |= unique")
|
| 514 |
fi
|
| 515 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 516 |
# Telegram (supports multiple user IDs, comma-separated)
|
| 517 |
if [ -n "${TELEGRAM_BOT_TOKEN:-}" ]; then
|
| 518 |
CONFIG_JSON=$(echo "$CONFIG_JSON" | jq '.plugins.entries.telegram = {"enabled": true}')
|
|
@@ -525,12 +632,12 @@ if [ -n "${TELEGRAM_BOT_TOKEN:-}" ]; then
|
|
| 525 |
# Force ipv4 for Telegram specifically as HF IPv6 often times out
|
| 526 |
export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--dns-result-order=ipv4first"
|
| 527 |
|
| 528 |
-
CONFIG_JSON=$(echo "$CONFIG_JSON" | jq --arg token "$CLEAN_TG_TOKEN" --arg proxy_url "$
|
| 529 |
.channels.telegram.enabled = true
|
| 530 |
| .channels.telegram.botToken = $token
|
| 531 |
| .channels.telegram.commands.native = false
|
| 532 |
| .channels.telegram.timeoutSeconds = 60
|
| 533 |
-
| (if $proxy_url != "" then .channels.telegram.apiRoot = $proxy_url else .
|
| 534 |
| .channels.telegram.retry = {
|
| 535 |
"attempts": 5,
|
| 536 |
"minDelayMs": 800,
|
|
@@ -594,7 +701,7 @@ if [ -f "$EXISTING_CONFIG" ]; then
|
|
| 594 |
| .channels = ((.channels // {}) * ($desired.channels // {}))
|
| 595 |
| .plugins.allow = (((.plugins.allow // []) + ($desired.plugins.allow // [])) | unique)
|
| 596 |
| .plugins.deny = (((.plugins.deny // []) + ($desired.plugins.deny // [])) | unique)
|
| 597 |
-
| .plugins.entries = ((
|
| 598 |
| if $whatsappEnabled then
|
| 599 |
($desired.channels.whatsapp // {"dmPolicy": "pairing"}) as $desiredWhatsapp
|
| 600 |
| .plugins.entries.whatsapp.enabled = true
|
|
@@ -649,8 +756,34 @@ fi
|
|
| 649 |
if [ -n "${CLOUDFLARE_PROXY_URL:-}" ]; then
|
| 650 |
echo "Proxy : ${CLOUDFLARE_PROXY_URL}"
|
| 651 |
fi
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 652 |
if [ -n "${SPACE_HOST:-}" ]; then
|
| 653 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 654 |
fi
|
| 655 |
echo ""
|
| 656 |
|
|
@@ -709,6 +842,105 @@ export LLM_MODEL="$LLM_MODEL"
|
|
| 709 |
node /home/node/app/health-server.js &
|
| 710 |
HEALTH_PID=$!
|
| 711 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 712 |
if [ -n "${CLOUDFLARE_WORKERS_TOKEN:-}" ]; then
|
| 713 |
echo "Setting up Cloudflare KeepAlive monitor..."
|
| 714 |
python3 /home/node/app/cloudflare-keepalive-setup.py || true
|
|
@@ -728,8 +960,14 @@ export NPM_CONFIG_PREFIX="${NPM_CONFIG_PREFIX:-/home/node/.local}"
|
|
| 728 |
export npm_config_prefix="$NPM_CONFIG_PREFIX"
|
| 729 |
export PYTHONUSERBASE="${PYTHONUSERBASE:-/home/node/.local}"
|
| 730 |
export DEBIAN_FRONTEND="${DEBIAN_FRONTEND:-noninteractive}"
|
|
|
|
|
|
|
|
|
|
| 731 |
STARTUP_FILE="/home/node/.openclaw/workspace/startup.sh"
|
| 732 |
_hc_append() {
|
|
|
|
|
|
|
|
|
|
| 733 |
local line="$*"
|
| 734 |
mkdir -p "$(dirname "$STARTUP_FILE")"
|
| 735 |
touch "$STARTUP_FILE"
|
|
@@ -756,6 +994,28 @@ _hc_append_cmd() {
|
|
| 756 |
_hc_append "$cmd"
|
| 757 |
fi
|
| 758 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 759 |
_hc_allow_openclaw_plugins() {
|
| 760 |
local config="/home/node/.openclaw/openclaw.json"
|
| 761 |
[ -f "$config" ] || return 0
|
|
@@ -807,7 +1067,7 @@ apt-get() {
|
|
| 807 |
_hc_apt_install "$@"
|
| 808 |
local rc=$?
|
| 809 |
if [ $rc -eq 0 ]; then
|
| 810 |
-
_hc_append_cmd "sudo apt-get update && sudo apt-get install -y" "$@"
|
| 811 |
fi
|
| 812 |
return $rc
|
| 813 |
;;
|
|
@@ -834,7 +1094,7 @@ apt() {
|
|
| 834 |
_hc_apt_install "$@"
|
| 835 |
local rc=$?
|
| 836 |
if [ $rc -eq 0 ]; then
|
| 837 |
-
_hc_append_cmd "sudo apt-get update && sudo apt-get install -y" "$@"
|
| 838 |
fi
|
| 839 |
return $rc
|
| 840 |
;;
|
|
@@ -856,32 +1116,65 @@ apt() {
|
|
| 856 |
}
|
| 857 |
pip() {
|
| 858 |
if [ "${1:-}" = "install" ] && [ -z "${VIRTUAL_ENV:-}" ] && ! _hc_has_arg --user "$@" && ! _hc_has_arg --prefix "$@"; then
|
| 859 |
-
command pip install --user "${@:2}"
|
| 860 |
else
|
| 861 |
command pip "$@"
|
| 862 |
fi
|
| 863 |
local rc=$?
|
| 864 |
-
|
|
|
|
|
|
|
|
|
|
| 865 |
_hc_append_cmd "python3 -m pip install --user" "${@:2}"
|
| 866 |
fi
|
| 867 |
return $rc
|
| 868 |
}
|
| 869 |
pip3() {
|
| 870 |
if [ "${1:-}" = "install" ] && [ -z "${VIRTUAL_ENV:-}" ] && ! _hc_has_arg --user "$@" && ! _hc_has_arg --prefix "$@"; then
|
| 871 |
-
command pip3 install --user "${@:2}"
|
| 872 |
else
|
| 873 |
command pip3 "$@"
|
| 874 |
fi
|
| 875 |
local rc=$?
|
| 876 |
-
if [ $rc -eq 0 ] && [ "${1:-}" = "install" ]
|
|
|
|
|
|
|
| 877 |
_hc_append_cmd "python3 -m pip install --user" "${@:2}"
|
| 878 |
fi
|
| 879 |
return $rc
|
| 880 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 881 |
npm() {
|
| 882 |
command npm "$@"
|
| 883 |
local rc=$?
|
| 884 |
-
if [ $rc -eq 0 ] && [ "${1:-}" = "install" ] && { [ "${2:-}" = "-g" ] || [ "${2:-}" = "--global" ]; }; then
|
| 885 |
_hc_append_cmd "npm install -g" "${@:3}"
|
| 886 |
fi
|
| 887 |
return $rc
|
|
@@ -889,15 +1182,37 @@ npm() {
|
|
| 889 |
openclaw() {
|
| 890 |
command openclaw "$@"
|
| 891 |
local rc=$?
|
| 892 |
-
if [ $rc -eq 0 ] && [ "${1:-}" = "plugins" ] && [ "${2:-}" = "install" ]; then
|
| 893 |
_hc_allow_openclaw_plugins "${@:3}"
|
| 894 |
_hc_append_cmd "openclaw plugins install" "${@:3}"
|
| 895 |
fi
|
| 896 |
return $rc
|
| 897 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 898 |
BASHRC
|
| 899 |
cat > /home/node/.profile <<'PROFILE'
|
| 900 |
-
[ -f ~/.bashrc ] && . ~/.bashrc
|
| 901 |
PROFILE
|
| 902 |
echo "Shell capture wrappers ready."
|
| 903 |
|
|
@@ -935,7 +1250,7 @@ hc_run_startup_command() {
|
|
| 935 |
|
| 936 |
echo "[startup:${source_label}] $command_text"
|
| 937 |
set +e
|
| 938 |
-
bash -lc "$command_text"
|
| 939 |
local rc=$?
|
| 940 |
set -e
|
| 941 |
if [ "$rc" -eq 0 ]; then
|
|
@@ -959,6 +1274,7 @@ hc_run_startup_script() {
|
|
| 959 |
# Load HuggingClaw's install wrappers for env-provided scripts too, so
|
| 960 |
# `apt install`, `pip install`, `npm install -g`, and OpenClaw plugin
|
| 961 |
# installs behave the same way as they do in the interactive shell.
|
|
|
|
| 962 |
echo '[ -f /home/node/.bashrc ] && . /home/node/.bashrc'
|
| 963 |
printf '%s\n' "$script_text"
|
| 964 |
} > "$script_file"
|
|
@@ -1076,7 +1392,7 @@ fi
|
|
| 1076 |
if [ -n "${HUGGINGCLAW_PIP_PACKAGES:-}" ]; then
|
| 1077 |
echo "Installing Python packages from HUGGINGCLAW_PIP_PACKAGES..."
|
| 1078 |
read -r -a HC_PIP_PACKAGES <<< "$HUGGINGCLAW_PIP_PACKAGES"
|
| 1079 |
-
if python3 -m pip install --user "${HC_PIP_PACKAGES[@]}"; then
|
| 1080 |
echo "HUGGINGCLAW_PIP_PACKAGES install complete."
|
| 1081 |
else
|
| 1082 |
HC_STARTUP_FAILURES=$((HC_STARTUP_FAILURES + 1))
|
|
@@ -1165,6 +1481,38 @@ sync_before_gateway_restart() {
|
|
| 1165 |
echo "Warning: could not sync settled state before gateway restart"
|
| 1166 |
}
|
| 1167 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1168 |
start_background_sync_once() {
|
| 1169 |
[ -n "${HF_TOKEN:-}" ] || return 0
|
| 1170 |
|
|
@@ -1172,7 +1520,7 @@ start_background_sync_once() {
|
|
| 1172 |
return 0
|
| 1173 |
fi
|
| 1174 |
|
| 1175 |
-
python3 -u /home/node/app/openclaw-sync.py loop &
|
| 1176 |
SYNC_LOOP_PID=$!
|
| 1177 |
}
|
| 1178 |
|
|
@@ -1189,6 +1537,30 @@ start_guardian_once() {
|
|
| 1189 |
}
|
| 1190 |
|
| 1191 |
while true; do
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1192 |
echo "Launching OpenClaw gateway on port 7860..."
|
| 1193 |
|
| 1194 |
GATEWAY_ARGS=(gateway run --port 7860 --bind lan)
|
|
@@ -1238,6 +1610,7 @@ while true; do
|
|
| 1238 |
# config edits can make OpenClaw exit/reload, and the gateway loop below will
|
| 1239 |
# relaunch it without rerunning all startup code.
|
| 1240 |
start_background_sync_once
|
|
|
|
| 1241 |
|
| 1242 |
set +e
|
| 1243 |
wait "$GATEWAY_PID"
|
|
|
|
| 8 |
# ════════════════════════════════════════════════════════════════
|
| 9 |
|
| 10 |
# ── Startup Banner ──
|
| 11 |
+
trim_var() {
|
| 12 |
+
# Trim leading/trailing whitespace from a value.
|
| 13 |
+
printf '%s' "$1" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//'
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
hc_is_true() {
|
| 17 |
+
case "$(printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]')" in
|
| 18 |
+
1|true|yes|on) return 0 ;;
|
| 19 |
+
*) return 1 ;;
|
| 20 |
+
esac
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
load_env_bundle() {
|
| 24 |
+
# HUGGINGCLAW_ENV_BUNDLE is a single base64url-encoded JSON object generated
|
| 25 |
+
# by /env-builder. Existing individual env vars win over bundled values.
|
| 26 |
+
local bundle="${HUGGINGCLAW_ENV_BUNDLE:-${ENV_BUNDLE:-}}"
|
| 27 |
+
[ -n "$bundle" ] || return 0
|
| 28 |
+
eval "$(HUGGINGCLAW_ENV_BUNDLE="$bundle" python3 - <<'PYBUNDLE'
|
| 29 |
+
import base64, json, os, re, shlex, sys
|
| 30 |
+
|
| 31 |
+
raw = os.environ.get("HUGGINGCLAW_ENV_BUNDLE", "").strip()
|
| 32 |
+
try:
|
| 33 |
+
if raw.startswith("{"):
|
| 34 |
+
data = json.loads(raw)
|
| 35 |
+
else:
|
| 36 |
+
padded = raw + "=" * (-len(raw) % 4)
|
| 37 |
+
data = json.loads(base64.urlsafe_b64decode(padded.encode()).decode())
|
| 38 |
+
if not isinstance(data, dict):
|
| 39 |
+
raise ValueError("bundle must decode to a JSON object")
|
| 40 |
+
for key, value in data.items():
|
| 41 |
+
if not re.fullmatch(r"[A-Z_][A-Z0-9_]*", str(key)):
|
| 42 |
+
continue
|
| 43 |
+
if str(key) in {"HUGGINGCLAW_ENV_BUNDLE", "ENV_BUNDLE"}:
|
| 44 |
+
continue
|
| 45 |
+
if os.environ.get(str(key), ""):
|
| 46 |
+
continue
|
| 47 |
+
if value is None or isinstance(value, (dict, list)):
|
| 48 |
+
continue
|
| 49 |
+
print(f"export {key}={shlex.quote(str(value))}")
|
| 50 |
+
except Exception as exc:
|
| 51 |
+
print(f"Warning: invalid HUGGINGCLAW_ENV_BUNDLE ignored: {exc}", file=sys.stderr)
|
| 52 |
+
PYBUNDLE
|
| 53 |
+
)"
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
load_env_bundle
|
| 57 |
+
|
| 58 |
+
# Normalize core env values so accidental surrounding spaces in HF Variables
|
| 59 |
+
# do not block updates or cause stale comparisons/merges.
|
| 60 |
+
LLM_MODEL="$(trim_var "${LLM_MODEL:-}")"
|
| 61 |
+
GATEWAY_TOKEN="$(trim_var "${GATEWAY_TOKEN:-}")"
|
| 62 |
+
OPENCLAW_PASSWORD="$(trim_var "${OPENCLAW_PASSWORD:-}")"
|
| 63 |
+
LLM_API_KEY="$(trim_var "${LLM_API_KEY:-}")"
|
| 64 |
+
CLOUDFLARE_PROXY_URL="$(trim_var "${CLOUDFLARE_PROXY_URL:-}")"
|
| 65 |
+
|
| 66 |
OPENCLAW_VERSION="${OPENCLAW_VERSION:-latest}"
|
| 67 |
+
APP_BASE="$(trim_var "${APP_BASE:-/app}")"
|
| 68 |
+
JUPYTER_BASE="$(trim_var "${JUPYTER_BASE:-/terminal}")"
|
| 69 |
+
PORT="$(trim_var "${PORT:-7861}")"
|
| 70 |
+
GATEWAY_PORT="$(trim_var "${GATEWAY_PORT:-7860}")"
|
| 71 |
+
JUPYTER_PORT="$(trim_var "${JUPYTER_PORT:-8888}")"
|
| 72 |
+
BACKUP_DATASET_NAME="$(trim_var "${BACKUP_DATASET_NAME:-${BACKUP_DATASET:-huggingclaw-backup}}")"
|
| 73 |
+
SPACE_AUTHOR_NAME="$(trim_var "${SPACE_AUTHOR_NAME:-}")"
|
| 74 |
+
SPACE_HOST="$(trim_var "${SPACE_HOST:-}")"
|
| 75 |
OPENCLAW_APP_DIR="/home/node/.openclaw/openclaw-app"
|
| 76 |
OPENCLAW_RUNTIME_VERSION=""
|
| 77 |
OPENCLAW_FILE_LOG_LEVEL_CONFIGURED=false
|
|
|
|
| 84 |
[ "${WHATSAPP_ENABLED+x}" = "x" ] && WHATSAPP_ENABLED_CONFIGURED=true
|
| 85 |
WHATSAPP_ENABLED="${WHATSAPP_ENABLED:-false}"
|
| 86 |
WHATSAPP_ENABLED_NORMALIZED=$(printf '%s' "$WHATSAPP_ENABLED" | tr '[:upper:]' '[:lower:]')
|
| 87 |
+
DEV_MODE_RAW="${DEV_MODE:-false}"
|
| 88 |
+
DEV_MODE_NORMALIZED=$(printf '%s' "$DEV_MODE_RAW" | tr '[:upper:]' '[:lower:]')
|
| 89 |
+
DEV_MODE_ENABLED=false
|
| 90 |
+
if hc_is_true "$DEV_MODE_NORMALIZED"; then
|
| 91 |
+
DEV_MODE_ENABLED=true
|
| 92 |
+
fi
|
| 93 |
+
SYNC_INTERVAL="$(trim_var "${SYNC_INTERVAL:-180}")"
|
| 94 |
+
DEVDATA_DATASET_NAME="$(trim_var "${DEVDATA_DATASET_NAME:-huggingclaw-devdata}")"
|
| 95 |
+
DEVDATA_SYNC_INTERVAL="$(trim_var "${DEVDATA_SYNC_INTERVAL:-180}")"
|
| 96 |
+
DEVDATA_RAW="$(trim_var "${DEVDATA:-on}")"
|
| 97 |
+
DEVDATA_NORMALIZED=$(printf '%s' "$DEVDATA_RAW" | tr '[:upper:]' '[:lower:]')
|
| 98 |
+
DEVDATA_ENABLED=true
|
| 99 |
+
if ! hc_is_true "$DEVDATA_NORMALIZED"; then
|
| 100 |
+
DEVDATA_ENABLED=false
|
| 101 |
+
fi
|
| 102 |
if [ -n "${SPACE_HOST:-}" ]; then
|
| 103 |
OPENCLAW_CONSOLE_LOG_LEVEL="${OPENCLAW_CONSOLE_LOG_LEVEL:-warn}"
|
| 104 |
OPENCLAW_FILE_LOG_LEVEL="${OPENCLAW_FILE_LOG_LEVEL:-info}"
|
|
|
|
| 116 |
fi
|
| 117 |
echo ""
|
| 118 |
echo " ╔══════════════════════════════════════════╗"
|
| 119 |
+
echo " ║ 🦞 HuggingClaw + 💻 JupyterLab ║"
|
| 120 |
echo " ╚══════════════════════════════════════════╝"
|
| 121 |
echo ""
|
| 122 |
|
|
|
|
| 134 |
if [ -n "$ERRORS" ]; then
|
| 135 |
echo "Missing required secrets:"
|
| 136 |
echo -e "$ERRORS"
|
| 137 |
+
echo "Add them in HF Spaces → Settings → Secrets"
|
| 138 |
exit 1
|
| 139 |
fi
|
| 140 |
|
|
|
|
| 252 |
promote_first_pool_key "VENICE_API_KEY" "VENICE_API_KEYS"
|
| 253 |
promote_first_pool_key "SYNTHETIC_API_KEY" "SYNTHETIC_API_KEYS"
|
| 254 |
promote_first_pool_key "COPILOT_GITHUB_TOKEN" "COPILOT_GITHUB_TOKENS"
|
| 255 |
+
promote_first_pool_key "AI_GATEWAY_API_KEY" "AI_GATEWAY_API_KEYS"
|
| 256 |
+
|
| 257 |
+
# kimi-coding uses Moonshot AI endpoint (api.moonshot.cn).
|
| 258 |
+
# If KIMI_API_KEY is set but MOONSHOT_API_KEY is not, mirror it so the
|
| 259 |
+
# multi-provider-key-rotator (which matches on api.moonshot.cn) injects it.
|
| 260 |
+
if [ -z "${MOONSHOT_API_KEY:-}" ] && [ -n "${KIMI_API_KEY:-}" ]; then
|
| 261 |
+
export MOONSHOT_API_KEY="$KIMI_API_KEY"
|
| 262 |
+
fi
|
| 263 |
promote_first_pool_key "HUGGINGFACE_HUB_TOKEN" "HUGGINGFACE_HUB_TOKENS"
|
| 264 |
|
| 265 |
# Compatibility aliases for Google provider secrets some users already have.
|
|
|
|
| 285 |
export npm_config_prefix="$NPM_CONFIG_PREFIX"
|
| 286 |
export PYTHONUSERBASE="${PYTHONUSERBASE:-/home/node/.local}"
|
| 287 |
export DEBIAN_FRONTEND="${DEBIAN_FRONTEND:-noninteractive}"
|
| 288 |
+
# Show current working directory in terminal prompt (JupyterLab terminals can
|
| 289 |
+
# otherwise display only "$" when PS1 is unset/minimal).
|
| 290 |
+
if [ -z "${PS1:-}" ] || [ "$PS1" = "$ " ]; then
|
| 291 |
+
export PS1='\u@\h:\w\$ '
|
| 292 |
+
fi
|
| 293 |
STARTUP_FILE="/home/node/.openclaw/workspace/startup.sh"
|
| 294 |
|
| 295 |
# ── Restore workspace/state from HF Dataset ──
|
|
|
|
| 523 |
# plugins that the Control UI/dashboard needs to render correctly
|
| 524 |
# on HF Spaces. Without these the UI shows blank panels.
|
| 525 |
# telegram/whatsapp/browser/acpx are added conditionally below.
|
| 526 |
+
# Do not create a disabled acpx entry when the plugin is absent;
|
| 527 |
+
# OpenClaw reports that as a config warning on HF Spaces.
|
| 528 |
# DENY: lmstudio crashes on boot when no local server is reachable;
|
| 529 |
# xai PLUGIN (separate from the xai model PROVIDER) is broken in
|
| 530 |
# current OpenClaw releases and prevents gateway start. Disabling
|
|
|
|
| 544 |
fi
|
| 545 |
|
| 546 |
# Apply plugin allow/deny + per-entry toggles in one jq pass.
|
|
|
|
|
|
|
| 547 |
BROWSER_DISABLED=true
|
| 548 |
if [ "$BROWSER_SHOULD_ENABLE" = "true" ]; then BROWSER_DISABLED=false; fi
|
| 549 |
|
| 550 |
CONFIG_JSON=$(jq \
|
| 551 |
--argjson allow "$PLUGIN_ALLOW_JSON" \
|
|
|
|
| 552 |
--argjson browserDisabled "$BROWSER_DISABLED" \
|
| 553 |
'.plugins.allow = $allow
|
| 554 |
| .plugins.deny = ["lmstudio","xai"]
|
| 555 |
| .plugins.entries.lmstudio.enabled = false
|
| 556 |
| .plugins.entries.xai.enabled = false
|
| 557 |
+
| del(.plugins.entries.acpx)
|
| 558 |
| (if $browserDisabled then
|
| 559 |
.plugins.entries.browser.enabled = false | .browser.enabled = false
|
| 560 |
else . end)' <<<"$CONFIG_JSON")
|
|
|
|
| 602 |
CONFIG_JSON=$(echo "$CONFIG_JSON" | jq ".gateway.controlUi.allowedOrigins += $ORIGINS_JSON | .gateway.controlUi.allowedOrigins |= unique")
|
| 603 |
fi
|
| 604 |
|
| 605 |
+
resolve_telegram_api_root() {
|
| 606 |
+
local candidate="$(trim_var "${CLOUDFLARE_PROXY_URL:-}")"
|
| 607 |
+
if [ -n "$candidate" ]; then
|
| 608 |
+
case "$candidate" in
|
| 609 |
+
http://*|https://*)
|
| 610 |
+
printf '%s' "$candidate"
|
| 611 |
+
return 0
|
| 612 |
+
;;
|
| 613 |
+
*)
|
| 614 |
+
echo "Warning: invalid CLOUDFLARE_PROXY_URL '$candidate' (must start with http:// or https://); falling back to direct Telegram API." >&2
|
| 615 |
+
;;
|
| 616 |
+
esac
|
| 617 |
+
fi
|
| 618 |
+
printf '%s' "https://api.telegram.org"
|
| 619 |
+
}
|
| 620 |
+
TELEGRAM_API_ROOT="$(resolve_telegram_api_root)"
|
| 621 |
+
|
| 622 |
+
|
| 623 |
# Telegram (supports multiple user IDs, comma-separated)
|
| 624 |
if [ -n "${TELEGRAM_BOT_TOKEN:-}" ]; then
|
| 625 |
CONFIG_JSON=$(echo "$CONFIG_JSON" | jq '.plugins.entries.telegram = {"enabled": true}')
|
|
|
|
| 632 |
# Force ipv4 for Telegram specifically as HF IPv6 often times out
|
| 633 |
export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--dns-result-order=ipv4first"
|
| 634 |
|
| 635 |
+
CONFIG_JSON=$(echo "$CONFIG_JSON" | jq --arg token "$CLEAN_TG_TOKEN" --arg proxy_url "$TELEGRAM_API_ROOT" '
|
| 636 |
.channels.telegram.enabled = true
|
| 637 |
| .channels.telegram.botToken = $token
|
| 638 |
| .channels.telegram.commands.native = false
|
| 639 |
| .channels.telegram.timeoutSeconds = 60
|
| 640 |
+
| (if $proxy_url != "" then .channels.telegram.apiRoot = $proxy_url else . end)
|
| 641 |
| .channels.telegram.retry = {
|
| 642 |
"attempts": 5,
|
| 643 |
"minDelayMs": 800,
|
|
|
|
| 701 |
| .channels = ((.channels // {}) * ($desired.channels // {}))
|
| 702 |
| .plugins.allow = (((.plugins.allow // []) + ($desired.plugins.allow // [])) | unique)
|
| 703 |
| .plugins.deny = (((.plugins.deny // []) + ($desired.plugins.deny // [])) | unique)
|
| 704 |
+
| .plugins.entries = ((.plugins.entries // {}) * ($desired.plugins.entries // {}))
|
| 705 |
| if $whatsappEnabled then
|
| 706 |
($desired.channels.whatsapp // {"dmPolicy": "pairing"}) as $desiredWhatsapp
|
| 707 |
| .plugins.entries.whatsapp.enabled = true
|
|
|
|
| 756 |
if [ -n "${CLOUDFLARE_PROXY_URL:-}" ]; then
|
| 757 |
echo "Proxy : ${CLOUDFLARE_PROXY_URL}"
|
| 758 |
fi
|
| 759 |
+
RUNTIME_JUPYTER_ENABLED="$DEV_MODE_ENABLED"
|
| 760 |
+
# Add user bin to PATH for jupyter-lab (installed in Dockerfile when DEV_MODE=true)
|
| 761 |
+
export PATH="$HOME/.local/bin:$PATH"
|
| 762 |
+
|
| 763 |
+
# Runtime install fallback: only attempt if DEV_MODE is enabled but install failed during build
|
| 764 |
+
if [ "$DEV_MODE_ENABLED" = "true" ] && ! python3 -c "import jupyterlab" >/dev/null 2>&1; then
|
| 765 |
+
echo "DEV_MODE enabled but jupyter-lab is missing; attempting runtime install..."
|
| 766 |
+
if python3 -m pip install --user --no-cache-dir --break-system-packages "jupyterlab>=4.2,<5" "tornado>=6.3" "ipywidgets>=8.1"; then
|
| 767 |
+
echo "Runtime Jupyter install complete."
|
| 768 |
+
python3 -c "from pathlib import Path; import shutil, jupyter_server; d=Path(jupyter_server.__file__).parent/'templates'; d.mkdir(parents=True,exist_ok=True); shutil.copyfile('/home/node/app/login.html', d/'login.html')" || true
|
| 769 |
+
else
|
| 770 |
+
echo "WARNING: Runtime Jupyter install failed; disabling terminal for this boot."
|
| 771 |
+
RUNTIME_JUPYTER_ENABLED=false
|
| 772 |
+
fi
|
| 773 |
+
fi
|
| 774 |
+
if [ "$RUNTIME_JUPYTER_ENABLED" = "true" ] && ! python3 -c "import jupyterlab" >/dev/null 2>&1; then
|
| 775 |
+
echo "WARNING: jupyter-lab still unavailable; disabling terminal for this boot."
|
| 776 |
+
RUNTIME_JUPYTER_ENABLED=false
|
| 777 |
+
fi
|
| 778 |
+
export HUGGINGCLAW_JUPYTER_ENABLED="$RUNTIME_JUPYTER_ENABLED"
|
| 779 |
+
|
| 780 |
if [ -n "${SPACE_HOST:-}" ]; then
|
| 781 |
+
if [ "$RUNTIME_JUPYTER_ENABLED" = "true" ]; then
|
| 782 |
+
echo "Routes : /app/ (Control UI), /terminal/ (JupyterLab)"
|
| 783 |
+
else
|
| 784 |
+
echo "Routes : /app/ (Control UI)"
|
| 785 |
+
fi
|
| 786 |
+
echo "Private : open the Hugging Face App tab first; raw https://${SPACE_HOST}/... links can show HF 404 without the embedded Space session."
|
| 787 |
fi
|
| 788 |
echo ""
|
| 789 |
|
|
|
|
| 842 |
node /home/node/app/health-server.js &
|
| 843 |
HEALTH_PID=$!
|
| 844 |
|
| 845 |
+
start_jupyter_once() {
|
| 846 |
+
[ "$RUNTIME_JUPYTER_ENABLED" = "true" ] || return 0
|
| 847 |
+
if [ -n "${JUPYTER_PID:-}" ] && kill -0 "$JUPYTER_PID" 2>/dev/null; then
|
| 848 |
+
return 0
|
| 849 |
+
fi
|
| 850 |
+
|
| 851 |
+
# Security guard: refuse to start JupyterLab with the insecure default token.
|
| 852 |
+
# JupyterLab exposes a full shell — a weak token is equivalent to no auth.
|
| 853 |
+
if [ -z "${JUPYTER_TOKEN:-}" ] || [ "${JUPYTER_TOKEN}" = "huggingface" ]; then
|
| 854 |
+
echo "ERROR: JUPYTER_TOKEN is unset or still set to the insecure default (\"huggingface\")." >&2
|
| 855 |
+
echo " JupyterLab grants full shell access. Set a strong, unique token in your Space secrets." >&2
|
| 856 |
+
echo " Hint: openssl rand -hex 32" >&2
|
| 857 |
+
echo " DEV_MODE active but JupyterLab will NOT start until JUPYTER_TOKEN is changed." >&2
|
| 858 |
+
return 1
|
| 859 |
+
fi
|
| 860 |
+
JUPYTER_ROOT_DIR="${JUPYTER_ROOT_DIR:-/home/node}"
|
| 861 |
+
if [ "$JUPYTER_ROOT_DIR" = "/home/node/.openclaw/workspace" ] && [ "$DEVDATA_ENABLED" = "true" ]; then
|
| 862 |
+
echo "Jupyter root was set to OpenClaw workspace; moving Jupyter root to /home/node/devdata to keep BACKUP and DEVDATA datasets separate."
|
| 863 |
+
JUPYTER_ROOT_DIR="/home/node/devdata"
|
| 864 |
+
fi
|
| 865 |
+
mkdir -p "$JUPYTER_ROOT_DIR"
|
| 866 |
+
export JUPYTER_ROOT_DIR
|
| 867 |
+
if [ "$JUPYTER_ROOT_DIR" != "/home/node/app" ]; then
|
| 868 |
+
if [ -L "$JUPYTER_ROOT_DIR/HuggingClaw" ] || [ ! -e "$JUPYTER_ROOT_DIR/HuggingClaw" ]; then
|
| 869 |
+
ln -sfn /home/node/app "$JUPYTER_ROOT_DIR/HuggingClaw"
|
| 870 |
+
fi
|
| 871 |
+
fi
|
| 872 |
+
if [ "$JUPYTER_ROOT_DIR" != "/home/node/.openclaw/workspace" ]; then
|
| 873 |
+
if [ -L "$JUPYTER_ROOT_DIR/HuggingClaw-Workspace" ] || [ ! -e "$JUPYTER_ROOT_DIR/HuggingClaw-Workspace" ]; then
|
| 874 |
+
ln -sfn /home/node/.openclaw/workspace "$JUPYTER_ROOT_DIR/HuggingClaw-Workspace"
|
| 875 |
+
fi
|
| 876 |
+
fi
|
| 877 |
+
|
| 878 |
+
# Pre-create runtime directory
|
| 879 |
+
mkdir -p "$JUPYTER_ROOT_DIR/.jupyter"
|
| 880 |
+
|
| 881 |
+
echo "DEV_MODE enabled (${DEV_MODE_RAW}) — starting JupyterLab terminal on internal port 8888 (path: /terminal/) with root: $JUPYTER_ROOT_DIR"
|
| 882 |
+
JUPYTER_LOG_FILE="/tmp/jupyterlab.log"
|
| 883 |
+
|
| 884 |
+
# Use explicit Python to avoid PATH issues; set memory-friendly limits
|
| 885 |
+
export PYTHONPATH=""
|
| 886 |
+
python3 -m jupyterlab \
|
| 887 |
+
--ip 127.0.0.1 \
|
| 888 |
+
--port 8888 \
|
| 889 |
+
--no-browser \
|
| 890 |
+
--IdentityProvider.token="$JUPYTER_TOKEN" \
|
| 891 |
+
--ServerApp.base_url=/terminal/ \
|
| 892 |
+
--ServerApp.terminals_enabled=True \
|
| 893 |
+
--ServerApp.terminado_settings='{"shell_command":["/bin/bash","-i"]}' \
|
| 894 |
+
--ServerApp.allow_origin='*' \
|
| 895 |
+
--ServerApp.allow_remote_access=True \
|
| 896 |
+
--ServerApp.trust_xheaders=True \
|
| 897 |
+
--ServerApp.tornado_settings="{'headers': {'Content-Security-Policy': 'frame-ancestors *'}}" \
|
| 898 |
+
--IdentityProvider.cookie_options="{'SameSite': 'None', 'Secure': True}" \
|
| 899 |
+
--ServerApp.disable_check_xsrf=True \
|
| 900 |
+
--LabApp.news_url=None \
|
| 901 |
+
--LabApp.check_for_updates_class=jupyterlab.NeverCheckForUpdate \
|
| 902 |
+
--ServerApp.log_level=WARN \
|
| 903 |
+
--ServerApp.root_dir="$JUPYTER_ROOT_DIR" \
|
| 904 |
+
>> "$JUPYTER_LOG_FILE" 2>&1 &
|
| 905 |
+
JUPYTER_PID=$!
|
| 906 |
+
export JUPYTER_PID
|
| 907 |
+
echo "JupyterLab started (PID: $JUPYTER_PID)"
|
| 908 |
+
}
|
| 909 |
+
|
| 910 |
+
# BUG FIX #3: DevData restore must happen BEFORE JupyterLab starts.
|
| 911 |
+
# The background jupyter-devdata-sync.py process is only launched AFTER the
|
| 912 |
+
# gateway is ready (20-90 s from now). If restore ran there, JupyterLab would
|
| 913 |
+
# already be live and the file writes would corrupt its runtime state → crash.
|
| 914 |
+
# Running --restore here (synchronous, before JupyterLab) solves that.
|
| 915 |
+
if [ "$RUNTIME_JUPYTER_ENABLED" = "true" ] && \
|
| 916 |
+
[ "$DEVDATA_ENABLED" = "true" ] && \
|
| 917 |
+
[ -n "${HF_TOKEN:-}" ] && \
|
| 918 |
+
[ -f "/home/node/app/jupyter-devdata-sync.py" ] && \
|
| 919 |
+
[ "${DEVDATA_DATASET_NAME:-huggingclaw-devdata}" != "${BACKUP_DATASET_NAME:-huggingclaw-backup}" ]; then
|
| 920 |
+
echo "DevData : restoring workspace from ${DEVDATA_DATASET_NAME:-huggingclaw-devdata} (before JupyterLab starts)..."
|
| 921 |
+
python3 /home/node/app/jupyter-devdata-sync.py --restore || \
|
| 922 |
+
echo "DevData : restore warning (non-fatal); continuing startup."
|
| 923 |
+
fi
|
| 924 |
+
|
| 925 |
+
# Fix: reinstall jsonschema AFTER devdata restore — restore can overwrite a broken
|
| 926 |
+
# version from .local/lib/python3.11/site-packages into the workspace, causing
|
| 927 |
+
# JupyterLab to crash with a circular import error on every boot.
|
| 928 |
+
if [ "$DEV_MODE_ENABLED" = "true" ]; then
|
| 929 |
+
if ! python3 -c "import jsonschema" >/dev/null 2>&1; then
|
| 930 |
+
echo "DevData : jsonschema broken after restore — reinstalling (circular import fix)..."
|
| 931 |
+
python3 -m pip install --force-reinstall --no-cache-dir --break-system-packages "jsonschema>=4.0" >/dev/null 2>&1 || true
|
| 932 |
+
echo "DevData : jsonschema reinstall done."
|
| 933 |
+
fi
|
| 934 |
+
fi
|
| 935 |
+
|
| 936 |
+
# 10.5. Start JupyterLab Terminal on internal port 8888 (DEV_MODE only)
|
| 937 |
+
# Accessible via /terminal/ path through the health-server proxy
|
| 938 |
+
if [ "$RUNTIME_JUPYTER_ENABLED" = "true" ]; then
|
| 939 |
+
start_jupyter_once
|
| 940 |
+
else
|
| 941 |
+
echo "Jupyter terminal disabled for this boot (DEV_MODE=${DEV_MODE_RAW})."
|
| 942 |
+
fi
|
| 943 |
+
|
| 944 |
if [ -n "${CLOUDFLARE_WORKERS_TOKEN:-}" ]; then
|
| 945 |
echo "Setting up Cloudflare KeepAlive monitor..."
|
| 946 |
python3 /home/node/app/cloudflare-keepalive-setup.py || true
|
|
|
|
| 960 |
export npm_config_prefix="$NPM_CONFIG_PREFIX"
|
| 961 |
export PYTHONUSERBASE="${PYTHONUSERBASE:-/home/node/.local}"
|
| 962 |
export DEBIAN_FRONTEND="${DEBIAN_FRONTEND:-noninteractive}"
|
| 963 |
+
if [ -z "${PS1:-}" ] || [ "$PS1" = "$ " ]; then
|
| 964 |
+
export PS1="\u@\h:\w\$ "
|
| 965 |
+
fi
|
| 966 |
STARTUP_FILE="/home/node/.openclaw/workspace/startup.sh"
|
| 967 |
_hc_append() {
|
| 968 |
+
if [ "${HUGGINGCLAW_CAPTURE_DISABLE:-0}" = "1" ]; then
|
| 969 |
+
return 0
|
| 970 |
+
fi
|
| 971 |
local line="$*"
|
| 972 |
mkdir -p "$(dirname "$STARTUP_FILE")"
|
| 973 |
touch "$STARTUP_FILE"
|
|
|
|
| 994 |
_hc_append "$cmd"
|
| 995 |
fi
|
| 996 |
}
|
| 997 |
+
_hc_args_without_flags() {
|
| 998 |
+
local out=()
|
| 999 |
+
local arg
|
| 1000 |
+
for arg in "$@"; do
|
| 1001 |
+
case "$arg" in
|
| 1002 |
+
''|-) ;;
|
| 1003 |
+
--*) ;;
|
| 1004 |
+
-*) ;;
|
| 1005 |
+
*) out+=("$arg") ;;
|
| 1006 |
+
esac
|
| 1007 |
+
done
|
| 1008 |
+
printf '%s\n' "${out[@]}"
|
| 1009 |
+
}
|
| 1010 |
+
_hc_has_install_targets() {
|
| 1011 |
+
local item
|
| 1012 |
+
while IFS= read -r item; do
|
| 1013 |
+
[ -n "$item" ] && return 0
|
| 1014 |
+
done <<EOF
|
| 1015 |
+
$(_hc_args_without_flags "$@")
|
| 1016 |
+
EOF
|
| 1017 |
+
return 1
|
| 1018 |
+
}
|
| 1019 |
_hc_allow_openclaw_plugins() {
|
| 1020 |
local config="/home/node/.openclaw/openclaw.json"
|
| 1021 |
[ -f "$config" ] || return 0
|
|
|
|
| 1067 |
_hc_apt_install "$@"
|
| 1068 |
local rc=$?
|
| 1069 |
if [ $rc -eq 0 ]; then
|
| 1070 |
+
_hc_has_install_targets "$@" && _hc_append_cmd "sudo apt-get update && sudo apt-get install -y" "$@"
|
| 1071 |
fi
|
| 1072 |
return $rc
|
| 1073 |
;;
|
|
|
|
| 1094 |
_hc_apt_install "$@"
|
| 1095 |
local rc=$?
|
| 1096 |
if [ $rc -eq 0 ]; then
|
| 1097 |
+
_hc_has_install_targets "$@" && _hc_append_cmd "sudo apt-get update && sudo apt-get install -y" "$@"
|
| 1098 |
fi
|
| 1099 |
return $rc
|
| 1100 |
;;
|
|
|
|
| 1116 |
}
|
| 1117 |
pip() {
|
| 1118 |
if [ "${1:-}" = "install" ] && [ -z "${VIRTUAL_ENV:-}" ] && ! _hc_has_arg --user "$@" && ! _hc_has_arg --prefix "$@"; then
|
| 1119 |
+
command pip install --user --break-system-packages "${@:2}"
|
| 1120 |
else
|
| 1121 |
command pip "$@"
|
| 1122 |
fi
|
| 1123 |
local rc=$?
|
| 1124 |
+
# Skip capture when -r/--requirement is used: the requirements file won't exist on next boot
|
| 1125 |
+
if [ $rc -eq 0 ] && [ "${1:-}" = "install" ] \
|
| 1126 |
+
&& ! _hc_has_arg -r "${@:2}" && ! _hc_has_arg --requirement "${@:2}" \
|
| 1127 |
+
&& _hc_has_install_targets "${@:2}"; then
|
| 1128 |
_hc_append_cmd "python3 -m pip install --user" "${@:2}"
|
| 1129 |
fi
|
| 1130 |
return $rc
|
| 1131 |
}
|
| 1132 |
pip3() {
|
| 1133 |
if [ "${1:-}" = "install" ] && [ -z "${VIRTUAL_ENV:-}" ] && ! _hc_has_arg --user "$@" && ! _hc_has_arg --prefix "$@"; then
|
| 1134 |
+
command pip3 install --user --break-system-packages "${@:2}"
|
| 1135 |
else
|
| 1136 |
command pip3 "$@"
|
| 1137 |
fi
|
| 1138 |
local rc=$?
|
| 1139 |
+
if [ $rc -eq 0 ] && [ "${1:-}" = "install" ] \
|
| 1140 |
+
&& ! _hc_has_arg -r "${@:2}" && ! _hc_has_arg --requirement "${@:2}" \
|
| 1141 |
+
&& _hc_has_install_targets "${@:2}"; then
|
| 1142 |
_hc_append_cmd "python3 -m pip install --user" "${@:2}"
|
| 1143 |
fi
|
| 1144 |
return $rc
|
| 1145 |
}
|
| 1146 |
+
python() {
|
| 1147 |
+
if [ "${1:-}" = "-m" ] && [ "${2:-}" = "pip" ] && [ "${3:-}" = "install" ] && [ -z "${VIRTUAL_ENV:-}" ] && ! _hc_has_arg --user "${@:3}" && ! _hc_has_arg --prefix "${@:3}"; then
|
| 1148 |
+
command python -m pip install --user --break-system-packages "${@:4}"
|
| 1149 |
+
else
|
| 1150 |
+
command python "$@"
|
| 1151 |
+
fi
|
| 1152 |
+
local rc=$?
|
| 1153 |
+
if [ $rc -eq 0 ] && [ "${1:-}" = "-m" ] && [ "${2:-}" = "pip" ] && [ "${3:-}" = "install" ] \
|
| 1154 |
+
&& ! _hc_has_arg -r "${@:4}" && ! _hc_has_arg --requirement "${@:4}" \
|
| 1155 |
+
&& _hc_has_install_targets "${@:4}"; then
|
| 1156 |
+
_hc_append_cmd "python3 -m pip install --user" "${@:4}"
|
| 1157 |
+
fi
|
| 1158 |
+
return $rc
|
| 1159 |
+
}
|
| 1160 |
+
python3() {
|
| 1161 |
+
if [ "${1:-}" = "-m" ] && [ "${2:-}" = "pip" ] && [ "${3:-}" = "install" ] && [ -z "${VIRTUAL_ENV:-}" ] && ! _hc_has_arg --user "${@:3}" && ! _hc_has_arg --prefix "${@:3}"; then
|
| 1162 |
+
command python3 -m pip install --user --break-system-packages "${@:4}"
|
| 1163 |
+
else
|
| 1164 |
+
command python3 "$@"
|
| 1165 |
+
fi
|
| 1166 |
+
local rc=$?
|
| 1167 |
+
if [ $rc -eq 0 ] && [ "${1:-}" = "-m" ] && [ "${2:-}" = "pip" ] && [ "${3:-}" = "install" ] \
|
| 1168 |
+
&& ! _hc_has_arg -r "${@:4}" && ! _hc_has_arg --requirement "${@:4}" \
|
| 1169 |
+
&& _hc_has_install_targets "${@:4}"; then
|
| 1170 |
+
_hc_append_cmd "python3 -m pip install --user" "${@:4}"
|
| 1171 |
+
fi
|
| 1172 |
+
return $rc
|
| 1173 |
+
}
|
| 1174 |
npm() {
|
| 1175 |
command npm "$@"
|
| 1176 |
local rc=$?
|
| 1177 |
+
if [ $rc -eq 0 ] && { [ "${1:-}" = "install" ] || [ "${1:-}" = "i" ]; } && { [ "${2:-}" = "-g" ] || [ "${2:-}" = "--global" ]; } && _hc_has_install_targets "${@:3}"; then
|
| 1178 |
_hc_append_cmd "npm install -g" "${@:3}"
|
| 1179 |
fi
|
| 1180 |
return $rc
|
|
|
|
| 1182 |
openclaw() {
|
| 1183 |
command openclaw "$@"
|
| 1184 |
local rc=$?
|
| 1185 |
+
if [ $rc -eq 0 ] && [ "${1:-}" = "plugins" ] && [ "${2:-}" = "install" ] && _hc_has_install_targets "${@:3}"; then
|
| 1186 |
_hc_allow_openclaw_plugins "${@:3}"
|
| 1187 |
_hc_append_cmd "openclaw plugins install" "${@:3}"
|
| 1188 |
fi
|
| 1189 |
return $rc
|
| 1190 |
}
|
| 1191 |
+
# uv pip install — increasingly popular fast pip replacement
|
| 1192 |
+
uv() {
|
| 1193 |
+
command uv "$@"
|
| 1194 |
+
local rc=$?
|
| 1195 |
+
# Only capture: uv pip install ... (not uv pip sync, uv add, etc.)
|
| 1196 |
+
# Skip if -r/--requirements flag present (file won't exist on next boot)
|
| 1197 |
+
if [ $rc -eq 0 ] && [ "${1:-}" = "pip" ] && [ "${2:-}" = "install" ] \
|
| 1198 |
+
&& ! _hc_has_arg -r "${@:3}" && ! _hc_has_arg --requirements "${@:3}" \
|
| 1199 |
+
&& _hc_has_install_targets "${@:3}"; then
|
| 1200 |
+
_hc_append_cmd "uv pip install" "${@:3}"
|
| 1201 |
+
fi
|
| 1202 |
+
return $rc
|
| 1203 |
+
}
|
| 1204 |
+
# pipx — isolated tool installs
|
| 1205 |
+
pipx() {
|
| 1206 |
+
command pipx "$@"
|
| 1207 |
+
local rc=$?
|
| 1208 |
+
if [ $rc -eq 0 ] && [ "${1:-}" = "install" ] && _hc_has_install_targets "${@:2}"; then
|
| 1209 |
+
_hc_append_cmd "pipx install" "${@:2}"
|
| 1210 |
+
fi
|
| 1211 |
+
return $rc
|
| 1212 |
+
}
|
| 1213 |
BASHRC
|
| 1214 |
cat > /home/node/.profile <<'PROFILE'
|
| 1215 |
+
[ -n "${BASH_VERSION:-}" ] && [ -f ~/.bashrc ] && . ~/.bashrc
|
| 1216 |
PROFILE
|
| 1217 |
echo "Shell capture wrappers ready."
|
| 1218 |
|
|
|
|
| 1250 |
|
| 1251 |
echo "[startup:${source_label}] $command_text"
|
| 1252 |
set +e
|
| 1253 |
+
HUGGINGCLAW_CAPTURE_DISABLE=1 bash -lc "$command_text"
|
| 1254 |
local rc=$?
|
| 1255 |
set -e
|
| 1256 |
if [ "$rc" -eq 0 ]; then
|
|
|
|
| 1274 |
# Load HuggingClaw's install wrappers for env-provided scripts too, so
|
| 1275 |
# `apt install`, `pip install`, `npm install -g`, and OpenClaw plugin
|
| 1276 |
# installs behave the same way as they do in the interactive shell.
|
| 1277 |
+
echo 'export HUGGINGCLAW_CAPTURE_DISABLE=1'
|
| 1278 |
echo '[ -f /home/node/.bashrc ] && . /home/node/.bashrc'
|
| 1279 |
printf '%s\n' "$script_text"
|
| 1280 |
} > "$script_file"
|
|
|
|
| 1392 |
if [ -n "${HUGGINGCLAW_PIP_PACKAGES:-}" ]; then
|
| 1393 |
echo "Installing Python packages from HUGGINGCLAW_PIP_PACKAGES..."
|
| 1394 |
read -r -a HC_PIP_PACKAGES <<< "$HUGGINGCLAW_PIP_PACKAGES"
|
| 1395 |
+
if python3 -m pip install --user --break-system-packages "${HC_PIP_PACKAGES[@]}"; then
|
| 1396 |
echo "HUGGINGCLAW_PIP_PACKAGES install complete."
|
| 1397 |
else
|
| 1398 |
HC_STARTUP_FAILURES=$((HC_STARTUP_FAILURES + 1))
|
|
|
|
| 1481 |
echo "Warning: could not sync settled state before gateway restart"
|
| 1482 |
}
|
| 1483 |
|
| 1484 |
+
start_background_devdata_sync() {
|
| 1485 |
+
if [ "$DEV_MODE_ENABLED" != "true" ]; then
|
| 1486 |
+
return 0
|
| 1487 |
+
fi
|
| 1488 |
+
if [ "$DEVDATA_ENABLED" != "true" ]; then
|
| 1489 |
+
echo "DevData : disabled by DEVDATA=${DEVDATA_RAW}"
|
| 1490 |
+
return 0
|
| 1491 |
+
fi
|
| 1492 |
+
if [ -z "${HF_TOKEN:-}" ]; then
|
| 1493 |
+
echo "DevData : disabled (HF_TOKEN missing)"
|
| 1494 |
+
return 0
|
| 1495 |
+
fi
|
| 1496 |
+
if [ "${DEVDATA_DATASET_NAME:-huggingclaw-devdata}" = "${BACKUP_DATASET_NAME:-huggingclaw-backup}" ]; then
|
| 1497 |
+
echo "DevData : disabled (DEVDATA_DATASET_NAME must be separate from BACKUP_DATASET_NAME)"
|
| 1498 |
+
return 0
|
| 1499 |
+
fi
|
| 1500 |
+
if [ ! -f "/home/node/app/jupyter-devdata-sync.py" ]; then
|
| 1501 |
+
echo "DevData : script missing; skipped"
|
| 1502 |
+
return 0
|
| 1503 |
+
fi
|
| 1504 |
+
# BUG FIX #1: Guard against spawning a second devdata-sync process on every
|
| 1505 |
+
# gateway restart. Without this check, each restart launched a fresh
|
| 1506 |
+
# jupyter-devdata-sync.py which called restore_once() while JupyterLab was
|
| 1507 |
+
# already running, corrupting its runtime state and killing it.
|
| 1508 |
+
if [ -n "${DEVDATA_SYNC_PID:-}" ] && kill -0 "$DEVDATA_SYNC_PID" 2>/dev/null; then
|
| 1509 |
+
return 0
|
| 1510 |
+
fi
|
| 1511 |
+
echo "DevData : enabled (dataset=${DEVDATA_DATASET_NAME:-huggingclaw-devdata})"
|
| 1512 |
+
python3 -u /home/node/app/jupyter-devdata-sync.py >> /tmp/devdata-sync.log 2>&1 &
|
| 1513 |
+
DEVDATA_SYNC_PID=$!
|
| 1514 |
+
}
|
| 1515 |
+
|
| 1516 |
start_background_sync_once() {
|
| 1517 |
[ -n "${HF_TOKEN:-}" ] || return 0
|
| 1518 |
|
|
|
|
| 1520 |
return 0
|
| 1521 |
fi
|
| 1522 |
|
| 1523 |
+
python3 -u /home/node/app/openclaw-sync.py loop >> /tmp/workspace-sync.log 2>&1 &
|
| 1524 |
SYNC_LOOP_PID=$!
|
| 1525 |
}
|
| 1526 |
|
|
|
|
| 1537 |
}
|
| 1538 |
|
| 1539 |
while true; do
|
| 1540 |
+
# Check health-server process - restart if died unexpectedly
|
| 1541 |
+
if [ -n "${HEALTH_PID:-}" ] && ! kill -0 "$HEALTH_PID" 2>/dev/null; then
|
| 1542 |
+
echo "Warning: health-server exited (PID $HEALTH_PID dead); restarting..."
|
| 1543 |
+
node /home/node/app/health-server.js &
|
| 1544 |
+
HEALTH_PID=$!
|
| 1545 |
+
echo "Health server restarted (PID: $HEALTH_PID)"
|
| 1546 |
+
fi
|
| 1547 |
+
|
| 1548 |
+
# Check JupyterLab process - restart if died unexpectedly
|
| 1549 |
+
if [ "$RUNTIME_JUPYTER_ENABLED" = "true" ]; then
|
| 1550 |
+
if [ -n "${JUPYTER_PID:-}" ]; then
|
| 1551 |
+
if ! kill -0 "$JUPYTER_PID" 2>/dev/null; then
|
| 1552 |
+
echo "Warning: JupyterLab exited (PID $JUPYTER_PID dead); checking log..."
|
| 1553 |
+
tail -5 /tmp/jupyterlab.log 2>/dev/null || echo "No log file"
|
| 1554 |
+
echo "Attempting JupyterLab restart..."
|
| 1555 |
+
unset JUPYTER_PID
|
| 1556 |
+
start_jupyter_once
|
| 1557 |
+
fi
|
| 1558 |
+
else
|
| 1559 |
+
# First start
|
| 1560 |
+
start_jupyter_once
|
| 1561 |
+
fi
|
| 1562 |
+
fi
|
| 1563 |
+
|
| 1564 |
echo "Launching OpenClaw gateway on port 7860..."
|
| 1565 |
|
| 1566 |
GATEWAY_ARGS=(gateway run --port 7860 --bind lan)
|
|
|
|
| 1610 |
# config edits can make OpenClaw exit/reload, and the gateway loop below will
|
| 1611 |
# relaunch it without rerunning all startup code.
|
| 1612 |
start_background_sync_once
|
| 1613 |
+
start_background_devdata_sync
|
| 1614 |
|
| 1615 |
set +e
|
| 1616 |
wait "$GATEWAY_PID"
|
wa-guardian.js
CHANGED
|
@@ -20,7 +20,7 @@ const { randomUUID } = require('node:crypto');
|
|
| 20 |
const GATEWAY_URL = "ws://127.0.0.1:7860";
|
| 21 |
const GATEWAY_TOKEN = process.env.GATEWAY_TOKEN || "huggingclaw";
|
| 22 |
const WHATSAPP_ENABLED = /^true$/i.test(process.env.WHATSAPP_ENABLED || "");
|
| 23 |
-
const CHECK_INTERVAL =
|
| 24 |
const WAIT_TIMEOUT = 120000;
|
| 25 |
const POST_515_NO_LOGOUT_MS = 90 * 1000;
|
| 26 |
const SUCCESS_COOLDOWN_MS = 60 * 1000;
|
|
@@ -141,7 +141,13 @@ async function callRpc(ws, method, params) {
|
|
| 141 |
}
|
| 142 |
};
|
| 143 |
ws.on("message", handler);
|
| 144 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
setTimeout(() => { ws.removeListener("message", handler); reject(new Error("RPC Timeout")); }, WAIT_TIMEOUT + 5000);
|
| 146 |
});
|
| 147 |
}
|
|
@@ -247,6 +253,13 @@ if (!WHATSAPP_ENABLED) {
|
|
| 247 |
process.exit(0);
|
| 248 |
}
|
| 249 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
writeStatus({ configured: true, connected: false, pairing: false });
|
| 251 |
console.log("[guardian] WhatsApp Guardian active. Monitoring pairing status...");
|
| 252 |
setInterval(checkStatus, CHECK_INTERVAL);
|
|
|
|
| 20 |
const GATEWAY_URL = "ws://127.0.0.1:7860";
|
| 21 |
const GATEWAY_TOKEN = process.env.GATEWAY_TOKEN || "huggingclaw";
|
| 22 |
const WHATSAPP_ENABLED = /^true$/i.test(process.env.WHATSAPP_ENABLED || "");
|
| 23 |
+
const CHECK_INTERVAL = 30000;
|
| 24 |
const WAIT_TIMEOUT = 120000;
|
| 25 |
const POST_515_NO_LOGOUT_MS = 90 * 1000;
|
| 26 |
const SUCCESS_COOLDOWN_MS = 60 * 1000;
|
|
|
|
| 141 |
}
|
| 142 |
};
|
| 143 |
ws.on("message", handler);
|
| 144 |
+
try {
|
| 145 |
+
ws.send(JSON.stringify({ type: "req", id, method, params }));
|
| 146 |
+
} catch (sendErr) {
|
| 147 |
+
ws.removeListener("message", handler);
|
| 148 |
+
reject(sendErr);
|
| 149 |
+
return;
|
| 150 |
+
}
|
| 151 |
setTimeout(() => { ws.removeListener("message", handler); reject(new Error("RPC Timeout")); }, WAIT_TIMEOUT + 5000);
|
| 152 |
});
|
| 153 |
}
|
|
|
|
| 253 |
process.exit(0);
|
| 254 |
}
|
| 255 |
|
| 256 |
+
process.on("unhandledRejection", (reason) => {
|
| 257 |
+
const msg = reason && reason.message ? reason.message : String(reason);
|
| 258 |
+
if (!/RPC Timeout|Timeout/i.test(msg)) {
|
| 259 |
+
console.log(`[guardian] Unhandled rejection: ${msg}`);
|
| 260 |
+
}
|
| 261 |
+
});
|
| 262 |
+
|
| 263 |
writeStatus({ configured: true, connected: false, pairing: false });
|
| 264 |
console.log("[guardian] WhatsApp Guardian active. Monitoring pairing status...");
|
| 265 |
setInterval(checkStatus, CHECK_INTERVAL);
|