xlyu0523 commited on
Commit
dda599d
·
verified ·
1 Parent(s): 48afc01

Update start.sh

Browse files
Files changed (1) hide show
  1. start.sh +115 -556
start.sh CHANGED
@@ -1,563 +1,122 @@
1
- #!/bin/bash
2
- set -euo pipefail
3
-
4
- # ─────────────────────────────────────────────────────────────────────────
5
- # 🔥 阶段一:特权初始化与存储桶强力清洗(Root空间)
6
- # ─────────────────────────────────────────────────────────────────────────
7
- if [ "$(id -u)" -eq 0 ]; then
8
- echo "[Storage-Sanitizer] Executing root initialization and lock scrubbing..."
9
-
10
- TARGET_HOME="${HERMES_HOME:-/opt/data}"
11
- # 预先建立好原本属于 hermes 用户的存储层级拓扑
12
- mkdir -p "$TARGET_HOME"/{cron,sessions,logs,hooks,memories,skills,skins,plans,workspace,home,plugins}
13
-
14
- # 清除上一次运行闪退在存储桶里留下的幽灵锁与僵尸 PID 文件
15
- find "$TARGET_HOME" -name "*.lock" -delete 2>/dev/null || true
16
- find "$TARGET_HOME" -name "*.pid" -delete 2>/dev/null || true
17
-
18
- # 动态纠正因跨容器调度可能引起的物理存储卷挂载 UID 偏差
19
- chown -R hermes:hermes "$TARGET_HOME" 2>/dev/null || true
20
-
21
- echo "[Storage-Sanitizer] Dropping privileges safely to user 'hermes'."
22
- # 携带整套业务底座所需的全部环境变量,无缝将主进程控制权交回用户空间
23
- exec runuser -u hermes -- env \
24
- PATH="/opt/hermes/.venv/bin:/opt/data/.local/bin:$PATH" \
25
- HOME="$HOME" \
26
- HERMES_HOME="$TARGET_HOME" \
27
- DEV_MODE="${DEV_MODE:-true}" \
28
- GATEWAY_TOKEN="${GATEWAY_TOKEN:-}" \
29
- API_SERVER_KEY="${API_SERVER_KEY:-}" \
30
- API_SERVER_ENABLED="${API_SERVER_ENABLED:-true}" \
31
- API_SERVER_HOST="${API_SERVER_HOST:-127.0.0.1}" \
32
- API_SERVER_PORT="${API_SERVER_PORT:-8642}" \
33
- GATEWAY_HEALTH_URL="${GATEWAY_HEALTH_URL:-http://127.0.0.1:8642}" \
34
- TELEGRAM_WEBHOOK_PORT="${TELEGRAM_WEBHOOK_PORT:-8765}" \
35
- PORT="${PORT:-7861}" \
36
- HERMES_MODEL="${HERMES_MODEL:-}" \
37
- LLM_MODEL="${LLM_MODEL:-}" \
38
- HERMES_INFERENCE_PROVIDER="${HERMES_INFERENCE_PROVIDER:-auto}" \
39
- LLM_API_KEY="${LLM_API_KEY:-}" \
40
- CUSTOM_BASE_URL="${CUSTOM_BASE_URL:-}" \
41
- CUSTOM_PROVIDER="${CUSTOM_PROVIDER:-}" \
42
- CUSTOM_API_KEY="${CUSTOM_API_KEY:-}" \
43
- CLOUDFLARE_WORKERS_TOKEN="${CLOUDFLARE_WORKERS_TOKEN:-}" \
44
- CLOUDFLARE_PROXY_URL="${CLOUDFLARE_PROXY_URL:-}" \
45
- TELEGRAM_BOT_TOKEN="${TELEGRAM_BOT_TOKEN:-}" \
46
- SPACE_HOST="${SPACE_HOST:-}" \
47
- TELEGRAM_MODE="${TELEGRAM_MODE:-}" \
48
- TELEGRAM_ALLOWED_USERS="${TELEGRAM_ALLOWED_USERS:-}" \
49
- TELEGRAM_USER_IDS="${TELEGRAM_USER_IDS:-}" \
50
- HUGGINGMES_APT_PACKAGES="${HUGGINGMES_APT_PACKAGES:-}" \
51
- HUGGINGMES_PIP_PACKAGES="${HUGGINGMES_PIP_PACKAGES:-}" \
52
- HUGGINGMES_NPM_PACKAGES="${HUGGINGMES_NPM_PACKAGES:-}" \
53
- HUGGINGMES_RUN="${HUGGINGMES_RUN:-}" \
54
- /bin/bash "$0" "$@"
55
- fi
56
-
57
- # ─────────────────────────────────────────────────────────────────────────
58
- # 🛡️ 阶段二:安全降权沙箱运行(Hermes空间)
59
- # ─────────────────────────────────────────────────────────────────────────
60
- # 恢复原项目高强度安全掩码阻断非法外部嗅探
61
- umask 0077
62
-
63
- APP_DIR="${HUGGINGMES_APP_DIR:-/opt/huggingmes}"
64
- HERMES_HOME="${HERMES_HOME:-/opt/data}"
65
- PUBLIC_PORT="${PORT:-7861}"
66
- GATEWAY_API_PORT="${API_SERVER_PORT:-8642}"
67
- DASHBOARD_PORT="${DASHBOARD_PORT:-9119}"
68
- TELEGRAM_WEBHOOK_PORT="${TELEGRAM_WEBHOOK_PORT:-8765}"
69
- CF_PROXY_ENV_FILE="/tmp/huggingmes-cloudflare-proxy.env"
70
- STARTUP_FILE="$HERMES_HOME/workspace/startup.sh"
71
-
72
- export HERMES_HOME
73
- export API_SERVER_ENABLED="${API_SERVER_ENABLED:-true}"
74
- export API_SERVER_HOST="${API_SERVER_HOST:-127.0.0.1}"
75
- export API_SERVER_PORT="$GATEWAY_API_PORT"
76
- export GATEWAY_HEALTH_URL="${GATEWAY_HEALTH_URL:-http://127.0.0.1:${GATEWAY_API_PORT}}"
77
- export TELEGRAM_WEBHOOK_PORT
78
-
79
- echo ""
80
- echo " ╔══════════════════════════════════════════╗"
81
- echo " ║ 🪽 HuggingMes Hermes Gateway ║"
82
- echo " ╚══════════════════════════════════════════╝"
83
- echo ""
84
-
85
- # Token 密钥配对自适应链条
86
- if [ -z "${API_SERVER_KEY:-}" ]; then
87
- if [ -n "${GATEWAY_TOKEN:-}" ]; then
88
- export API_SERVER_KEY="$GATEWAY_TOKEN"
89
- else
90
- API_SERVER_KEY="$(python3 - <<'PY'
91
- import secrets
92
- print(secrets.token_urlsafe(32))
93
- PY
94
- )"
95
- export API_SERVER_KEY
96
- echo "GATEWAY_TOKEN not set - generated an ephemeral API token for this boot."
97
- fi
98
- fi
99
-
100
- # 【终极同步修复】:强行对齐全局变量断言,确保官方前端 Terminal 握手成功���报错
101
- export GATEWAY_TOKEN="$API_SERVER_KEY"
102
-
103
- # 系统本地运行依赖树拓扑生成
104
- mkdir -p "$HERMES_HOME"/{cron,sessions,logs,hooks,memories,skills,skins,plans,workspace,home,plugins}
105
- mkdir -p "$HERMES_HOME/.local/bin"
106
-
107
- if [ ! -L "$HERMES_HOME/.local/bin/hermes" ]; then
108
- rm -f "$HERMES_HOME/.local/bin/hermes"
109
- ln -sfn /opt/hermes/.venv/bin/hermes "$HERMES_HOME/.local/bin/hermes"
110
- fi
111
-
112
- if [ ! -L "${HOME}/.hermes/plugins" ]; then
113
- mkdir -p "${HOME}/.hermes"
114
- rm -rf "${HOME}/.hermes/plugins"
115
- ln -sfn "$HERMES_HOME/plugins" "${HOME}/.hermes/plugins"
116
- fi
117
-
118
- # Cloudflare 网络层代理反向构建
119
- CLOUDFLARE_WORKERS_TOKEN="${CLOUDFLARE_WORKERS_TOKEN:-${CLOUDFLARE_API_TOKEN:-}}"
120
- export CLOUDFLARE_WORKERS_TOKEN
121
- if [ -n "${CLOUDFLARE_WORKERS_TOKEN:-}" ] || [ -n "${CLOUDFLARE_PROXY_URL:-}" ]; then
122
- export CLOUDFLARE_PROXY_DEBUG="${CLOUDFLARE_PROXY_DEBUG:-false}"
123
- echo "Preparing Cloudflare Telegram proxy..."
124
- python3 "$APP_DIR/cloudflare-proxy-setup.py" || true
125
- if [ -f "$CF_PROXY_ENV_FILE" ]; then
126
- . "$CF_PROXY_ENV_FILE"
127
- fi
128
- fi
129
-
130
- if [ -n "${CLOUDFLARE_WORKERS_TOKEN:-}" ]; then
131
- echo "Preparing Cloudflare Keepalive worker..."
132
- python3 "$APP_DIR/cloudflare-keepalive-setup.py" || true
133
- fi
134
-
135
- # Telegram 鉴权通道提取
136
- if [ -n "${TELEGRAM_USER_IDS:-}" ] && [ -z "${TELEGRAM_ALLOWED_USERS:-}" ]; then
137
- export TELEGRAM_ALLOWED_USERS="$TELEGRAM_USER_IDS"
138
- elif [ -n "${TELEGRAM_USER_ID:-}" ] && [ -z "${TELEGRAM_ALLOWED_USERS:-}" ]; then
139
- export TELEGRAM_ALLOWED_USERS="$TELEGRAM_USER_ID"
140
- fi
141
-
142
- if [ -n "${TELEGRAM_BOT_TOKEN:-}" ] && [ -n "${SPACE_HOST:-}" ] && [ -z "${TELEGRAM_WEBHOOK_URL:-}" ]; then
143
- if [ "${TELEGRAM_MODE:-webhook}" != "polling" ]; then
144
- export TELEGRAM_WEBHOOK_URL="https://${SPACE_HOST}/telegram"
145
- fi
146
- fi
147
-
148
- if [ -n "${TELEGRAM_WEBHOOK_URL:-}" ] && [ -z "${TELEGRAM_WEBHOOK_SECRET:-}" ]; then
149
- SECRET_FILE="$HERMES_HOME/.huggingmes-telegram-webhook-secret"
150
- if [ -f "$SECRET_FILE" ]; then
151
- TELEGRAM_WEBHOOK_SECRET="$(cat "$SECRET_FILE")"
152
- export TELEGRAM_WEBHOOK_SECRET
153
- else
154
- TELEGRAM_WEBHOOK_SECRET="$(python3 - <<'PY'
155
- import secrets
156
- print(secrets.token_hex(32))
157
- PY
158
- )"
159
- printf '%s' "$TELEGRAM_WEBHOOK_SECRET" > "$SECRET_FILE"
160
- chmod 600 "$SECRET_FILE"
161
- export TELEGRAM_WEBHOOK_SECRET
162
- fi
163
- fi
164
-
165
- # 大模型特有转换矩阵矩阵(100% 完整继承)
166
- MODEL_INPUT="${HERMES_MODEL:-${LLM_MODEL:-}}"
167
- MODEL_FOR_CONFIG="$MODEL_INPUT"
168
- PROVIDER_FOR_CONFIG="${HERMES_INFERENCE_PROVIDER:-auto}"
169
- LLM_API_KEY="${LLM_API_KEY:-}"
170
-
171
- if [ -n "$MODEL_INPUT" ]; then
172
- MODEL_PREFIX="${MODEL_INPUT%%/*}"
173
- else
174
- MODEL_PREFIX=""
175
- fi
176
-
177
- case "$MODEL_PREFIX" in
178
- openrouter)
179
- [ -n "$LLM_API_KEY" ] && export OPENROUTER_API_KEY="${OPENROUTER_API_KEY:-$LLM_API_KEY}"
180
- [ "$PROVIDER_FOR_CONFIG" = "auto" ] && PROVIDER_FOR_CONFIG="openrouter"
181
- MODEL_FOR_CONFIG="${MODEL_INPUT#openrouter/}"
182
- ;;
183
- huggingface|hf)
184
- [ -n "$LLM_API_KEY" ] && export HF_TOKEN="${HF_TOKEN:-$LLM_API_KEY}"
185
- [ "$PROVIDER_FOR_CONFIG" = "auto" ] && PROVIDER_FOR_CONFIG="huggingface"
186
- MODEL_FOR_CONFIG="${MODEL_INPUT#huggingface/}"
187
- ;;
188
- vercel-ai-gateway|ai-gateway)
189
- [ -n "$LLM_API_KEY" ] && export AI_GATEWAY_API_KEY="${AI_GATEWAY_API_KEY:-$LLM_API_KEY}"
190
- [ "$PROVIDER_FOR_CONFIG" = "auto" ] && PROVIDER_FOR_CONFIG="ai-gateway"
191
- MODEL_FOR_CONFIG="${MODEL_INPUT#*/}"
192
- ;;
193
- anthropic) [ -n "$LLM_API_KEY" ] && export ANTHROPIC_API_KEY="${ANTHROPIC_API_KEY:-$LLM_API_KEY}" ;;
194
- openai|openai-codex) [ -n "$LLM_API_KEY" ] && export OPENAI_API_KEY="${OPENAI_API_KEY:-$LLM_API_KEY}" ;;
195
- google|gemini)
196
- [ -n "$LLM_API_KEY" ] && export GOOGLE_API_KEY="${GOOGLE_API_KEY:-$LLM_API_KEY}" GEMINI_API_KEY="${GEMINI_API_KEY:-$LLM_API_KEY}"
197
- PROVIDER_FOR_CONFIG="gemini"
198
- MODEL_FOR_CONFIG="${MODEL_INPUT#*/}"
199
- ;;
200
- deepseek) [ -n "$LLM_API_KEY" ] && export DEEPSEEK_API_KEY="${DEEPSEEK_API_KEY:-$LLM_API_KEY}" ;;
201
- kimi-coding|moonshot) [ -n "$LLM_API_KEY" ] && export KIMI_API_KEY="${KIMI_API_KEY:-$LLM_API_KEY}" ;;
202
- kimi-coding-cn|moonshot-cn|kimi-cn) [ -n "$LLM_API_KEY" ] && export KIMI_CN_API_KEY="${KIMI_CN_API_KEY:-$LLM_API_KEY}" ;;
203
- minimax) [ -n "$LLM_API_KEY" ] && export MINIMAX_API_KEY="${MINIMAX_API_KEY:-$LLM_API_KEY}" ;;
204
- minimax-cn) [ -n "$LLM_API_KEY" ] && export MINIMAX_CN_API_KEY="${MINIMAX_CN_API_KEY:-$LLM_API_KEY}" ;;
205
- xiaomi) [ -n "$LLM_API_KEY" ] && export XIAOMI_API_KEY="${XIAOMI_API_KEY:-$LLM_API_KEY}" ;;
206
- zai|z-ai|z.ai|glm) [ -n "$LLM_API_KEY" ] && export GLM_API_KEY="${GLM_API_KEY:-$LLM_API_KEY}" ;;
207
- arcee|arcee-ai|arceeai) [ -n "$LLM_API_KEY" ] && export ARCEEAI_API_KEY="${ARCEEAI_API_KEY:-$LLM_API_KEY}" ;;
208
- gmi|gmi-cloud|gmicloud) [ -n "$LLM_API_KEY" ] && export GMI_API_KEY="${GMI_API_KEY:-$LLM_API_KEY}" ;;
209
- alibaba|alibaba-coding-plan|alibaba_coding) [ -n "$LLM_API_KEY" ] && export DASHSCOPE_API_KEY="${DASHSCOPE_API_KEY:-$LLM_API_KEY}" ;;
210
- tencent-tokenhub|tencent|tokenhub|tencentmaas) [ -n "$LLM_API_KEY" ] && export TOKENHUB_API_KEY="${TOKENHUB_API_KEY:-$LLM_API_KEY}" ;;
211
- nvidia) [ -n "$LLM_API_KEY" ] && export NVIDIA_API_KEY="${NVIDIA_API_KEY:-$LLM_API_KEY}" ;;
212
- xai|grok) [ -n "$LLM_API_KEY" ] && export XAI_API_KEY="${XAI_API_KEY:-$LLM_API_KEY}" ;;
213
- kilocode) [ -n "$LLM_API_KEY" ] && export KILOCODE_API_KEY="${KILOCODE_API_KEY:-$LLM_API_KEY}" ;;
214
- opencode-zen) [ -n "$LLM_API_KEY" ] && export OPENCODE_ZEN_API_KEY="${OPENCODE_ZEN_API_KEY:-$LLM_API_KEY}" ;;
215
- opencode-go) [ -n "$LLM_API_KEY" ] && export OPENCODE_GO_API_KEY="${OPENCODE_GO_API_KEY:-$LLM_API_KEY}" ;;
216
- esac
217
-
218
- if [ -n "${CUSTOM_BASE_URL:-}" ]; then
219
- PROVIDER_FOR_CONFIG="${CUSTOM_PROVIDER:-custom}"
220
- [ -n "$LLM_API_KEY" ] && export OPENAI_API_KEY="${OPENAI_API_KEY:-$LLM_API_KEY}"
221
- fi
222
-
223
- export MODEL_FOR_CONFIG PROVIDER_FOR_CONFIG
224
- export CUSTOM_BASE_URL="${CUSTOM_BASE_URL:-}"
225
- export CUSTOM_API_KEY="${CUSTOM_API_KEY:-${LLM_API_KEY:-}}"
226
- export CUSTOM_MODEL_CONTEXT_LENGTH="${CUSTOM_MODEL_CONTEXT_LENGTH:-131072}"
227
- export CUSTOM_MODEL_MAX_TOKENS="${CUSTOM_MODEL_MAX_TOKENS:-8192}"
228
- export TELEGRAM_BASE_URL="${TELEGRAM_BASE_URL:-}"
229
- export TELEGRAM_BASE_FILE_URL="${TELEGRAM_BASE_FILE_URL:-}"
230
-
231
- if [ -n "${CLOUDFLARE_PROXY_URL:-}" ] && [ -z "$TELEGRAM_BASE_URL" ]; then
232
- CLOUDFLARE_PROXY_URL="${CLOUDFLARE_PROXY_URL%/}"
233
- export TELEGRAM_BASE_URL="${CLOUDFLARE_PROXY_URL}/bot"
234
- export TELEGRAM_BASE_FILE_URL="${CLOUDFLARE_PROXY_URL}/file/bot"
235
- fi
236
-
237
- promote_first_pool_key() {
238
- local singular_var="$1" local pool_var="$2"
239
- local singular_val="${!singular_var:-}" pool_val="${!pool_var:-}"
240
- [ -n "$singular_val" ] && return 0
241
- [ -n "$pool_val" ] || return 0
242
- local first
243
- first=$(printf '%s' "$pool_val" | tr ',' '\n' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' | awk 'NF{print; exit}')
244
- [ -n "$first" ] || return 0
245
- export "${singular_var}=$first"
246
- }
247
- promote_first_pool_key "OPENROUTER_API_KEY" "OPENROUTER_API_KEYS"
248
- promote_first_pool_key "ANTHROPIC_API_KEY" "ANTHROPIC_API_KEYS"
249
- promote_first_pool_key "OPENAI_API_KEY" "OPENAI_API_KEYS"
250
- promote_first_pool_key "GOOGLE_API_KEY" "GOOGLE_API_KEYS"
251
- promote_first_pool_key "GEMINI_API_KEY" "GEMINI_API_KEYS"
252
- promote_first_pool_key "DEEPSEEK_API_KEY" "DEEPSEEK_API_KEYS"
253
- promote_first_pool_key "KIMI_API_KEY" "KIMI_API_KEYS"
254
- promote_first_pool_key "MINIMAX_API_KEY" "MINIMAX_API_KEYS"
255
- promote_first_pool_key "NVIDIA_API_KEY" "NVIDIA_API_KEYS"
256
- promote_first_pool_key "XAI_API_KEY" "XAI_API_KEYS"
257
- promote_first_pool_key "KILOCODE_API_KEY" "KILOCODE_API_KEYS"
258
- promote_first_pool_key "GLM_API_KEY" "GLM_API_KEYS"
259
- promote_first_pool_key "ARCEEAI_API_KEY" "ARCEEAI_API_KEYS"
260
- promote_first_pool_key "DASHSCOPE_API_KEY" "DASHSCOPE_API_KEYS"
261
- promote_first_pool_key "GMI_API_KEY" "GMI_API_KEYS"
262
- promote_first_pool_key "TOKENHUB_API_KEY" "TOKENHUB_API_KEYS"
263
-
264
- # YAML 多组学环境配置刷入
265
- python3 - <<'PY'
266
- import os
267
- from pathlib import Path
268
- import yaml
269
- home = Path(os.environ["HERMES_HOME"])
270
- path = home / "config.yaml"
271
  try:
272
- config = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
273
- except FileNotFoundError:
274
- config = {}
275
-
276
- model_name = os.environ.get("MODEL_FOR_CONFIG", "").strip()
277
- provider_name = os.environ.get("PROVIDER_FOR_CONFIG", "").strip()
278
- if model_name:
279
- model = config.setdefault("model", {})
280
- model["default"] = model_name
281
- if provider_name and provider_name != "auto":
282
- model["provider"] = provider_name
283
- else:
284
- model.pop("provider", None)
285
-
286
- custom_base = os.environ.get("CUSTOM_BASE_URL", "").strip()
287
- if custom_base and model_name:
288
- model.setdefault("base_url", custom_base.rstrip("/"))
289
- if os.environ.get("CUSTOM_API_KEY"):
290
- model.setdefault("api_key", os.environ["CUSTOM_API_KEY"])
291
- try:
292
- model.setdefault("context_length", int(os.environ.get("CUSTOM_MODEL_CONTEXT_LENGTH", "131072")))
293
- model.setdefault("max_tokens", int(os.environ.get("CUSTOM_MODEL_MAX_TOKENS", "8192")))
294
- except ValueError:
295
- pass
296
-
297
- config.setdefault("terminal", {}).setdefault("cwd", os.environ.get("MESSAGING_CWD", str(home / "workspace")))
298
- config.setdefault("compression", {}).setdefault("enabled", True)
299
- config.setdefault("display", {}).setdefault("background_process_notifications", os.environ.get("HERMES_BACKGROUND_NOTIFICATIONS", "result"))
300
- config.setdefault("security", {}).setdefault("redact_secrets", True)
301
-
302
- platforms = config.setdefault("platforms", {})
303
- if os.environ.get("TELEGRAM_BOT_TOKEN"):
304
- telegram = platforms.setdefault("telegram", {})
305
- telegram.setdefault("enabled", True)
306
- extra = telegram.setdefault("extra", {})
307
- if os.environ.get("TELEGRAM_BASE_URL"):
308
- extra.setdefault("base_url", os.environ["TELEGRAM_BASE_URL"])
309
- extra.setdefault("base_file_url", os.environ.get("TELEGRAM_BASE_FILE_URL") or os.environ["TELEGRAM_BASE_URL"])
310
- if os.environ.get("TELEGRAM_ALLOWED_USERS"):
311
- config.setdefault("telegram", {}).setdefault("allow_from", [
312
- item.strip() for item in os.environ["TELEGRAM_ALLOWED_USERS"].split(",") if item.strip()
313
- ])
314
-
315
- path.write_text(yaml.safe_dump(config, sort_keys=False), encoding="utf-8")
316
- path.chmod(0o664)
317
  PY
318
 
319
- # 环境就绪面板构建
320
- HERMES_RUNTIME_VERSION="$(/opt/hermes/.venv/bin/hermes --version 2>/dev/null | awk '{print $NF; exit}' || true)"
321
- echo ""
322
- [ -n "${HERMES_RUNTIME_VERSION:-}" ] && echo "Version : ${HERMES_RUNTIME_VERSION}"
323
- echo "Model : ${MODEL_FOR_CONFIG:-unset}"
324
- echo "Provider : ${PROVIDER_FOR_CONFIG:-unset}"
325
- echo "Routes : /app/ (Hermes UI), /terminal/ (JupyterLab)"
326
- echo "Dashboard : http://127.0.0.1:${DASHBOARD_PORT}"
327
- echo "Gateway : http://127.0.0.1:${GATEWAY_API_PORT}"
328
- echo ""
329
-
330
- # JupyterLab 后台运行线程
331
- JUPYTER_PID=""
332
- start_jupyter() {
333
- if [ "${DEV_MODE:-true}" = "false" ]; then
334
- echo "JupyterLab disabled (DEV_MODE=false)."
335
- return 0
336
- fi
337
- if [ -n "${JUPYTER_PID:-}" ] && kill -0 "$JUPYTER_PID" 2>/dev/null; then
338
- return 0
339
- fi
340
-
341
- export JUPYTER_TOKEN="$GATEWAY_TOKEN"
342
- local VENV_PYTHON="/opt/hermes/.venv/bin/python"
343
- local root_dir="${JUPYTER_ROOT_DIR:-$HERMES_HOME/workspace}"
344
- mkdir -p "$root_dir"
345
-
346
- # 完美复现原项目第 331 行行为
347
- ln -sfn "$HERMES_HOME" "$root_dir/HuggingMes" 2>/dev/null || true
348
-
349
- echo "Starting JupyterLab terminal on port 8888 (root: $root_dir)"
350
- "$VENV_PYTHON" -m jupyterlab \
351
- --ip 127.0.0.1 \
352
- --port 8888 \
353
- --no-browser \
354
- --IdentityProvider.token="$JUPYTER_TOKEN" \
355
- --ServerApp.base_url=/terminal/ \
356
- --ServerApp.terminals_enabled=True \
357
- --ServerApp.terminado_settings='{"shell_command":["/bin/bash","-i"]}' \
358
- --ServerApp.allow_origin='*' \
359
- --ServerApp.allow_remote_access=True \
360
- --ServerApp.trust_xheaders=True \
361
- --ServerApp.tornado_settings="{'headers': {'Content-Security-Policy': 'frame-ancestors *'}}" \
362
- --IdentityProvider.cookie_options="{'SameSite': 'None', 'Secure': True}" \
363
- --ServerApp.disable_check_xsrf=True \
364
- --LabApp.news_url=None \
365
- --LabApp.check_for_updates_class=jupyterlab.NeverCheckForUpdate \
366
- --ServerApp.log_level=WARN \
367
- --ServerApp.root_dir="$root_dir" \
368
- >> "$HERMES_HOME/logs/jupyter.log" 2>&1 &
369
- JUPYTER_PID=$!
370
- export JUPYTER_PID
371
- echo "JupyterLab started (PID: $JUPYTER_PID)"
372
- }
373
-
374
- graceful_shutdown() {
375
- echo "Shutting down HuggingMes..."
376
- kill $(jobs -p) 2>/dev/null || true
377
- exit 0
378
- }
379
- trap graceful_shutdown SIGTERM SIGINT
380
-
381
- # Shell 行为劫持恢复矩阵 (.bashrc 全量注入)
382
- if [ ! -f "$STARTUP_FILE" ]; then
383
- touch "$STARTUP_FILE" && chmod +x "$STARTUP_FILE"
384
- fi
385
- cat > "$HOME/.bashrc" << 'BASHRC'
386
- export PATH="/opt/hermes/.venv/bin:/opt/data/.local/bin:$PATH"
387
- export DEBIAN_FRONTEND="${DEBIAN_FRONTEND:-noninteractive}"
388
- [ -z "${PS1:-}" ] || PS1="\u@\h:\w\$ "
389
- HERMES_HOME="${HERMES_HOME:-/opt/data}"
390
- STARTUP_FILE="$HERMES_HOME/workspace/startup.sh"
391
- _hm_append() {
392
- [ "${HUGGINGMES_CAPTURE_DISABLE:-0}" = "1" ] && return 0
393
- local line="$*"
394
- mkdir -p "$(dirname "$STARTUP_FILE")"
395
- touch "$STARTUP_FILE" && chmod +x "$STARTUP_FILE" 2>/dev/null || true
396
- grep -qxF "$line" "$STARTUP_FILE" 2>/dev/null || echo "$line" >> "$STARTUP_FILE"
397
- }
398
- _hm_quote_args() {
399
- local quoted=() arg
400
- for arg in "$@"; do printf -v arg '%q' "$arg"; quoted+=("$arg"); done
401
- printf '%s' "${quoted[*]}"
402
- }
403
- _hm_append_cmd() {
404
- local cmd="$1"; shift; local args; args=$(_hm_quote_args "$@")
405
- if [ -n "$args" ]; then _hm_append "$cmd $args"; else _hm_append "$cmd"; fi
406
- }
407
- _hm_args_without_flags() {
408
- for arg in "$@"; do case "$arg" in ''|-|--*|-*) ;; *) printf '%s\n' "$arg" ;; esac; done
409
- }
410
- _hm_has_install_targets() {
411
- local item
412
- while IFS= read -r item; do [ -n "$item" ] && return 0; done <<EOF
413
- $(_hm_args_without_flags "$@")
414
- EOF
415
- return 1
416
- }
417
- _hm_can_sudo_apt() { command -v sudo >/dev/null 2>&1 && sudo -n apt-get --version >/dev/null 2>&1; }
418
- _hm_apt_install() {
419
- if [ "$(id -u)" -eq 0 ]; then command apt-get update && command apt-get install -y "$@";
420
- elif _hm_can_sudo_apt; then sudo apt-get update && sudo apt-get install -y "$@";
421
- else echo "Error: apt install needs root." >&2; return 1; fi
422
- }
423
- apt-get() {
424
- case "${1:-}" in
425
- install) shift; _hm_apt_install "$@"; local rc=$?; [ $rc -eq 0 ] && _hm_has_install_targets "$@" && _hm_append_cmd "sudo apt-get update && sudo apt-get install -y" "$@"; return $rc ;;
426
- update) if [ "$(id -u)" -eq 0 ]; then command apt-get "$@"; elif _hm_can_sudo_apt; then sudo apt-get "$@"; else command apt-get "$@"; fi; return $? ;;
427
- *) command apt-get "$@"; return $? ;;
428
- esac
429
- }
430
- apt() {
431
- case "${1:-}" in
432
- install) shift; _hm_apt_install "$@"; local rc=$?; [ $rc -eq 0 ] && _hm_has_install_targets "$@" && _hm_append_cmd "sudo apt-get update && sudo apt-get install -y" "$@"; return $rc ;;
433
- update) if [ "$(id -u)" -eq 0 ]; then command apt "$@"; elif _hm_can_sudo_apt; then sudo apt "$@"; else command apt "$@"; fi; return $? ;;
434
- *) command apt "$@"; return $? ;;
435
- esac
436
- }
437
- pip() {
438
- command pip "$@"; local rc=$?
439
- [ $rc -eq 0 ] && [ "${1:-}" = "install" ] && ! grep -qE '\-r|\-\-requirement' <<<"$*" && _hm_has_install_targets "${@:2}" && _hm_append_cmd "pip install" "${@:2}"
440
- return $rc
441
- }
442
- pip3() {
443
- command pip3 "$@"; local rc=$?
444
- [ $rc -eq 0 ] && [ "${1:-}" = "install" ] && ! grep -qE '\-r|\-\-requirement' <<<"$*" && _hm_has_install_targets "${@:2}" && _hm_append_cmd "pip install" "${@:2}"
445
- return $rc
446
- }
447
- uv() {
448
- command uv "$@"; local rc=$?
449
- [ $rc -eq 0 ] && [ "${1:-}" = "pip" ] && [ "${2:-}" = "install" ] && ! grep -qE '\-r|\-\-requirements' <<<"$*" && _hm_has_install_targets "${@:3}" && _hm_append_cmd "uv pip install" "${@:3}"
450
- return $rc
451
- }
452
- npm() {
453
- command npm "$@"; local rc=$?
454
- [ $rc -eq 0 ] && { [ "${1:-}" = "install" ] || [ "${1:-}" = "i" ]; } && { [ "${2:-}" = "-g" ] || [ "${2:-}" = "--global" ]; } && _hm_has_install_targets "${@:3}" && _hm_append_cmd "npm install -g" "${@:3}"
455
- return $rc
456
- }
457
- hermes() {
458
- command hermes "$@"
459
- local grading=$?
460
- [ $grading -eq 0 ] && [ "${1:-}" = "plugins" ] && [ "${2:-}" = "install" ] && _hm_has_install_targets "${@:3}" && _hm_append_cmd "hermes plugins install" "${@:3}"
461
- return $grading
462
- }
463
- BASHRC
464
-
465
- cat > "$HOME/.profile" << 'PROFILE'
466
- [ -n "${BASH_VERSION:-}" ] && [ -f ~/.bashrc ] && . ~/.bashrc
467
- PROFILE
468
-
469
- # 环境自动化动态包加载
470
- HM_STARTUP_FAILURES=0
471
- if [ -n "${HUGGINGMES_APT_PACKAGES:-}" ]; then
472
- read -r -a HM_APT_PACKAGES <<< "$HUGGINGMES_APT_PACKAGES"
473
- if command -v sudo >/dev/null 2>&1; then sudo apt-get update && sudo apt-get install -y "${HM_APT_PACKAGES[@]}" || HM_STARTUP_FAILURES=$((HM_STARTUP_FAILURES + 1))
474
- else apt-get update && apt-get install -y "${HM_APT_PACKAGES[@]}" || HM_STARTUP_FAILURES=$((HM_STARTUP_FAILURES + 1)); fi
475
- fi
476
- if [ -n "${HUGGINGMES_PIP_PACKAGES:-}" ]; then
477
- read -r -a HM_PIP_PACKAGES <<< "$HUGGINGMES_PIP_PACKAGES"
478
- /opt/hermes/.venv/bin/pip install "${HM_PIP_PACKAGES[@]}" || HM_STARTUP_FAILURES=$((HM_STARTUP_FAILURES + 1))
479
- fi
480
- if [ -n "${HUGGINGMES_NPM_PACKAGES:-}" ]; then
481
- read -r -a HM_NPM_PACKAGES <<< "$HUGGINGMES_NPM_PACKAGES"
482
- npm install -g "${HM_NPM_PACKAGES[@]}" || HM_STARTUP_FAILURES=$((HM_STARTUP_FAILURES + 1))
483
- fi
484
-
485
- hm_run_startup_auto() {
486
- local payload="$1"
487
- [ -n "$payload" ] || return 0
488
- local script_file
489
- script_file=$(mktemp "/tmp/huggingmes-startup.XXXXXX.sh")
490
- {
491
- echo 'export HUGGINGMES_CAPTURE_DISABLE=1'
492
- echo '[ -f ~/.bashrc ] && . ~/.bashrc'
493
- if [[ "$payload" == base64:* ]] || [[ "$payload" == b64:* ]]; then
494
- printf '%s' "${payload#*:}" | base64 -d
495
- else
496
- printf '%s\n' "$payload"
497
- fi
498
- } > "$script_file" && chmod 700 "$script_file"
499
- set +e; bash "$script_file"; local rc=$?; set -e; rm -f "$script_file"
500
- [ $rc -eq 0 ] || HM_STARTUP_FAILURES=$((HM_STARTUP_FAILURES + 1))
501
- }
502
- [ -n "${HUGGINGMES_RUN:-}" ] && hm_run_startup_auto "$HUGGINGMES_RUN"
503
-
504
- if [ -s "$STARTUP_FILE" ]; then
505
- echo "Replaying workspace/startup.sh packages..."
506
- set +e; HUGGINGMES_CAPTURE_DISABLE=1 bash -l "$STARTUP_FILE"; set -e
507
- fi
508
-
509
- # 反睡眠多探针 Node 节点开启
510
- node "$APP_DIR/health-server.js" &
511
- HEALTH_PID=$!
512
-
513
- start_dashboard_once() {
514
- if [ -n "${DASHBOARD_PID:-}" ] && kill -0 "$DASHBOARD_PID" 2>/dev/null; then return 0; fi
515
- echo "Launching Hermes dashboard..."
516
- (hermes dashboard --host 127.0.0.1 --insecure 2>&1 | tee -a "$HERMES_HOME/logs/dashboard.log") &
517
- DASHBOARD_PID=$!
518
- }
519
-
520
- start_dashboard_once
521
- start_jupyter
522
-
523
- # ── 常驻核心守候管理循环 ──
524
- GATEWAY_RESTART_DELAY="${GATEWAY_RESTART_DELAY:-5}"
525
- GATEWAY_READY_TIMEOUT="${GATEWAY_READY_TIMEOUT:-120}"
526
-
527
- while true; do
528
- if [ -n "${HEALTH_PID:-}" ] && ! kill -0 "$HEALTH_PID" 2>/dev/null; then
529
- node "$APP_DIR/health-server.js" &
530
- HEALTH_PID=$!
531
- fi
532
- if [ -n "${DASHBOARD_PID:-}" ] && ! kill -0 "$DASHBOARD_PID" 2>/dev/null; then
533
- start_dashboard_once
534
- fi
535
- if [ "${DEV_MODE:-true}" != "false" ] && [ -n "${JUPYTER_PID:-}" ] && ! kill -0 "$JUPYTER_PID" 2>/dev/null; then
536
- unset JUPYTER_PID
537
- start_jupyter
538
- fi
539
 
540
- echo "Launching Hermes gateway..."
541
- (hermes gateway run 2>&1 | tee -a "$HERMES_HOME/logs/gateway.log") &
542
- GATEWAY_PID=$!
 
 
 
543
 
544
- ready=false
545
- for ((i=0; i<GATEWAY_READY_TIMEOUT; i++)); do
546
- if (echo > "/dev/tcp/127.0.0.1/${GATEWAY_API_PORT}") 2>/dev/null; then
547
- ready=true
548
- break
549
- fi
550
- if ! kill -0 "$GATEWAY_PID" 2>/dev/null; then break; fi
551
- sleep 1
552
- done
553
 
554
- if [ "$ready" != "true" ]; then
555
- echo "Hermes gateway failed to expose ports. Logs tail:"
556
- tail -40 "$HERMES_HOME/logs/gateway.log" || true
557
- exit 1
558
- fi
559
 
560
- set +e; wait "$GATEWAY_PID"; set -e
561
- echo "Gateway exited, recovering in ${GATEWAY_RESTART_DELAY}s..."
562
- sleep "$GATEWAY_RESTART_DELAY"
563
- done
 
1
+ # 采用官方给定的核心底座
2
+ ARG HERMES_AGENT_VERSION=latest
3
+ FROM nousresearch/hermes-agent:${HERMES_AGENT_VERSION}
4
+
5
+ USER root
6
+
7
+ # 1. [span_2](start_span)完整保留原项目的全部系统依赖[span_2](end_span)
8
+ RUN apt-get update && apt-get install -y --no-install-recommends \
9
+ ca-certificates \
10
+ curl \
11
+ jq \
12
+ sudo \
13
+ python3 \
14
+ python3-venv \
15
+ python3-pip \
16
+ chromium \
17
+ dbus \
18
+ dbus-x11 \
19
+ libnss3 \
20
+ libatk1.0-0 \
21
+ libatk-bridge2.0-0 \
22
+ libdrm2 \
23
+ libgbm1 \
24
+ libxcomposite1 \
25
+ libxdamage1 \
26
+ libxrandr2 \
27
+ libxkbcommon0 \
28
+ libx11-6 \
29
+ libxext6 \
30
+ libxfixes3 \
31
+ fonts-dejavu-core \
32
+ fonts-liberation \
33
+ fonts-noto-color-emoji \
34
+ && (apt-get install -y --no-install-recommends libasound2 2>/dev/null \
35
+ || apt-get install -y --no-install-recommends libasound2t64 2>/dev/null \
36
+ || true) \
37
+ && rm -rf /var/lib/apt/lists/*
38
+
39
+ # 2. [span_3](start_span)将 JupyterLab 注入到虚拟环境中以驱动原项目的 Terminal 终端[span_3](end_span)
40
+ RUN uv pip install --python /opt/hermes/.venv/bin/python --no-cache-dir \
41
+ "jupyterlab>=4.0,<5" \
42
+ "tornado>=6.4" \
43
+ "ipywidgets>=8.1" \
44
+ && printf 'hermes ALL=(ALL) NOPASSWD: ALL\n' > /etc/sudoers.d/hermes \
45
+ && chmod 0440 /etc/sudoers.d/hermes \
46
+ && /usr/sbin/visudo -cf /etc/sudoers.d/hermes
47
+
48
+ # 3. 复制魔改项目的全部核心组件
49
+ COPY --chown=hermes:hermes start.sh /opt/huggingmes/start.sh
50
+ COPY --chown=hermes:hermes health-server.js /opt/huggingmes/health-server.js
51
+ COPY --chown=hermes:hermes cloudflare-proxy-setup.py /opt/huggingmes/cloudflare-proxy-setup.py
52
+ COPY --chown=hermes:hermes cloudflare-keepalive-setup.py /opt/huggingmes/cloudflare-keepalive-setup.py
53
+ COPY --chown=hermes:hermes env-builder.html /opt/huggingmes/env-builder.html
54
+ COPY --chown=hermes:hermes env-builder.js /opt/huggingmes/env-builder.js
55
+
56
+ RUN chmod +x \
57
+ /opt/huggingmes/start.sh \
58
+ /opt/huggingmes/cloudflare-proxy-setup.py \
59
+ /opt/huggingmes/cloudflare-keepalive-setup.py
60
+
61
+ # 4. [span_4](start_span)保留原项目看板数据库迁移幂等补丁[span_4](end_span)
62
+ RUN python3 - <<'PY'
63
+ import sys
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  try:
65
+ from pathlib import Path
66
+
67
+ p = Path("/opt/hermes/hermes_cli/kanban_db.py")
68
+ if not p.exists():
69
+ print("kanban patch: file not found, skipping")
70
+ sys.exit(0)
71
+
72
+ src = p.read_text(encoding="utf-8", errors="replace")
73
+ sentinel = "# huggingmes: idempotent-alter"
74
+ if sentinel in src:
75
+ print("kanban patch: already applied, skipping")
76
+ sys.exit(0)
77
+
78
+ old = (
79
+ ' conn.execute(\n'
80
+ ' "ALTER TABLE tasks ADD COLUMN consecutive_failures "\n'
81
+ ' "INTEGER NOT NULL DEFAULT 0"\n'
82
+ ' )'
83
+ )
84
+ new = (
85
+ f' try: {sentinel}\n'
86
+ ' conn.execute(\n'
87
+ ' "ALTER TABLE tasks ADD COLUMN consecutive_failures "\n'
88
+ ' "INTEGER NOT NULL DEFAULT 0"\n'
89
+ ' )\n'
90
+ ' except Exception:\n'
91
+ ' pass'
92
+ )
93
+
94
+ if old not in src:
95
+ print("kanban patch: pattern not found, may be fixed upstream, skipping")
96
+ sys.exit(0)
97
+
98
+ p.write_text(src.replace(old, new), encoding="utf-8")
99
+ print("kanban patch: applied")
100
+ except Exception as e:
101
+ print(f"kanban patch: error ({e}), skipping", file=sys.stderr)
 
 
 
 
 
 
 
 
102
  PY
103
 
104
+ # 5. [span_5](start_span)坚决维持原项目持久化存储桶挂载路径 /opt/data[span_5](end_span)
105
+ RUN echo 'export PATH="/opt/hermes/.venv/bin:/opt/data/.local/bin:$PATH"' \
106
+ > /etc/profile.d/hermes-venv.sh
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
 
108
+ # 环境变量修正:完全对齐官方与魔改共存的工作空间路径
109
+ ENV HERMES_HOME=/opt/data \
110
+ HUGGINGMES_APP_DIR=/opt/huggingmes \
111
+ HERMES_AGENT_VERSION=${HERMES_AGENT_VERSION} \
112
+ PYTHONUNBUFFERED=1 \
113
+ PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium
114
 
115
+ EXPOSE 7861
 
 
 
 
 
 
 
 
116
 
117
+ # [span_6](start_span)维持 HF Spaces 健康检查探针[span_6](end_span)
118
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=60s \
119
+ CMD curl -fsS http://localhost:7861/health || exit 1
 
 
120
 
121
+ # root 入口启动,用于在容器挂载冷启动时清洗和重置权限
122
+ CMD ["/opt/huggingmes/start.sh"]