| #!/bin/bash |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| set -e |
|
|
| echo "🚀 Hermes Agent v0.10.0 - Hugging Face Spaces" |
| echo "==============================================" |
|
|
| |
| |
| export PATH="$PATH:/usr/local/bin:/home/appuser/.local/bin" |
|
|
| |
| 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 |
|
|
| echo "🔍 调试:初始化目录结构后检查" |
| pwd |
| ls -al /data/ |
| ls -al /data/.hermes/ |
| ls -al /data/.hermes/skills/ |
|
|
| |
| |
| export SKIP_CONFIG_RESTORE=true |
|
|
| if [ -n "$HF_DATASET_REPO" ]; then |
| echo "📥 从 Dataset 恢复数据..." |
| python -m src.data_sync restore || { |
| echo "⚠️ 数据恢复失败,使用空配置启动" |
| } |
| fi |
|
|
| |
| AGENT_SRC="/data/.hermes/hermes-agent" |
| if [ ! -f "$AGENT_SRC/run_agent.py" ]; then |
| echo "📥 克隆 hermes-agent 源码 (bridge 需要)..." |
| git clone -q --depth 1 --branch v0.15.1 https://github.com/NousResearch/hermes-agent.git "$AGENT_SRC" 2>/dev/null || echo " ⚠️ Clone failed, will retry later" |
| fi |
|
|
| |
| echo "🤖 配置模型系统..." |
|
|
| |
| declare -A PROVIDER_MODELS=( |
| ["nvidia"]="moonshotai/kimi-k2-thinking" |
| ["siliconflow"]="Pro/moonshotai/Kimi-K2.5" |
| ["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_API_KEYS=( |
| ["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=( |
| ["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 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]}" |
| } |
|
|
| |
| detect_vision_model() { |
| if [ -n "$VISION_MODEL" ]; then echo "$VISION_MODEL"; return; fi |
| if [ -n "$GEMINI_API_KEY" ] || [ -n "$GOOGLE_API_KEY" ]; then echo "google/gemini-2.5-flash"; 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 "$GEMINI_API_KEY" ] || [ -n "$GOOGLE_API_KEY" ]; then echo "google/gemini-2.0-flash"; return; fi |
| echo "" |
| } |
|
|
| detect_delegation_model() { |
| if [ -n "$DELEGATION_MODEL" ]; then echo "$DELEGATION_MODEL"; return; fi |
| if [ -n "$SILICONFLOW_API_KEY" ]; then echo "Pro/moonshotai/Kimi-K2.5"; 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)" |
|
|
| 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 "siliconflow" |
| else echo "$MAIN_PROVIDER"; fi |
| } |
|
|
| VISION_PROVIDER_VAL=$(infer_provider "$VISION_MODEL_VAL") |
| AUX_PROVIDER_VAL=$(infer_provider "$AUX_MODEL_VAL") |
| DELEGATION_PROVIDER_VAL=$(infer_provider "$DELEGATION_MODEL_VAL") |
|
|
| cat > "$CONFIG_FILE" << EOF |
| # Hermes Agent Configuration |
| # Generated by entrypoint.sh at $(date -Iseconds) |
| |
| # 主模型配置 |
| model: |
| default: "$MAIN_MODEL" |
| provider: "$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 配置 (Web UI BFF 的上游代理目标) |
| api_server: |
| enabled: true |
| port: 8642 |
| host: "127.0.0.1" |
| |
| # 终端配置 |
| terminal: |
| backend: local |
| timeout: 300 |
| shell: /bin/bash |
| # 允许 baoyu-skills 使用的 API Key 传递到子进程 |
| # (Hermes 默认会过滤包含 KEY/TOKEN/SECRET 的环境变量) |
| env_passthrough: |
| - GEMINI_API_KEY |
| - GOOGLE_API_KEY |
| - SILICONFLOW_API_KEY |
| - GOOGLE_IMAGE_MODEL |
| - GOOGLE_BASE_URL |
| |
| # 显示配置 |
| 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] |
| merged_other_keys = [k for k in all_keys - ENTRYPOINT_PRIORITY - USER_PRIORITY if k in restored and k not in generated] |
| 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 "🌐 设置供应商 Base URL 环境变量..." |
|
|
| 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}" |
| 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 [ -z "$API_SERVER_KEY" ]; then |
| KF="/data/.hermes/.api_server_key" |
| if [ -f "$KF" ]; then |
| export API_SERVER_KEY=$(cat "$KF") |
| else |
| API_SERVER_KEY=$(openssl rand -hex 32) |
| mkdir -p /data/.hermes |
| echo "$API_SERVER_KEY" > "$KF" |
| chmod 600 "$KF" |
| export API_SERVER_KEY |
| fi |
| else |
| export API_SERVER_KEY |
| fi |
|
|
| |
| export GATEWAY_ALLOW_ALL_USERS="${GATEWAY_ALLOW_ALL_USERS:-true}" |
|
|
| |
| export HERMES_MODEL="$MAIN_MODEL" |
|
|
| |
| if [ -n "$SILICONFLOW_API_KEY" ]; then |
| export SILICONFLOW_API_KEY |
| export SILICONFLOW_BASE_URL="${SILICONFLOW_BASE_URL:-https://api.siliconflow.cn/v1}" |
| echo " ✅ SILICONFLOW_API_KEY 已导出(baoyu-imagine 技能可用)" |
| fi |
|
|
| if [ -n "$GEMINI_API_KEY" ]; then |
| export GEMINI_API_KEY |
| export GEMINI_BASE_URL="${GEMINI_BASE_URL:-https://generativelanguage.googleapis.com}" |
| |
| export GOOGLE_API_KEY="${GEMINI_API_KEY}" |
| export GOOGLE_BASE_URL="${GEMINI_BASE_URL:-https://generativelanguage.googleapis.com}" |
| export GOOGLE_IMAGE_MODEL="gemini-3.1-flash-image-preview" |
| echo " ✅ GEMINI_API_KEY 已导出(baoyu-imagine 技能可用)" |
| echo " ✅ GOOGLE_API_KEY 已设置(baoyu-imagine google provider)" |
| echo " ✅ GOOGLE_IMAGE_MODEL=gemini-3.1-flash-image-preview" |
| fi |
|
|
| echo " ✅ Base URL 环境变量已设置" |
| echo " ✅ API Server 环境变量已设置 (端口: 8642)" |
| 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" |
| "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" |
| ) |
|
|
| |
| |
| |
|
|
| |
| 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}" |
| else |
| |
| |
| : |
| 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} 个环境变量(含恢复的持久化变量)" |
|
|
| |
| |
| |
| |
| |
| BAOYU_SKILLS_BASE="/home/appuser/.baoyu-skills" |
|
|
| |
| IMAGINE_EXTEND_DIR="${BAOYU_SKILLS_BASE}/baoyu-imagine" |
| IMAGINE_EXTEND_FILE="${IMAGINE_EXTEND_DIR}/EXTEND.md" |
|
|
| if [ -n "$SILICONFLOW_API_KEY" ] || [ -n "$GEMINI_API_KEY" ]; then |
| echo "⚙️ 配置 baoyu-imagine 技能..." |
| mkdir -p "${IMAGINE_EXTEND_DIR}" |
| |
| if [ -n "$GEMINI_API_KEY" ]; then |
| |
| |
| cat > "${IMAGINE_EXTEND_FILE}" << EOF_IMAGINE |
| # Baoyu Imagine Configuration |
| |
| # 默认供应商 (Google/Gemini) |
| default_provider = "google" |
| |
| # 默认质量 |
| default_quality = "2k" |
| |
| # 默认宽高比 |
| default_aspect_ratio = "16:9" |
| |
| # 默认图片尺寸 (Google 使用 1K/2K/4K) |
| default_image_size = "2K" |
| |
| # Google/Gemini 供应商配置 |
| [default_model.google] |
| provider = "google" |
| model = "gemini-3.1-flash-image-preview" |
| |
| # 批量设置 |
| [batch] |
| max_workers = 4 |
| EOF_IMAGINE |
| echo " ✅ baoyu-imagine EXTEND.md 已写入 (Gemini 主供应商)" |
| |
| |
| if [ -n "$SILICONFLOW_API_KEY" ]; then |
| echo " 🔄 SiliconFlow 已配置为备用供应商" |
| fi |
| elif [ -n "$SILICONFLOW_API_KEY" ]; then |
| |
| cat > "${IMAGINE_EXTEND_FILE}" << EOF_IMAGINE |
| # Baoyu Imagine Configuration |
| |
| # 默认供应商 |
| default_provider = "siliconflow" |
| |
| # 默认质量 |
| default_quality = "2k" |
| |
| # 默认宽高比 |
| default_aspect_ratio = "16:9" |
| |
| # 默认图片尺寸 |
| default_image_size = "1024x1024" |
| |
| # SiliconFlow 供应商配置 |
| [default_model.siliconflow] |
| provider = "siliconflow" |
| model = "Kwai-Kolors/Kolors" |
| |
| # 批量设置 |
| [batch] |
| max_workers = 4 |
| EOF_IMAGINE |
| echo " ✅ baoyu-imagine EXTEND.md 已写入 (SiliconFlow 后端)" |
| fi |
| |
| |
| |
| |
| BAOYU_ENV_FILE="${BAOYU_SKILLS_BASE}/.env" |
| echo " 📝 生成 baoyu-skills .env 文件..." |
| > "${BAOYU_ENV_FILE}" |
| if [ -n "$GEMINI_API_KEY" ]; then |
| echo "GEMINI_API_KEY=${GEMINI_API_KEY}" >> "${BAOYU_ENV_FILE}" |
| echo "GOOGLE_API_KEY=${GEMINI_API_KEY}" >> "${BAOYU_ENV_FILE}" |
| echo "GOOGLE_IMAGE_MODEL=gemini-3.1-flash-image-preview" >> "${BAOYU_ENV_FILE}" |
| echo "GOOGLE_BASE_URL=${GEMINI_BASE_URL:-https://generativelanguage.googleapis.com}" >> "${BAOYU_ENV_FILE}" |
| fi |
| if [ -n "$SILICONFLOW_API_KEY" ]; then |
| echo "SILICONFLOW_API_KEY=${SILICONFLOW_API_KEY}" >> "${BAOYU_ENV_FILE}" |
| fi |
| echo " ✅ .env 文件已写入 (${BAOYU_ENV_FILE})" |
| else |
| echo " ℹ️ 未配置 SILICONFLOW_API_KEY 或 GEMINI_API_KEY,跳过 baoyu-imagine 技能配置" |
| fi |
|
|
| |
| COVER_EXTEND_DIR="${BAOYU_SKILLS_BASE}/baoyu-cover-image" |
| COVER_EXTEND_FILE="${COVER_EXTEND_DIR}/EXTEND.md" |
|
|
| if [ -n "$SILICONFLOW_API_KEY" ] || [ -n "$GEMINI_API_KEY" ]; then |
| echo "⚙️ 配置 baoyu-cover-image 技能..." |
| mkdir -p "${COVER_EXTEND_DIR}" |
| cat > "${COVER_EXTEND_FILE}" << EOF_COVER |
| # Baoyu Cover Image Configuration |
| |
| # 首选图像后端 |
| preferred_image_backend = "baoyu-imagine" |
| |
| # 默认输出目录 (图片保存到哪里) |
| # independent = cover-image/{topic-slug}/ |
| # imgs-subdir = {article-dir}/imgs/ |
| # same-dir = {article-dir}/ |
| # 使用 independent,图片会保存到 /data/cover-image/{topic-slug}/ |
| # image-proxy.js 已配置扫描此目录 |
| default_output_dir = "independent" |
| |
| # 默认宽高比 |
| default_aspect = "16:9" |
| |
| # 默认类型与风格 |
| preferred_type = "scene" |
| preferred_palette = "warm" |
| preferred_rendering = "digital" |
| preferred_font = "clean" |
| |
| # 语言 |
| language = "zh" |
| EOF_COVER |
| echo " ✅ baoyu-cover-image EXTEND.md 已写入 (${COVER_EXTEND_FILE})" |
| fi |
|
|
| |
| ILLUSTRATOR_EXTEND_DIR="${BAOYU_SKILLS_BASE}/baoyu-article-illustrator" |
| ILLUSTRATOR_EXTEND_FILE="${ILLUSTRATOR_EXTEND_DIR}/EXTEND.md" |
|
|
| if [ -n "$SILICONFLOW_API_KEY" ] || [ -n "$GEMINI_API_KEY" ]; then |
| echo "⚙️ 配置 baoyu-article-illustrator 技能..." |
| mkdir -p "${ILLUSTRATOR_EXTEND_DIR}" |
| cat > "${ILLUSTRATOR_EXTEND_FILE}" << EOF_ILLUSTRATOR |
| # Baoyu Article Illustrator Configuration |
| |
| # 首选图像后端 |
| preferred_image_backend = "baoyu-imagine" |
| |
| # 默认输出目录 |
| default_output_dir = "imgs-subdir" |
| |
| # 默认类型与风格 |
| preferred_type = "infographic" |
| preferred_style = "minimal-flat" |
| preferred_palette = "warm" |
| |
| # 语言 |
| language = "zh" |
| EOF_ILLUSTRATOR |
| echo " ✅ baoyu-article-illustrator EXTEND.md 已写入 (${ILLUSTRATOR_EXTEND_FILE})" |
| fi |
|
|
| |
| if [ -n "$SILICONFLOW_API_KEY" ] || [ -n "$GEMINI_API_KEY" ]; then |
| echo "🔍 调试:验证 baoyu-skills EXTEND.md 文件" |
| ls -la "${BAOYU_SKILLS_BASE}/" 2>/dev/null || echo " ⚠️ ${BAOYU_SKILLS_BASE} 不存在" |
| for skill_dir in "${BAOYU_SKILLS_BASE}"/*/; do |
| if [ -f "${skill_dir}EXTEND.md" ]; then |
| echo " ✅ ${skill_dir}EXTEND.md 存在" |
| |
| grep -E "^(default_provider|preferred_image_backend)" "${skill_dir}EXTEND.md" 2>/dev/null || true |
| else |
| echo " ⚠️ ${skill_dir}EXTEND.md 缺失" |
| fi |
| done |
| fi |
|
|
| |
| |
| |
| |
| |
|
|
| SKILL_IMAGINE_DIR="/data/.hermes/skills/baoyu-imagine" |
| SKILL_IMAGINE_SCRIPTS="${SKILL_IMAGINE_DIR}/scripts" |
| BUILTIN_IMAGINE_SCRIPTS="${BAOYU_SKILLS_BASE}/baoyu-imagine/scripts" |
|
|
| |
| echo "🔍 调试:检查 baoyu-imagine 脚本源..." |
| echo " 内置脚本路径: ${BUILTIN_IMAGINE_SCRIPTS}" |
| if [ -d "$BUILTIN_IMAGINE_SCRIPTS" ]; then |
| echo " ✅ 内置脚本目录存在" |
| ls -la "${BUILTIN_IMAGINE_SCRIPTS}/" 2>/dev/null | head -5 || echo " ⚠️ 无法列出内置脚本内容" |
| else |
| echo " ⚠️ 内置脚本目录不存在(Dockerfile 构建时可能下载失败)" |
| fi |
| echo " 目标脚本路径: ${SKILL_IMAGINE_SCRIPTS}" |
| if [ -f "${SKILL_IMAGINE_SCRIPTS}/main.ts" ]; then |
| echo " ✅ 目标脚本已存在" |
| else |
| echo " ⚠️ 目标脚本缺失" |
| fi |
|
|
| |
| if [ -n "$GEMINI_API_KEY" ]; then |
| echo "⚙️ 修复 baoyu-imagine 技能脚本..." |
| |
| |
| mkdir -p "${SKILL_IMAGINE_DIR}" |
| |
| |
| |
| |
| |
| if [ -d "$BUILTIN_IMAGINE_SCRIPTS" ] && [ -f "${BUILTIN_IMAGINE_SCRIPTS}/main.ts" ]; then |
| echo " 📁 从内置目录复制 scripts/..." |
| rm -rf "${SKILL_IMAGINE_DIR}/scripts" 2>/dev/null || true |
| cp -r "${BUILTIN_IMAGINE_SCRIPTS}" "${SKILL_IMAGINE_DIR}/" |
| else |
| echo " 📥 内置脚本不可用,运行时下载..." |
| |
| rm -rf "${SKILL_IMAGINE_SCRIPTS}" |
| mkdir -p "${SKILL_IMAGINE_SCRIPTS}" |
| TEMP_SKILLS_DIR="/tmp/baoyu-skills-download" |
| rm -rf "$TEMP_SKILLS_DIR" |
| if git clone --depth 1 https://github.com/JimLiu/baoyu-skills.git "$TEMP_SKILLS_DIR" 2>/dev/null; then |
| if [ -f "${TEMP_SKILLS_DIR}/skills/baoyu-image-gen/scripts/main.ts" ]; then |
| cp -r "${TEMP_SKILLS_DIR}/skills/baoyu-image-gen/scripts/" "${SKILL_IMAGINE_DIR}/" |
| echo " ✅ 运行时下载成功(原始完整版本)" |
| else |
| echo " ❌ 下载的仓库中找不到 main.ts" |
| fi |
| rm -rf "$TEMP_SKILLS_DIR" |
| else |
| echo " ❌ git clone 失败,请检查网络连接" |
| echo " ⚠️ 使用现有脚本(可能不是原始版本)" |
| fi |
| fi |
| |
| |
| if [ ! -f "${SKILL_IMAGINE_DIR}/package.json" ]; then |
| echo " 📝 创建 package.json..." |
| cat > "${SKILL_IMAGINE_DIR}/package.json" << 'EOF_PKG' |
| { |
| "name": "baoyu-imagine", |
| "version": "1.58.0", |
| "type": "module", |
| "scripts": { |
| "build": "tsc", |
| "test": "bun test" |
| }, |
| "dependencies": { |
| "@google/generative-ai": "^0.24.0" |
| }, |
| "devDependencies": { |
| "typescript": "^5.8.0", |
| "@types/node": "^22.14.0" |
| } |
| } |
| EOF_PKG |
| fi |
| |
| |
| |
| |
| echo " 🔧 修复 google.ts 以支持 Google API 响应格式..." |
| if [ -f "${SKILL_IMAGINE_DIR}/scripts/providers/google.ts" ]; then |
| node -e " |
| const fs = require('fs'); |
| const filePath = '${SKILL_IMAGINE_DIR}/scripts/providers/google.ts'; |
| let content = fs.readFileSync(filePath, 'utf8'); |
| let modified = false; |
| |
| // 修复1: responseModalities |
| if (content.includes('responseModalities: [\"IMAGE\"],')) { |
| content = content.replace(/responseModalities: \\[\"IMAGE\"\\],/g, 'responseModalities: [\"TEXT\", \"IMAGE\"],'); |
| console.log(' ✅ 已修复 responseModalities: [\"TEXT\", \"IMAGE\"]'); |
| modified = true; |
| } |
| |
| // 修复2: extractInlineImageData 支持 inline_data 字段 |
| const oldLine = ' const data = part.inlineData?.data;'; |
| const newLine = ' const data = part.inlineData?.data ?? part.inline_data?.data;'; |
| if (content.includes(oldLine)) { |
| content = content.replace(oldLine, newLine); |
| console.log(' ✅ 已修复 extractInlineImageData 函数'); |
| modified = true; |
| } |
| |
| if (modified) { |
| fs.writeFileSync(filePath, content, 'utf8'); |
| console.log(' ✅ 所有修复应用完成'); |
| } else { |
| console.log(' ⚠️ 未找到需要修复的代码'); |
| } |
| " |
| fi |
| |
| |
| if [ ! -d "${SKILL_IMAGINE_DIR}/node_modules" ]; then |
| echo " 📦 安装 baoyu-imagine 依赖..." |
| (cd "${SKILL_IMAGINE_DIR}" && bun install) 2>&1 | tail -5 || { |
| echo " ⚠️ bun install 失败,尝试 npm install..." |
| (cd "${SKILL_IMAGINE_DIR}" && npm install) 2>&1 | tail -5 || true |
| } |
| fi |
| |
| |
| echo " 🔍 验证脚本完整性..." |
| if [ -f "${SKILL_IMAGINE_SCRIPTS}/main.ts" ]; then |
| echo " ✅ baoyu-imagine 技能已就绪" |
| echo " 脚本: ${SKILL_IMAGINE_SCRIPTS}/main.ts" |
| ls -lh "${SKILL_IMAGINE_SCRIPTS}/main.ts" |
| if [ -d "${SKILL_IMAGINE_DIR}/node_modules" ]; then |
| echo " 依赖: 已安装 (${SKILL_IMAGINE_DIR}/node_modules)" |
| else |
| echo " ⚠️ 依赖: 未安装" |
| fi |
| else |
| echo " ❌ baoyu-imagine 技能修复失败: main.ts 仍然缺失" |
| echo " 这通常是因为网络问题导致无法下载脚本" |
| fi |
| |
| |
| |
| |
| if [ -n "$GEMINI_API_KEY" ]; then |
| echo " 🎯 检测到 GEMINI_API_KEY,启用 Gemini 主供应商..." |
| echo " 模型: gemini-3.1-flash-image-preview" |
| |
| |
| |
| if [ -f "${SKILL_IMAGINE_SCRIPTS}/main.ts.orig" ]; then |
| cp "${SKILL_IMAGINE_SCRIPTS}/main.ts.orig" "${SKILL_IMAGINE_SCRIPTS}/main.ts" |
| echo " ✅ 已恢复原始 main.ts(支持 google provider)" |
| fi |
| |
| |
| WRAPPER_DIR="/home/appuser/.local/bin" |
| mkdir -p "$WRAPPER_DIR" |
| |
| if [ -n "$SILICONFLOW_API_KEY" ]; then |
| |
| |
| cat > "${WRAPPER_DIR}/baoyu-imagine" << EOF_WRAPPER |
| #!/bin/bash |
| # Smart wrapper: Gemini primary, SiliconFlow fallback |
| |
| # 加载 baoyu-skills .env 文件(绕过 Hermes 环境变量过滤) |
| if [ -f ~/.baoyu-skills/.env ]; then |
| set -a |
| source ~/.baoyu-skills/.env |
| set +a |
| fi |
| |
| # 同时尝试从环境变量加载(如果未被过滤) |
| export GEMINI_API_KEY="\${GEMINI_API_KEY:-\$GEMINI_API_KEY}" |
| export GOOGLE_API_KEY="\${GOOGLE_API_KEY:-\$GEMINI_API_KEY}" |
| export GOOGLE_IMAGE_MODEL="\${GOOGLE_IMAGE_MODEL:-gemini-3.1-flash-image-preview}" |
| export GOOGLE_BASE_URL="\${GOOGLE_BASE_URL:-https://generativelanguage.googleapis.com}" |
| export SILICONFLOW_API_KEY="\${SILICONFLOW_API_KEY:-\$SILICONFLOW_API_KEY}" |
| |
| # 确保图片保存到可访问的目录 |
| mkdir -p /data/.hermes/image_cache |
| cd /data/.hermes/image_cache |
| |
| # 尝试 Gemini 主供应商 |
| # baoyu-imagine 的 google provider 会自动使用 GOOGLE_IMAGE_MODEL |
| echo "🎯 Trying Gemini (gemini-3.1-flash-image-preview)..." |
| if bun "${SKILL_IMAGINE_SCRIPTS}/main.ts" "\$@" 2>/tmp/gemini_error.log; then |
| exit 0 |
| fi |
| |
| # Fallback 到 SiliconFlow |
| echo "⚠️ Gemini failed, falling back to SiliconFlow (Kwai-Kolors/Kolors)..." |
| if [ -f "/app/image-gen-siliconflow.ts" ]; then |
| exec bun "/app/image-gen-siliconflow.ts" --model "Kwai-Kolors/Kolors" "\$@" |
| else |
| echo "❌ SiliconFlow fallback script not found" |
| cat /tmp/gemini_error.log >&2 |
| exit 1 |
| fi |
| EOF_WRAPPER |
| echo " ✅ 智能包装脚本: ${WRAPPER_DIR}/baoyu-imagine (Gemini主 + SiliconFlow备)" |
| else |
| |
| cat > "${WRAPPER_DIR}/baoyu-imagine" << EOF_WRAPPER |
| #!/bin/bash |
| # Gemini-only wrapper |
| |
| # 加载 baoyu-skills .env 文件(绕过 Hermes 环境变量过滤) |
| if [ -f ~/.baoyu-skills/.env ]; then |
| set -a |
| source ~/.baoyu-skills/.env |
| set +a |
| fi |
| |
| # 同时尝试从环境变量加载(如果未被过滤) |
| export GEMINI_API_KEY="\${GEMINI_API_KEY:-\$GEMINI_API_KEY}" |
| export GOOGLE_API_KEY="\${GOOGLE_API_KEY:-\$GEMINI_API_KEY}" |
| export GOOGLE_IMAGE_MODEL="\${GOOGLE_IMAGE_MODEL:-gemini-3.1-flash-image-preview}" |
| export GOOGLE_BASE_URL="\${GOOGLE_BASE_URL:-https://generativelanguage.googleapis.com}" |
| |
| mkdir -p /data/.hermes/image_cache |
| cd /data/.hermes/image_cache |
| |
| exec bun "${SKILL_IMAGINE_SCRIPTS}/main.ts" "\$@" |
| EOF_WRAPPER |
| echo " ✅ 包装脚本: ${WRAPPER_DIR}/baoyu-imagine (仅 Gemini)" |
| fi |
| chmod +x "${WRAPPER_DIR}/baoyu-imagine" |
| |
| elif [ -n "$SILICONFLOW_API_KEY" ]; then |
| echo " 🎯 检测到 SILICONFLOW_API_KEY,启用 SiliconFlow 后端..." |
| |
| |
| if [ ! -f "${SKILL_IMAGINE_SCRIPTS}/main.ts.orig" ]; then |
| cp "${SKILL_IMAGINE_SCRIPTS}/main.ts" "${SKILL_IMAGINE_SCRIPTS}/main.ts.orig" |
| fi |
| |
| |
| ENHANCED_GEN="/app/image-gen-siliconflow.ts" |
| if [ -f "$ENHANCED_GEN" ]; then |
| cp "$ENHANCED_GEN" "${SKILL_IMAGINE_SCRIPTS}/main.ts" |
| echo " ✅ 已复制增强版生成器 (${ENHANCED_GEN})" |
| else |
| echo " ⚠️ 增强版生成器不存在,使用内联简化版..." |
| |
| cat > "${SKILL_IMAGINE_SCRIPTS}/main.ts" << 'EOF_SILICONFLOW' |
| |
| // Fallback simplified version |
| interface CliArgs { prompt: string; imagePath: string; model: string; } |
| function parseArgs(argv: string[]): CliArgs { |
| const args: CliArgs = { prompt: "", imagePath: "", model: "black-forest-labs/FLUX.1-dev" }; |
| for (let i = 0; i < argv.length; i++) { |
| if (argv[i] === "--prompt" || argv[i] === "-p") args.prompt = argv[++i] || ""; |
| else if (argv[i] === "--image") args.imagePath = argv[++i] || ""; |
| else if (argv[i] === "--model" || argv[i] === "-m") args.model = argv[++i] || args.model; |
| } |
| return args; |
| } |
| async function generateImage(args: CliArgs): Promise<void> { |
| const apiKey = process.env.SILICONFLOW_API_KEY; |
| if (!apiKey) { console.error("Error: SILICONFLOW_API_KEY not set"); process.exit(1); } |
| console.log(`🎨 Generating image with ${args.model}...`); |
| const response = await fetch("https://api.siliconflow.cn/v1/images/generations", { |
| method: "POST", headers: { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json" }, |
| body: JSON.stringify({ model: args.model, prompt: args.prompt, image_size: "1024x1024", num_inference_steps: 20 }) |
| }); |
| if (!response.ok) { console.error(`❌ API error (${response.status})`); process.exit(1); } |
| const result = await response.json(); |
| if (!result.images?.length) { console.error("❌ No images in response"); process.exit(1); } |
| const imageUrl = result.images[0].url; |
| const imageResponse = await fetch(imageUrl); |
| const imageBuffer = await imageResponse.arrayBuffer(); |
| await Bun.write(args.imagePath, new Uint8Array(imageBuffer)); |
| console.log(`✅ Saved: ${args.imagePath} (${imageBuffer.byteLength} bytes)`); |
| } |
| const args = parseArgs(process.argv.slice(2)); |
| if (!args.prompt || !args.imagePath) { console.error("Usage: bun main.ts --prompt <text> --image <path>"); process.exit(1); } |
| await generateImage(args); |
| EOF_SILICONFLOW |
| fi |
| |
| echo " ✅ 已配置 SiliconFlow 后端" |
| echo " 模型: Kwai-Kolors/Kolors" |
| echo " API: https://api.siliconflow.cn/v1/images/generations" |
| echo " 功能: --ar, --size, --quality, --n, --seed, --promptfiles" |
| |
| |
| cat > "${WRAPPER_DIR}/baoyu-imagine" << EOF_WRAPPER |
| #!/bin/bash |
| # SiliconFlow wrapper |
| |
| # 加载 baoyu-skills .env 文件(绕过 Hermes 环境变量过滤) |
| if [ -f ~/.baoyu-skills/.env ]; then |
| set -a |
| source ~/.baoyu-skills/.env |
| set +a |
| fi |
| |
| # 同时尝试从环境变量加载(如果未被过滤) |
| export SILICONFLOW_API_KEY="\${SILICONFLOW_API_KEY:-\$SILICONFLOW_API_KEY}" |
| |
| # 确保图片保存到可访问的目录 |
| mkdir -p /data/.hermes/image_cache |
| cd /data/.hermes/image_cache |
| |
| exec bun "${SKILL_IMAGINE_SCRIPTS}/main.ts" "\$@" |
| EOF_WRAPPER |
| chmod +x "${WRAPPER_DIR}/baoyu-imagine" |
| echo " ✅ 包装脚本: ${WRAPPER_DIR}/baoyu-imagine" |
| else |
| echo " ⚠️ 未检测到 SILICONFLOW_API_KEY 或 GEMINI_API_KEY" |
| echo " 图像生成功能不可用" |
| echo " 请设置以下环境变量之一:" |
| echo " - GEMINI_API_KEY (推荐,图像质量更好)" |
| echo " - SILICONFLOW_API_KEY (国内可访问)" |
| fi |
| |
| |
| chmod -R 555 "${SKILL_IMAGINE_SCRIPTS}/" 2>/dev/null || true |
| echo " 🔒 已锁定 scripts/ 目录" |
| |
| |
| |
| OLD_EXTEND="/data/.hermes/skills/baoyu-imagine/EXTEND.md" |
| if [ -f "${IMAGINE_EXTEND_FILE}" ]; then |
| mkdir -p "$(dirname "$OLD_EXTEND")" |
| ln -sf "${IMAGINE_EXTEND_FILE}" "$OLD_EXTEND" |
| echo " 🔗 创建 EXTEND.md 软链接: $OLD_EXTEND -> ${IMAGINE_EXTEND_FILE}" |
| fi |
| fi |
|
|
| |
| mkdir -p /data/.hermes/image_cache |
| chmod 755 /data/.hermes/image_cache |
| chown appuser:appuser /data/.hermes/image_cache 2>/dev/null || true |
|
|
| |
| SYNC_INTERVAL=${SYNC_INTERVAL:-60} |
| echo "🔄 数据同步间隔: ${SYNC_INTERVAL}秒" |
|
|
| echo "🔄 启动数据同步服务..." |
| python -m src.data_sync daemon & |
| SYNC_PID=$! |
| echo " 同步服务 PID: $SYNC_PID" |
|
|
| |
| echo "🔄 检查配置..." |
| hermes config check 2>/dev/null || echo " 配置检查完成" |
|
|
| echo "🔒 强制写入模型配置(防止 Hermes 启动时被覆盖)..." |
| hermes config set model.default "$MAIN_MODEL" 2>/dev/null || { |
| echo " ⚠️ hermes config set 不可用,使用直接写入方式" |
| if command -v yq &>/dev/null; then |
| yq -i ".model.default = \"$MAIN_MODEL\"" "$CONFIG_FILE" |
| fi |
| } |
| hermes config set model.provider "$MAIN_PROVIDER" 2>/dev/null || true |
| hermes config set model.base_url "$MAIN_BASE_URL" 2>/dev/null || true |
|
|
| |
| if command -v yq &>/dev/null; then |
| ACTUAL_MODEL=$(yq '.model.default' "$CONFIG_FILE" 2>/dev/null) |
| if [ "$ACTUAL_MODEL" != "$MAIN_MODEL" ]; then |
| echo " ⚠️ 模型被覆盖! 期望: $MAIN_MODEL, 实际: $ACTUAL_MODEL" |
| echo " 🔄 重新写入模型配置..." |
| yq -i ".model.default = \"$MAIN_MODEL\"" "$CONFIG_FILE" |
| yq -i ".model.provider = \"$MAIN_PROVIDER\"" "$CONFIG_FILE" |
| yq -i ".model.base_url = \"$MAIN_BASE_URL\"" "$CONFIG_FILE" |
| fi |
| fi |
|
|
| echo " ✅ 模型配置已锁定: $MAIN_PROVIDER/$MAIN_MODEL" |
|
|
| |
| echo "📡 启动 Hermes Gateway + API Server..." |
|
|
| |
| 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 |
|
|
| |
| |
| |
|
|
| update_hermes_web_ui() { |
| local WEBUI_DIR="/opt/hermes-web-ui" |
| local TEMP_DIR="/tmp/hermes-web-ui-update" |
| |
| echo "🔄 检查 hermes-web-ui 更新..." |
| |
| |
| local LATEST_VERSION |
| LATEST_VERSION=$(curl -s https://api.github.com/repos/EKKOLearnAI/hermes-web-ui/releases/latest | grep '"tag_name":' | sed -E 's/.*"tag_name": "([^"]+)".*/\1/') |
| |
| if [ -z "$LATEST_VERSION" ]; then |
| echo " ⚠️ 无法获取远程版本,跳过更新" |
| return 0 |
| fi |
| |
| |
| local CURRENT_VERSION="unknown" |
| if [ -f "${WEBUI_DIR}/package.json" ]; then |
| CURRENT_VERSION=$(cat "${WEBUI_DIR}/package.json" | grep '"version"' | head -1 | sed -E 's/.*"version": "([^"]+)".*/\1/') |
| fi |
| |
| echo " 当前版本: ${CURRENT_VERSION}" |
| echo " 最新版本: ${LATEST_VERSION}" |
| |
| |
| if [ "$CURRENT_VERSION" = "$LATEST_VERSION" ]; then |
| echo " ✅ 已是最新版本,跳过更新" |
| return 0 |
| fi |
| |
| echo " 📥 检测到新版本,开始更新..." |
| |
| |
| rm -rf "$TEMP_DIR" |
| mkdir -p "$TEMP_DIR" |
| |
| |
| if ! git clone --depth 1 https://github.com/EKKOLearnAI/hermes-web-ui.git "$TEMP_DIR"; then |
| echo " ❌ Git clone 失败,保留当前版本" |
| rm -rf "$TEMP_DIR" |
| return 0 |
| fi |
| |
| cd "$TEMP_DIR" |
| |
| |
| local CLONED_VERSION |
| CLONED_VERSION=$(cat package.json | grep '"version"' | head -1 | sed -E 's/.*"version": "([^"]+)".*/\1/') |
| echo " 克隆版本: ${CLONED_VERSION}" |
| |
| |
| if [ "$CLONED_VERSION" = "$CURRENT_VERSION" ]; then |
| echo " ✅ 版本相同,跳过更新" |
| cd /app |
| rm -rf "$TEMP_DIR" |
| return 0 |
| fi |
| |
| |
| echo " 📦 安装依赖..." |
| if ! npm install; then |
| echo " ❌ npm install 失败,保留当前版本" |
| cd /app |
| rm -rf "$TEMP_DIR" |
| return 0 |
| fi |
| |
| echo " 🔨 构建..." |
| if ! npm run build; then |
| echo " ❌ 构建失败,保留当前版本" |
| cd /app |
| rm -rf "$TEMP_DIR" |
| return 0 |
| fi |
| |
| |
| echo " 🧹 精简..." |
| npm prune --omit=dev |
| |
| |
| echo " 📝 替换旧版本..." |
| rm -rf "${WEBUI_DIR}.bak" 2>/dev/null || true |
| mv "$WEBUI_DIR" "${WEBUI_DIR}.bak" 2>/dev/null || true |
| mkdir -p "$WEBUI_DIR" |
| cp -r dist node_modules package.json "$WEBUI_DIR/" |
| |
| cd /app |
| rm -rf "$TEMP_DIR" "${WEBUI_DIR}.bak" |
| |
| echo " ✅ hermes-web-ui 已更新至 ${CLONED_VERSION}" |
| } |
|
|
| |
| if [ "${WEBUI_AUTO_UPDATE:-true}" = "true" ]; then |
| update_hermes_web_ui |
| else |
| echo " ℹ️ Web UI 自动更新已禁用 (WEBUI_AUTO_UPDATE=false)" |
| fi |
|
|
| |
| |
| |
| |
| |
| |
| |
| echo "🌐 启动 Hermes Web UI..." |
| echo " Image+Proxy: http://0.0.0.0:7860" |
| echo " BFF Server: http://127.0.0.1:7861" |
| echo " Upstream: http://127.0.0.1:8642" |
| echo " 📷 图片浏览: http://localhost:7860/images/" |
| echo "" |
|
|
| |
| export PORT=7861 |
| 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 "$PROXY_PID" ] && kill -0 $PROXY_PID 2>/dev/null; then |
| echo " 🛑 停止 Image Proxy..." |
| kill $PROXY_PID 2>/dev/null || true |
| wait $PROXY_PID 2>/dev/null || true |
| 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 |
|
|
| |
| echo "🧹 清理旧的 web-ui 数据库..." |
| rm -f /home/appuser/.hermes-web-ui/hermes-web-ui.db 2>/dev/null |
| rm -f /data/.hermes-web-ui/hermes-web-ui.db 2>/dev/null |
| echo " ✅ 已清理" |
|
|
| |
| node /opt/hermes-web-ui/dist/server/index.js & |
| BFF_PID=$! |
|
|
| |
| echo " ⏳ 等待 BFF 就绪 (:7861)..." |
| BFF_READY=false |
| for i in $(seq 1 20); do |
| if curl -sf http://localhost:7861/health > /dev/null 2>&1; then |
| BFF_READY=true |
| break |
| fi |
| sleep 1 |
| done |
|
|
| if [ "$BFF_READY" = true ]; then |
| echo " ✅ BFF 已就绪 → http://127.0.0.1:7861" |
| else |
| echo " ⚠️ BFF 未在 20 秒内就绪,请查看日志" |
| fi |
|
|
| |
| echo "🖼️ 启动 Image Proxy..." |
| BFF_PORT=7861 LISTEN_PORT=7860 IMAGE_DIR=/data/.hermes/image_cache \ |
| node /app/image-proxy.js & |
| PROXY_PID=$! |
|
|
| |
| PROXY_READY=false |
| for i in $(seq 1 10); do |
| if curl -sf http://localhost:7860/health > /dev/null 2>&1; then |
| PROXY_READY=true |
| break |
| fi |
| sleep 1 |
| done |
|
|
| if [ "$PROXY_READY" = true ]; then |
| echo " ✅ Web UI 已就绪 → http://localhost:7860" |
| echo " 📷 图片浏览 → http://localhost:7860/images/" |
| else |
| echo " ⚠️ Image Proxy 未就绪,Web UI 可能不可用" |
| fi |
|
|
| |
| |
| |
| echo "🔐 确保默认凭据可用 (admin/123456)..." |
| sleep 3 |
|
|
| |
| for i in $(seq 1 15); do |
| if curl -sf http://127.0.0.1:7861/health >/dev/null 2>&1; then |
| break |
| fi |
| [ "$i" -lt 15 ] && sleep 2 |
| done |
|
|
| |
| HERMES_CLI="/opt/hermes-web-ui/node_modules/.bin/hermes-web-ui" |
| if [ -f "$HERMES_CLI" ]; then |
| node "$HERMES_CLI" reset-default-login 2>/dev/null && \ |
| echo " ✅ 默认凭据已就绪" || \ |
| echo " ⚠️ CLI 重置失败" |
| else |
| echo " ⚠️ CLI 未找到,使用内联脚本..." |
| node -e " |
| const{DatabaseSync}=require('node:sqlite');const{randomBytes,scryptSync}=require('node:crypto'); |
| const o=require('node:os'),p=require('node:path'),fs=require('node:fs'); |
| const h=process.env.HERMES_WEB_UI_HOME?.trim()||p.join(o.homedir(),'.hermes-web-ui'); |
| const d=p.join(h,'hermes-web-ui.db'); |
| if(!fs.existsSync(d)){process.exit(1)} |
| const db=new DatabaseSync(d); |
| db.exec('CREATE TABLE IF NOT EXISTS users(id INTEGER PRIMARY KEY AUTOINCREMENT,username TEXT NOT NULL UNIQUE,password_hash TEXT NOT NULL,role TEXT NOT NULL DEFAULT \"admin\",status TEXT NOT NULL DEFAULT \"active\",created_at INTEGER NOT NULL,updated_at INTEGER NOT NULL,last_login_at INTEGER)'); |
| const s=randomBytes(16).toString('hex'); |
| const h2=scryptSync('123456',s,64).toString('hex'); |
| const ph='scrypt:'+s+':'+h2; |
| const n=Date.now(); |
| const e=db.prepare('SELECT id FROM users WHERE username=?').get('admin'); |
| if(e?.id){db.prepare('UPDATE users SET password_hash=?,role=\"super_admin\",updated_at=? WHERE id=?').run(ph,n,e.id)} |
| else{db.prepare('INSERT INTO users(username,password_hash,role,status,created_at,updated_at) VALUES(\"admin\",?,\"super_admin\",\"active\",?,?)').run(ph,n,n)} |
| db.close() |
| " 2>/dev/null && echo " ✅ 默认凭据已就绪" || echo " ⚠️ 内联脚本失败" |
| fi |
|
|
| echo " 🔑 默认登录: admin / 123456" |
| echo " 💡 登录后请在 "设置 → 账户" 中修改用户名和密码" |
|
|
| |
| 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 = \"$MAIN_PROVIDER\"" "$CONFIG_FILE" |
| yq -i ".model.base_url = \"$MAIN_BASE_URL\"" "$CONFIG_FILE" |
| echo " ✅ 模型已修正: $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 = \"$MAIN_PROVIDER\"" "$CONFIG_FILE" |
| yq -i ".model.base_url = \"$MAIN_BASE_URL\"" "$CONFIG_FILE" |
| echo " ✅ 模型已修正: $MAIN_PROVIDER/$MAIN_MODEL" |
| else |
| echo " ✅ 模型配置验证通过: $MAIN_PROVIDER/$MAIN_MODEL" |
| fi |
| fi |
| fi |
|
|
| |
| |
| wait $PROXY_PID |
|
|