| #!/bin/bash |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| 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" |
| ) |
|
|
| |
| 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]}" |
| } |
|
|
| |
| 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_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_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 |
|
|
| |
| export API_SERVER_ENABLED=true |
| export API_SERVER_PORT=8642 |
| export API_SERVER_HOST=127.0.0.1 |
|
|
| |
| 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}" |
|
|
| |
| export HERMES_HOME=/data/.hermes |
|
|
| |
| 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:]]* |
| [[ -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} 个环境变量(含恢复的持久化变量)" |
|
|
| |
| echo "🔌 启动 Agent Bridge..." |
| BRIDGE_ENDPOINT="ipc:///tmp/hermes-agent-bridge.sock" |
|
|
| |
| 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_SCRIPT="/opt/hermes-web-ui/dist/server/agent-bridge/hermes_bridge.py" |
|
|
| if [ -f "$BRIDGE_SCRIPT" ]; then |
| |
| rm -f /tmp/hermes-agent-bridge.sock |
| |
| |
| 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)" |
| |
| |
| 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 |
|
|
| |
| 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" |
|
|
| |
| ( |
| 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=$! |
|
|
| |
| 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 "" |
|
|
| |
| 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 |
|
|
| |
| 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 |
|
|
| |
| 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 |
|
|
| |
| 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 |
|
|
| |
| 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 |
|
|
| |
| PORT=7860 node /opt/hermes-web-ui/dist/server/index.js & |
| BFF_PID=$! |
|
|
| |
| 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 |
| |
| 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 |
|
|
| |
| wait $BFF_PID |