#!/bin/bash # Entrypoint script for Hermes Agent on Hugging Face Spaces # 基于 Hermes Agent 真实 config.yaml 格式(source: cli-config.yaml.example + hermes_cli/config.py) # # 启动架构: # entrypoint.sh # ├── data_sync daemon (后台,数据持久化) # ├── Agent Bridge (后台,Unix socket IPC) ← 新增 # ├── hermes gateway run (后台,API Server :8642 + 消息平台) # └── node /opt/hermes-web-ui/dist/server/index.js (前台,BFF :7860, 替代 hermes dashboard) # set -e echo "🚀 Hermes Agent v0.10.0 - Hugging Face Spaces" echo "==============================================" # 检查必要的环境变量 if [ -z "$HF_DATASET_REPO" ]; then echo "⚠️ 警告:HF_DATASET_REPO 未设置,数据将不会持久化到 Dataset" fi # ==================== 初始化目录 ==================== echo "📁 初始化目录结构..." mkdir -p /data/.hermes/{cron,sessions,logs,memories,skills,pairing,hooks,image_cache,audio_cache,whatsapp/session} mkdir -p /data/.hermes-web-ui mkdir -p /app/logs # ==================== 数据恢复 ==================== export SKIP_CONFIG_RESTORE=true if [ -n "$HF_DATASET_REPO" ]; then echo "📥 从 Dataset 恢复数据..." python -m src.data_sync restore || { echo "⚠️ 数据恢复失败,使用空配置启动" } fi # ==================== 会话模型同步 ==================== echo "🔄 同步历史会话模型配置..." python3 << 'SESSION_SYNC' import json, os, glob from pathlib import Path current_model = os.environ.get('HERMES_MODEL', os.environ.get('MODEL_NAME', '')) if not current_model: print(" ⚠️ 未配置模型,跳过会话同步") exit(0) sessions_dir = Path('/data/.hermes/sessions') if not sessions_dir.exists(): print(" ⚠️ 会话目录不存在") exit(0) updated = 0 for session_file in sessions_dir.glob('*.json'): try: with open(session_file, 'r', encoding='utf-8') as f: data = json.load(f) if 'original_model' not in data and 'model' in data: data['original_model'] = data['model'] if data.get('model') != current_model: data['model'] = current_model with open(session_file, 'w', encoding='utf-8') as f: json.dump(data, f, ensure_ascii=False, indent=2) updated += 1 except Exception as e: print(f" ⚠️ 处理 {session_file.name} 失败:{e}") print(f" ✅ 已同步 {updated} 个会话的模型配置") SESSION_SYNC # ==================== 模型配置系统 ==================== echo "🤖 配置模型系统..." # ---- 供应商定义 ---- declare -A PROVIDER_MODELS=( ["xai"]="grok-4.3" ["nvidia"]="nvidia/nemotron-3-super-120b-a12b" ["siliconflow"]="deepseek-ai/DeepSeek-V4-Flash" ["openai"]="gpt-4o" ["anthropic"]="claude-3-5-sonnet-20241022" ["google"]="gemini-2.0-flash" ["gemini"]="gemini-2.5-flash" ["openrouter"]="meta-llama/llama-3.1-8b-instruct:free" ["longcat"]="LongCat-Flash-Thinking-2601" ) # ---- 供应商 Fallback 模型 ---- declare -A PROVIDER_FALLBACK_MODELS=( ["nvidia"]="nvidia/nemotron-3-nano-30b-a3b" ) declare -A PROVIDER_API_KEYS=( ["xai"]="XAI_API_KEY" ["nvidia"]="NVIDIA_API_KEY" ["siliconflow"]="SILICONFLOW_API_KEY" ["openai"]="OPENAI_API_KEY" ["anthropic"]="ANTHROPIC_API_KEY" ["google"]="GOOGLE_API_KEY" ["gemini"]="GEMINI_API_KEY" ["openrouter"]="OPENROUTER_API_KEY" ["longcat"]="LONGCAT_API_KEY" ) declare -A PROVIDER_BASE_URLS=( ["xai"]="https://api.x.ai/v1" ["nvidia"]="https://integrate.api.nvidia.com/v1" ["siliconflow"]="https://api.siliconflow.cn/v1" ["openai"]="https://api.openai.com/v1" ["anthropic"]="https://api.anthropic.com/v1" ["google"]="https://generativelanguage.googleapis.com" ["gemini"]="https://generativelanguage.googleapis.com" ["openrouter"]="https://openrouter.ai/api/v1" ["longcat"]="https://api.longcat.chat/openai" ) # ---- 检测主模型 ---- detect_main_model() { if [ -n "$MODEL_PROVIDER" ] && [ -n "$MODEL_NAME" ]; then echo "manual:$MODEL_PROVIDER:$MODEL_NAME" return fi for provider in xai nvidia siliconflow openai anthropic google openrouter longcat; do api_key_var="${PROVIDER_API_KEYS[$provider]}" if [ -n "${!api_key_var}" ]; then if [ -n "$MODEL_NAME" ]; then echo "auto:$provider:$MODEL_NAME" else echo "auto:$provider:${PROVIDER_MODELS[$provider]}" fi return fi done if [ -n "$GEMINI_API_KEY" ]; then echo "auto:gemini:${PROVIDER_MODELS[gemini]}" return fi echo "default:nvidia:${PROVIDER_MODELS[nvidia]}" } # Hermes Gateway 实际使用的 provider 名称映射 map_provider_for_gateway() { local provider="$1" case "$provider" in siliconflow) echo "custom" ;; *) echo "$provider" ;; esac } # ---- 检测辅助模型 ---- detect_vision_model() { if [ -n "$VISION_MODEL" ]; then echo "$VISION_MODEL"; return; fi if [ -n "$NVIDIA_API_KEY" ]; then echo "meta/llama-3.2-11b-vision-instruct"; return; fi echo "" } detect_aux_model() { if [ -n "$AUX_MODEL" ]; then echo "$AUX_MODEL"; return; fi if [ -n "$OPENROUTER_API_KEY" ]; then echo "google/gemini-3-flash-preview"; return; fi if [ -n "$NVIDIA_API_KEY" ]; then echo "openai/gpt-oss-120b"; return; fi echo "" } detect_delegation_model() { if [ -n "$DELEGATION_MODEL" ]; then echo "$DELEGATION_MODEL"; return; fi if [ -n "$NVIDIA_API_KEY" ]; then echo "moonshotai/kimi-k2.6"; return; fi echo "" } # ---- 执行检测 ---- echo "" echo "📋 模型配置检测:" echo "────────────────────────────────────────" MAIN_DETECTED=$(detect_main_model) IFS=':' read -r MAIN_MODE MAIN_PROVIDER MAIN_MODEL <<< "$MAIN_DETECTED" echo "🎯 Main Model: $MAIN_PROVIDER/$MAIN_MODEL (模式:$MAIN_MODE)" # ---- Fallback 模型 ---- FALLBACK_MODEL="${PROVIDER_FALLBACK_MODELS[$MAIN_PROVIDER]:-}" if [ -n "$FALLBACK_MODEL" ]; then echo "🔄 Fallback Model: $FALLBACK_MODEL (when busy)" fi VISION_MODEL_VAL=$(detect_vision_model) echo "👁️ Vision Model: ${VISION_MODEL_VAL:-auto-detect}" AUX_MODEL_VAL=$(detect_aux_model) echo "⚡ Aux Model: ${AUX_MODEL_VAL:-auto-detect}" DELEGATION_MODEL_VAL=$(detect_delegation_model) echo "💻 Delegation Model: ${DELEGATION_MODEL_VAL:-inherit-main}" MAIN_BASE_URL="${PROVIDER_BASE_URLS[$MAIN_PROVIDER]}" echo " Base URL: $MAIN_BASE_URL" echo "────────────────────────────────────────" # ==================== 生成 config.yaml ==================== CONFIG_FILE="/data/.hermes/config.yaml" echo "📝 生成 config.yaml (Hermes 真实格式)..." infer_provider() { local model_id="$1" if [[ "$model_id" == google/* ]]; then echo "google" elif [[ "$model_id" == openrouter/* ]]; then echo "openrouter" elif [[ "$model_id" == Pro/* ]]; then echo "custom" else echo "$MAIN_PROVIDER"; fi } GATEWAY_MAIN_PROVIDER=$(map_provider_for_gateway "$MAIN_PROVIDER") VISION_PROVIDER_VAL=$(map_provider_for_gateway "$(infer_provider "$VISION_MODEL_VAL")") AUX_PROVIDER_VAL=$(map_provider_for_gateway "$(infer_provider "$AUX_MODEL_VAL")") DELEGATION_PROVIDER_VAL=$(map_provider_for_gateway "$(infer_provider "$DELEGATION_MODEL_VAL")") echo "🔄 Gateway provider 映射:$MAIN_PROVIDER → $GATEWAY_MAIN_PROVIDER" cat > "$CONFIG_FILE" << EOF # Hermes Agent Configuration # Generated by entrypoint.sh at $(date -Iseconds) # 主模型配置 model: default: "$MAIN_MODEL" fallback: "${FALLBACK_MODEL:-}" provider: "$GATEWAY_MAIN_PROVIDER" base_url: "$MAIN_BASE_URL" # 辅助模型配置 (per-task overrides) auxiliary: vision: provider: "${VISION_PROVIDER_VAL:-auto}" model: "${VISION_MODEL_VAL}" timeout: 120 download_timeout: 30 web_extract: provider: "${AUX_PROVIDER_VAL:-auto}" model: "${AUX_MODEL_VAL}" timeout: 360 compression: provider: "${AUX_PROVIDER_VAL:-auto}" model: "${AUX_MODEL_VAL}" timeout: 120 title_generation: provider: "${AUX_PROVIDER_VAL:-auto}" model: "${AUX_MODEL_VAL}" timeout: 30 session_search: provider: "auto" model: "" timeout: 30 skills_hub: provider: "auto" model: "" timeout: 30 approval: provider: "auto" model: "" timeout: 30 mcp: provider: "auto" model: "" timeout: 30 flush_memories: provider: "auto" model: "" timeout: 30 # 子代理 (Delegation) 配置 delegation: model: "${DELEGATION_MODEL_VAL}" provider: "${DELEGATION_PROVIDER_VAL}" max_iterations: 50 reasoning_effort: "medium" # API Server 配置 api_server: enabled: true port: 8642 host: "127.0.0.1" # 终端配置 terminal: backend: local timeout: 300 shell: /bin/bash # 显示配置 display: skin: default show_tool_progress: true show_resume: true spinner: dots # Agent 配置 agent: max_iterations: 50 approval_mode: ask dangerous_command_approval: ask gateway_timeout: 300 # 记忆配置 memory: enabled: true provider: local # 压缩配置 compression: enabled: true threshold: 0.50 # 定时任务 cron: enabled: true tick_interval: 60 EOF echo " ✅ 配置文件已生成" # ==================== 合并用户配置 ==================== RESTORED_CONFIG="/data/.hermes/config.yaml.restored" if [ -f "$RESTORED_CONFIG" ]; then echo "🔄 合并用户配置 (platforms, display, agent 等)..." python3 << 'MERGE_SCRIPT' import yaml import sys GENERATED = '/data/.hermes/config.yaml' RESTORED = '/data/.hermes/config.yaml.restored' ENTRYPOINT_PRIORITY = {'model', 'auxiliary', 'delegation', 'api_server'} USER_PRIORITY = {'platforms', 'display', 'agent', 'memory', 'compression', 'cron', 'terminal'} try: with open(GENERATED) as f: generated = yaml.safe_load(f) or {} with open(RESTORED) as f: restored = yaml.safe_load(f) or {} merged = {} all_keys = set(list(generated.keys()) + list(restored.keys())) for key in all_keys: if key in ENTRYPOINT_PRIORITY: if key in generated: merged[key] = generated[key] elif key in USER_PRIORITY: if key in restored: merged[key] = restored[key] elif key in generated: merged[key] = generated[key] else: if key in restored: merged[key] = restored[key] elif key in generated: merged[key] = generated[key] with open(GENERATED, 'w') as f: yaml.dump(merged, f, default_flow_style=False, allow_unicode=True, sort_keys=False) merged_user_keys = [k for k in USER_PRIORITY if k in restored] print(f" ✅ 已合并用户区块:{', '.join(merged_user_keys) if merged_user_keys else '无'}") except Exception as e: print(f" ⚠️ 合并配置失败:{e},使用生成的默认配置") sys.exit(0) MERGE_SCRIPT rm -f "$RESTORED_CONFIG" else echo " ℹ️ 无需合并(无恢复的用户配置)" fi # ==================== 导出供应商环境变量 ==================== echo "🌐 设置供应商环境变量..." for var in XAI_API_KEY NVIDIA_API_KEY SILICONFLOW_API_KEY OPENAI_API_KEY ANTHROPIC_API_KEY GOOGLE_API_KEY GEMINI_API_KEY OPENROUTER_API_KEY LONGCAT_API_KEY; do if [ -n "${!var}" ]; then export "$var" fi done if [ -n "$XAI_API_KEY" ]; then export XAI_BASE_URL="${XAI_BASE_URL:-https://api.x.ai/v1}" fi if [ -n "$NVIDIA_API_KEY" ]; then export NVIDIA_BASE_URL="${NVIDIA_BASE_URL:-https://integrate.api.nvidia.com/v1}" fi if [ -n "$SILICONFLOW_API_KEY" ]; then export SILICONFLOW_BASE_URL="${SILICONFLOW_BASE_URL:-https://api.siliconflow.cn/v1}" if [ -z "$OPENAI_API_KEY" ]; then export OPENAI_API_KEY="$SILICONFLOW_API_KEY" export OPENAI_BASE_URL="${SILICONFLOW_BASE_URL:-https://api.siliconflow.cn/v1}" echo " ℹ️ 已将 SILICONFLOW_API_KEY 复制到 OPENAI_API_KEY (Gateway 兼容性)" fi fi if [ -n "$GEMINI_API_KEY" ]; then export GEMINI_BASE_URL="${GEMINI_BASE_URL:-https://generativelanguage.googleapis.com}" fi if [ -n "$OPENROUTER_API_KEY" ]; then export OPENROUTER_BASE_URL="${OPENROUTER_BASE_URL:-https://openrouter.ai/api/v1}" fi if [ -n "$LONGCAT_API_KEY" ]; then export LONGCAT_BASE_URL="${LONGCAT_BASE_URL:-https://api.longcat.chat/openai}" fi # 导出 API Server 环境变量 export API_SERVER_ENABLED=true export API_SERVER_PORT=8642 export API_SERVER_HOST=127.0.0.1 # 生成或复用 API Server 密钥 if [ -n "$API_SERVER_KEY" ]; then echo " ℹ️ 使用已有的 API_SERVER_KEY" else API_SERVER_KEY=$(python3 -c "import secrets; print(secrets.token_hex(32))") echo " 🔑 已生成新的 API_SERVER_KEY" fi export API_SERVER_KEY # 默认允许所有用户 export GATEWAY_ALLOW_ALL_USERS="${GATEWAY_ALLOW_ALL_USERS:-true}" # 导出 HERMES_HOME export HERMES_HOME=/data/.hermes # 导出 HERMES_MODEL export HERMES_MODEL="$MAIN_MODEL" echo " ✅ API Key 环境变量已导出" echo " ✅ Base URL 环境变量已设置" echo " ✅ API Server 环境变量已设置 (端口:8642)" echo " ✅ HERMES_HOME=$HERMES_HOME" echo " ✅ HERMES_MODEL=$HERMES_MODEL (进程级模型覆盖)" # ==================== 环境变量注入 ==================== echo "⚙️ 注入环境变量到 .env..." ENV_FILE="/data/.hermes/.env" mkdir -p /data/.hermes PERSISTENT_VARS=( "MODEL_PROVIDER" "MODEL_NAME" "HERMES_MODEL" "VISION_MODEL" "AUX_MODEL" "DELEGATION_MODEL" "XAI_API_KEY" "XAI_BASE_URL" "NVIDIA_API_KEY" "NVIDIA_BASE_URL" "SILICONFLOW_API_KEY" "SILICONFLOW_BASE_URL" "OPENAI_API_KEY" "ANTHROPIC_API_KEY" "GOOGLE_API_KEY" "GEMINI_API_KEY" "GEMINI_BASE_URL" "OPENROUTER_API_KEY" "OPENROUTER_BASE_URL" "LONGCAT_API_KEY" "LONGCAT_BASE_URL" "API_SERVER_ENABLED" "API_SERVER_PORT" "API_SERVER_HOST" "TELEGRAM_BOT_TOKEN" "TELEGRAM_ALLOWED_USERS" "TELEGRAM_PROXY" "DISCORD_BOT_TOKEN" "DISCORD_CLIENT_ID" "SLACK_BOT_TOKEN" "SLACK_APP_TOKEN" "SLACK_SIGNING_SECRET" "WHATSAPP_BUSINESS_ID" "WHATSAPP_PHONE_NUMBER" "WHATSAPP_ACCESS_TOKEN" "WEIXIN_ACCOUNT_ID" "WEIXIN_TOKEN" "WEIXIN_BASE_URL" "GATEWAY_ALLOW_ALL_USERS" "AUTH_TOKEN" "HF_TOKEN" "HF_DATASET_REPO" "API_SERVER_ENABLED" "API_SERVER_PORT" "API_SERVER_HOST" "API_SERVER_KEY" ) declare -A env_entries=() if [ -f "$ENV_FILE" ]; then while IFS= read -r line; do [[ "$line" =~ ^[[:space:]]*# ]] && continue [[ -z "${line// }" ]] && continue eq_idx="${line%%=*}" if [ -n "$eq_idx" ] && [ "$eq_idx" != "$line" ]; then env_entries["$eq_idx"]="$line" fi done < "$ENV_FILE" fi for var in "${PERSISTENT_VARS[@]}"; do if [ -n "${!var}" ]; then env_entries["$var"]="${var}=${!var}" fi done { for key in "${!env_entries[@]}"; do echo "${env_entries[$key]}" done } | sort > "$ENV_FILE" RESTORED_COUNT=$(grep -c '=' "$ENV_FILE") echo " ✅ 已写入 ${RESTORED_COUNT} 个环境变量(含恢复的持久化变量)" # ==================== 启动 Agent Bridge (必须在 Gateway 之前) ==================== echo "🔌 启动 Agent Bridge..." BRIDGE_ENDPOINT="ipc:///tmp/hermes-agent-bridge.sock" # 自动检测正确的 agent-root(优先尝试 site-packages) BRIDGE_AGENT_ROOT="" for candidate in "/usr/local/lib/python3.11/site-packages/" "$HOME/.hermes/hermes-agent" "/opt/hermes/hermes-agent"; do if [ -f "$candidate/run_agent.py" ] || [ -d "$candidate/hermes_cli" ]; then BRIDGE_AGENT_ROOT="$candidate" break fi done if [ -z "$BRIDGE_AGENT_ROOT" ]; then BRIDGE_AGENT_ROOT="/usr/local/lib/python3.11/site-packages/" fi BRIDGE_HERMES_HOME="${HERMES_HOME:-$HOME/.hermes}" # 确保 bridge 脚本存在 BRIDGE_SCRIPT="/opt/hermes-web-ui/dist/server/agent-bridge/hermes_bridge.py" if [ -f "$BRIDGE_SCRIPT" ]; then # 清理旧的 socket 文件 rm -f /tmp/hermes-agent-bridge.sock # 启动 bridge 服务(后台运行) python3 "$BRIDGE_SCRIPT" \ --endpoint "$BRIDGE_ENDPOINT" \ --agent-root "$BRIDGE_AGENT_ROOT" \ --hermes-home "$BRIDGE_HERMES_HOME" \ --worker-profile default \ > /data/.hermes/logs/bridge.log 2>&1 & BRIDGE_PID=$! echo " ✅ Bridge 服务已启动 (PID: $BRIDGE_PID, Agent-Root: $BRIDGE_AGENT_ROOT)" # 智能等待 bridge socket 创建(最多 10 秒) echo " ⏳ 等待 Bridge socket 就绪..." BRIDGE_READY=false for i in $(seq 1 10); do if [ -S "/tmp/hermes-agent-bridge.sock" ]; then BRIDGE_READY=true break fi # 检查进程是否还活着 if ! kill -0 $BRIDGE_PID 2>/dev/null; then echo " ⚠️ Bridge 进程意外退出,查看日志:tail -f /data/.hermes/logs/bridge.log" break fi sleep 1 done if [ "$BRIDGE_READY" = true ]; then echo " ✅ Bridge socket 已就绪:/tmp/hermes-agent-bridge.sock" else echo " ⚠️ Bridge socket 未就绪,尝试备用 agent-root 重启..." # 尝试备用路径 for alt_root in "/usr/local/lib/python3.11/site-packages/" "$HOME/.hermes/hermes-agent"; do if [ "$alt_root" != "$BRIDGE_AGENT_ROOT" ]; then rm -f /tmp/hermes-agent-bridge.sock python3 "$BRIDGE_SCRIPT" --endpoint "$BRIDGE_ENDPOINT" --agent-root "$alt_root" --hermes-home "$BRIDGE_HERMES_HOME" --worker-profile default > /data/.hermes/logs/bridge.log 2>&1 & BRIDGE_PID=$! echo " 🔄 尝试备用路径:$alt_root (PID: $BRIDGE_PID)" sleep 2 if [ -S "/tmp/hermes-agent-bridge.sock" ]; then echo " ✅ Bridge socket 在备用路径就绪:$alt_root" break fi fi done fi if [ "$BRIDGE_READY" = true ]; then # 最终验证:检查进程是否存活 if ! kill -0 $BRIDGE_PID 2>/dev/null; then echo " ⚠️ Bridge 进程已退出,但 socket 仍存在(可能是僵尸进程)" fi fi else echo " ❌ Bridge 脚本未找到:$BRIDGE_SCRIPT" echo " ⚠️ 将跳过 Bridge 启动,但某些功能可能不可用" fi # ==================== 启动 Gateway (API Server + 消息平台) ==================== echo "📡 启动 Hermes Gateway + API Server..." MAIN_API_KEY_VAR="${PROVIDER_API_KEYS[$MAIN_PROVIDER]}" MAIN_API_KEY_VAL="${!MAIN_API_KEY_VAR}" echo "🔍 Gateway 配置诊断:" echo " 原始 Provider: $MAIN_PROVIDER" echo " Gateway Provider: $GATEWAY_MAIN_PROVIDER" echo " Model: $MAIN_MODEL" echo " Base URL: $MAIN_BASE_URL" if [ -n "$MAIN_API_KEY_VAL" ]; then echo " API Key ($MAIN_API_KEY_VAR): 已设置 (长度:${#MAIN_API_KEY_VAL})" else echo " ⚠️ API Key ($MAIN_API_KEY_VAR): 未设置!Provider authentication 可能失败" fi echo "📋 可用的 Provider 列表:" hermes model 2>/dev/null | head -30 || echo " ⚠️ 无法获取 provider 列表" echo "🔧 运行配置诊断:" hermes doctor 2>/dev/null | grep -E "(provider|model|config|error|warning)" | head -20 || echo " ⚠️ hermes doctor 不可用" GATEWAY_PIDFILE="/data/.hermes/gateway.pid" # Gateway 包装器:自动重启 + 崩溃恢复 ( while true; do hermes gateway run --replace 2>&1 | while IFS= read -r line; do echo "$line" case "$line" in *"Gateway failed to connect"*) echo " ⚠️ 网关消息平台连接失败,API Server 仍可使用,30 秒后重试..." ;; esac done EXIT_CODE=${PIPESTATUS[0]} if [ "$EXIT_CODE" -ne 0 ]; then echo " ⚠️ 网关进程退出 (code=$EXIT_CODE),30 秒后重启..." sleep 30 else echo " 🛑 网关正常退出(可能被 BFF restartGateway 替换)" sleep 5 if [ -f "$GATEWAY_PIDFILE" ]; then NEW_PID=$(python3 -c "import json; print(json.load(open('$GATEWAY_PIDFILE')).get('pid',0))" 2>/dev/null || echo 0) if [ "$NEW_PID" -gt 0 ] && kill -0 "$NEW_PID" 2>/dev/null; then echo " 🔄 检测到新网关进程 (PID: $NEW_PID),等待其退出..." while kill -0 "$NEW_PID" 2>/dev/null; do sleep 5; done echo " ⚠️ 新网关进程已退出,30 秒后重启包装器..." sleep 30 continue fi fi echo " 🛑 无新网关进程,不再重启" break fi done ) & GATEWAY_PID=$! # 等待 API Server 就绪 echo " ⏳ 等待 API Server 就绪 (:8642)..." API_READY=false for i in $(seq 1 30); do if curl -sf http://127.0.0.1:8642/health > /dev/null 2>&1; then API_READY=true break fi sleep 1 done if [ "$API_READY" = true ]; then echo " ✅ API Server 已就绪 (http://127.0.0.1:8642)" else echo " ⚠️ API Server 未在 30 秒内就绪,继续启动 Web UI(API Server 可能稍后可用)" fi if kill -0 $GATEWAY_PID 2>/dev/null; then echo " ✅ 网关进程运行中 (PID: $GATEWAY_PID)" else echo " ⚠️ 网关进程已退出,仅 Web UI 可用" fi echo "" echo "💡 提示:" echo " - Channels 页面可配置微信/飞书/企业微信等平台" echo " - Models 页面可管理模型供应商" echo " - Jobs 页面可管理定时任务" echo "" # ==================== Auth Token 处理 ==================== echo "🔑 配置 Web UI 认证..." if [ -z "$AUTH_TOKEN" ]; then AUTH_TOKEN_FILE="/data/.hermes-web-ui/.token" if [ -f "$AUTH_TOKEN_FILE" ]; then AUTH_TOKEN=$(cat "$AUTH_TOKEN_FILE") echo " ✅ 已恢复 Web UI 认证 Token" else AUTH_TOKEN=$(openssl rand -hex 16 2>/dev/null || head -c 32 /dev/urandom | xxd -p | head -c 32) mkdir -p /data/.hermes-web-ui echo "$AUTH_TOKEN" > "$AUTH_TOKEN_FILE" echo "" echo " ╔══════════════════════════════════════════════════╗" echo " ║ 🔑 Web UI 认证 Token (请保存!) ║" echo " ║ $AUTH_TOKEN" echo " ║ ║" echo " ║ 在 Web UI 登录页面输入此 Token ║" echo " ║ 也可在 HF Spaces Settings 设置 AUTH_TOKEN 覆盖 ║" echo " ╚══════════════════════════════════════════════════╝" echo "" fi else echo " ✅ 使用环境变量中的 AUTH_TOKEN" fi export AUTH_TOKEN # ==================== 修复 hermes-web-ui v0.5.30+ agent bridge ==================== echo "🔧 修复 agent bridge..." BRIDGE_PATHS=( "/opt/hermes-web-ui/dist/server/agent-bridge/hermes_bridge.py" "/usr/lib/node_modules/hermes-web-ui/dist/server/agent-bridge/hermes_bridge.py" ) BRIDGE_FOUND=false for bridge_path in "${BRIDGE_PATHS[@]}"; do if [ -f "$bridge_path" ]; then chmod +x "$bridge_path" echo " ✅ bridge 脚本权限已修复:$bridge_path" BRIDGE_FOUND=true fi done if [ "$BRIDGE_FOUND" = false ]; then echo " ⚠️ 未在常用路径找到 bridge 脚本,尝试全局搜索..." find /opt /usr -name "hermes_bridge.py" -type f 2>/dev/null | while read -r f; do chmod +x "$f" echo " ✅ 已设置权限:$f" done fi # 确保 hermes 模块可被 bridge 加载 echo "🔍 查找 hermes 包路径..." HERMES_PKG_PATH="" if [ -z "$HERMES_PKG_PATH" ]; then for candidate in $(find /usr/local/lib/python*/site-packages -maxdepth 1 -type d \( -name "hermes" -o -name "hermes_agent" -o -name "hermes_cli" \) 2>/dev/null); do if [ -f "$candidate/__init__.py" ]; then HERMES_PKG_PATH="$candidate" echo " ✅ 方法 1 找到 hermes 包:$HERMES_PKG_PATH" break fi done fi if [ -z "$HERMES_PKG_PATH" ]; then PIP_LOCATION=$(python3 -m pip show hermes-agent 2>/dev/null | grep ^Location | cut -d' ' -f2-) if [ -n "$PIP_LOCATION" ]; then for name in hermes hermes_agent hermes_cli; do if [ -d "$PIP_LOCATION/$name" ] && [ -f "$PIP_LOCATION/$name/__init__.py" ]; then HERMES_PKG_PATH="$PIP_LOCATION/$name" echo " ✅ 方法 2 找到 hermes 包:$HERMES_PKG_PATH" break fi done fi fi if [ -z "$HERMES_PKG_PATH" ]; then HERMES_CMD=$(which hermes 2>/dev/null) if [ -n "$HERMES_CMD" ]; then HERMES_MODULE=$(head -50 "$HERMES_CMD" 2>/dev/null | grep -E "from|import" | grep -oE "hermes[a-z_]*" | head -1) if [ -n "$HERMES_MODULE" ]; then HERMES_TRY=$(python3 -c "import $HERMES_MODULE; print($HERMES_MODULE.__path__[0])" 2>/dev/null) if [ -n "$HERMES_TRY" ]; then HERMES_PKG_PATH="$HERMES_TRY" echo " ✅ 方法 3 找到 hermes 包:$HERMES_PKG_PATH (模块:$HERMES_MODULE)" fi fi fi fi if [ -z "$HERMES_PKG_PATH" ]; then HERMES_PKG_PATH=$(python3 -c " import sys, os for p in sys.path: for name in ['hermes', 'hermes_agent', 'hermes_cli']: candidate = os.path.join(p, name) if os.path.isdir(candidate) and os.path.exists(os.path.join(candidate, '__init__.py')): print(candidate) sys.exit(0) print('') " 2>/dev/null) if [ -n "$HERMES_PKG_PATH" ]; then echo " ✅ 方法 4 找到 hermes 包:$HERMES_PKG_PATH" fi fi if [ -z "$HERMES_PKG_PATH" ] && [ -d "/usr/local/lib/hermes-agent" ]; then HERMES_PKG_PATH="/usr/local/lib/hermes-agent" echo " ✅ 方法 5 找到 hermes 包:$HERMES_PKG_PATH" fi HERMES_REAL_HOME=$(realpath ~/.hermes 2>/dev/null || echo "$HOME/.hermes") mkdir -p "$HERMES_REAL_HOME" if [ -n "$HERMES_PKG_PATH" ] && [ -d "$HERMES_PKG_PATH" ]; then if [ ! -e "$HERMES_REAL_HOME/hermes-agent" ] && [ ! -L "$HERMES_REAL_HOME/hermes-agent" ]; then ln -s "$HERMES_PKG_PATH" "$HERMES_REAL_HOME/hermes-agent" echo " ✅ hermes-agent 模块链接已创建 → $HERMES_PKG_PATH" fi if [ ! -e "$HERMES_REAL_HOME/hermes" ] && [ ! -L "$HERMES_REAL_HOME/hermes" ]; then ln -s "$HERMES_PKG_PATH" "$HERMES_REAL_HOME/hermes" echo " ✅ hermes 模块链接已创建 → $HERMES_PKG_PATH" fi PYTHON_SITE_PACKAGES=$(python3 -c "import site; print(site.getsitepackages()[0])" 2>/dev/null) if [ -n "$PYTHON_SITE_PACKAGES" ]; then export PYTHONPATH="${PYTHONPATH:+$PYTHONPATH:}$PYTHON_SITE_PACKAGES" echo " ✅ PYTHONPATH 已设置:$PYTHONPATH" fi echo "🔍 测试 bridge 脚本..." if [ -f "$BRIDGE_SCRIPT" ]; then python3 "$BRIDGE_SCRIPT" --help 2>&1 | head -5 || echo " ⚠️ bridge 脚本测试失败" fi else echo " ❌ 所有方法都无法找到 hermes 包路径!" echo " 📋 诊断信息:" echo " Python site-packages:" python3 -c "import site; [print(' ', p) for p in site.getsitepackages()]" 2>/dev/null || true echo " sys.path:" python3 -c "import sys; [print(' ', p) for p in sys.path if __import__('os').path.isdir(p)]" 2>/dev/null || true echo " pip list | grep hermes:" python3 -m pip list 2>/dev/null | grep -i hermes || echo " (无结果)" echo " which hermes: $(which hermes 2>/dev/null || echo '未找到')" fi # ==================== 启动 Web UI (BFF Server) ==================== echo "🌐 启动 Hermes Web UI..." echo " BFF Server: http://0.0.0.0:7860" echo " Upstream: http://127.0.0.1:8642" echo "" export PORT=7860 export UPSTREAM=http://127.0.0.1:8642 export HERMES_BIN=/usr/local/bin/hermes export HERMES_HOME=/data/.hermes # 优雅关闭 cleanup() { echo "" echo "🛑 执行清理..." if [ -n "$HF_DATASET_REPO" ]; then echo " 💾 执行最终数据备份..." python -m src.data_sync backup --force 2>/dev/null || echo " ⚠️ 备份失败" fi # 停止各进程(顺序:BFF → Bridge → Gateway → Sync) if [ -n "$BRIDGE_PID" ] && kill -0 $BRIDGE_PID 2>/dev/null; then echo " 🛑 停止 Agent Bridge..." kill $BRIDGE_PID 2>/dev/null || true wait $BRIDGE_PID 2>/dev/null || true rm -f /tmp/hermes-agent-bridge.sock 2>/dev/null fi if [ -n "$BFF_PID" ] && kill -0 $BFF_PID 2>/dev/null; then echo " 🛑 停止 Web UI..." kill $BFF_PID 2>/dev/null || true wait $BFF_PID 2>/dev/null || true fi if [ -n "$GATEWAY_PID" ] && kill -0 $GATEWAY_PID 2>/dev/null; then echo " 🛑 停止 Gateway..." kill $GATEWAY_PID 2>/dev/null || true wait $GATEWAY_PID 2>/dev/null || true fi if kill -0 $SYNC_PID 2>/dev/null; then echo " 🛑 停止数据同步..." kill $SYNC_PID 2>/dev/null || true wait $SYNC_PID 2>/dev/null || true fi echo "👋 再见!" exit 0 } trap cleanup SIGTERM SIGINT # 启动 BFF Server (直接绑定 7860 端口) PORT=7860 node /opt/hermes-web-ui/dist/server/index.js & BFF_PID=$! # 等待 BFF 就绪 echo " ⏳ 等待 Web UI 就绪..." BFF_READY=false for i in $(seq 1 20); do if curl -sf http://localhost:7860/health > /dev/null 2>&1; then BFF_READY=true break fi sleep 1 done if [ "$BFF_READY" = true ]; then echo " ✅ Web UI 已就绪 → http://localhost:7860" else echo " ⚠️ Web UI 未在 20 秒内就绪,请查看日志" fi # 再次验证模型配置 if [ -f "$CONFIG_FILE" ]; then if command -v yq &>/dev/null; then ACTUAL_MODEL=$(yq '.model.default' "$CONFIG_FILE" 2>/dev/null) if [ -n "$ACTUAL_MODEL" ] && [ "$ACTUAL_MODEL" != "$MAIN_MODEL" ] && [ "$ACTUAL_MODEL" != "null" ]; then echo " ⚠️ 检测到模型被 BFF 启动流程覆盖!" echo " 📋 期望:$MAIN_MODEL, 实际:$ACTUAL_MODEL" echo " 🔒 重新写入正确的模型配置..." yq -i ".model.default = \"$MAIN_MODEL\"" "$CONFIG_FILE" yq -i ".model.provider = \"$GATEWAY_MAIN_PROVIDER\"" "$CONFIG_FILE" yq -i ".model.base_url = \"$MAIN_BASE_URL\"" "$CONFIG_FILE" echo " ✅ 模型已修正:$GATEWAY_MAIN_PROVIDER/$MAIN_MODEL" elif [ -z "$ACTUAL_MODEL" ] || [ "$ACTUAL_MODEL" = "null" ]; then echo " ⚠️ 检测到模型字段为空!重新写入..." yq -i ".model.default = \"$MAIN_MODEL\"" "$CONFIG_FILE" yq -i ".model.provider = \"$GATEWAY_MAIN_PROVIDER\"" "$CONFIG_FILE" yq -i ".model.base_url = \"$MAIN_BASE_URL\"" "$CONFIG_FILE" echo " ✅ 模型已修正:$GATEWAY_MAIN_PROVIDER/$MAIN_MODEL" else echo " ✅ 模型配置验证通过:$GATEWAY_MAIN_PROVIDER/$MAIN_MODEL" fi # 验证 fallback 模型 if [ -n "$FALLBACK_MODEL" ]; then ACTUAL_FALLBACK=$(yq '.model.fallback' "$CONFIG_FILE" 2>/dev/null) if [ "$ACTUAL_FALLBACK" != "$FALLBACK_MODEL" ]; then yq -i ".model.fallback = \"$FALLBACK_MODEL\"" "$CONFIG_FILE" echo " 🔄 Fallback 模型已写入:$FALLBACK_MODEL" else echo " 🔄 Fallback 模型验证通过:$FALLBACK_MODEL" fi fi fi fi # 等待 BFF 主进程(前台阻塞,容器生命周期由 BFF 控制) wait $BFF_PID