#!/usr/bin/env bash # ============================================================ # CCMR (Claude Code Model Router) Supervisor 启动包装脚本 # # 功能说明: # 作为 Supervisor 子进程管理 CCMR 网关的完整生命周期 # 支持文件热加载和进程崩溃自动恢复 # # 工作流程: # 1. 启动时读取 /root/.env.d/ccmr.env(如果存在) # 2. 检查 CCMR_ENABLED 是否为 true # - false: exit 0,Supervisor 视作正常退出不再拉起 # - true: 进入内部重启循环 # 3. 每次循环迭代: # a) 重新 source ccmr.env(热加载最新配置) # b) 调用 ccmr-setup.sh 重新生成 .env 和 models.yaml # c) 启动 ccmr start --port $CCMR_PORT & # d) 后台启动 inotifywait 监听 ccmr.env 文件变化 # e) 轮询检测 CCMR 进程存活(每 $CCMR_RELOAD_INTERVAL 秒) # f) 检测到文件变化或进程崩溃 -> 杀死旧进程 -> goto (a) # # 文件热加载机制: # - inotify 模式: 后台 inotifywait -m 持续监听 ccmr.env # 当文件被 modify/attrib/move/delete 时,写入 /tmp/.ccmr_config_changed 标记 # 主循环在下次轮询时检测到标记,立即触发重启 # - 轮询模式: 没有 inotify 时每 5 秒检查文件 mtime # - 两种模式都确保: 编辑保存 ccmr.env 后 <= 5 秒生效 # # Supervisor 行为: # - 未启用时 exit 0(Supervisor 不再重启) # - 启用后 while true 内部循环,永不退出 # - 如果 wrapper 脚本自身崩溃(set -e 触发),Supervisor 重新拉起 # # 所有支持的环境变量(共 10 个 API Key + 3 个系统设置): # # 系统设置: # CCMR_ENABLED - 是否启用 CCMR (true/false, 默认 false) # CCMR_PORT - 网关监听端口 (默认 8080) # CCMR_CONFIG_DIR - 配置存放目录 (默认 ~/.ccmr) # CCMR_CONFIG_FILE - 配置文件路径 (默认 /root/.env.d/ccmr.env) # CCMR_RELOAD_INTERVAL - 文件变化轮询间隔秒数 (默认 5) # # API Key 列表(按平台分组): # 1) DeepSeek: CCMR_DEEPSEEK_API_KEY=sk-xxx # 2) 通义千问 / Qwen: CCMR_QWEN_API_KEY=sk-xxx # 3) Kimi / Moonshot: CCMR_KIMI_API_KEY=sk-xxx # 4) 智谱 GLM: CCMR_GLM_API_KEY=xxx # 5) MiniMax CN: CCMR_MINIMAX_API_KEY=xxx # 6) MiniMax Global: CCMR_MINIMAX_GLOBAL_API_KEY=xxx # 7) MiMo SGP: CCMR_MIMO_API_KEY=tp-xxx # 8) MiMo CN: CCMR_MIMO_TOKEN_CN_API_KEY=tp-xxx # 9) MiMo AMS: CCMR_MIMO_TOKEN_AMS_API_KEY=tp-xxx # 10) MiMo Pay-as-you-go: CCMR_MIMO_PAYG_API_KEY=sk-xxx # # ============================================================ set -euo pipefail # ============================================================ # 从文件加载 CCMR 环境变量(优先级高于 Docker 环境变量) # ============================================================ CCMR_CONFIG_FILE="${CCMR_CONFIG_FILE:-/root/.env.d/ccmr.env}" if [[ -f "$CCMR_CONFIG_FILE" ]]; then # shellcheck source=/dev/null source "$CCMR_CONFIG_FILE" fi CCMR_ENABLED="${CCMR_ENABLED:-false}" CCMR_PORT="${CCMR_PORT:-8080}" CCMR_CONFIG_DIR="${CCMR_CONFIG_DIR:-$HOME/.ccmr}" CCMR_RELOAD_INTERVAL="${CCMR_RELOAD_INTERVAL:-5}" # 未启用时直接退出(exit 0 让 Supervisor 不再重启) if [[ "$CCMR_ENABLED" != "true" ]]; then echo "[CCMR-WRAPPER] CCMR is disabled (CCMR_ENABLED != true). Exiting." exit 0 fi echo "[CCMR-WRAPPER] CCMR is enabled, entering restart loop..." echo "[CCMR-WRAPPER] Config file: ${CCMR_CONFIG_FILE}" echo "[CCMR-WRAPPER] Watching for changes every ${CCMR_RELOAD_INTERVAL}s" # 检查 inotifywait 是否可用 USE_INOTIFY=false if command -v inotifywait &>/dev/null; then USE_INOTIFY=true echo "[CCMR-WRAPPER] Using inotifywait for efficient file monitoring" else echo "[CCMR-WRAPPER] inotifywait not available, using polling mode" fi # ============================================================ # 内部重启循环 # ============================================================ restart_count=0 while true; do restart_count=$((restart_count + 1)) echo "[CCMR-WRAPPER] === Restart #${restart_count} ===" # 重新加载配置文件(避免超长运行后环境变量过期) if [[ -f "$CCMR_CONFIG_FILE" ]]; then # shellcheck source=/dev/null source "$CCMR_CONFIG_FILE" CCMR_PORT="${CCMR_PORT:-8080}" fi # 重新生成 CCMR 配置(.env + models.yaml) /usr/local/bin/ccmr-setup.sh # 检查 API Key 是否已配置 if ! grep -q '=' "$CCMR_CONFIG_DIR/.env" 2>/dev/null || \ ! grep -v '^#' "$CCMR_CONFIG_DIR/.env" | grep -q '=' 2>/dev/null; then echo "[CCMR-WRAPPER] WARNING: No API keys configured yet." echo "[CCMR-WRAPPER] CCMR will start but may be non-functional." echo "[CCMR-WRAPPER] Create ${CCMR_CONFIG_FILE} with CCMR_*_API_KEY entries." fi echo "[CCMR-WRAPPER] Starting CCMR gateway on port ${CCMR_PORT}..." echo "[CCMR-WRAPPER] Config dir: ${CCMR_CONFIG_DIR}" echo "[CCMR-WRAPPER] Command: ccmr start --port ${CCMR_PORT}" # 启动 CCMR 网关(后台运行) cd "$CCMR_CONFIG_DIR" export NODE_ENV=production ccmr start --port "$CCMR_PORT" & CCMR_PID=$! echo "[CCMR-WRAPPER] CCMR gateway started (PID: ${CCMR_PID})" # ========================================================== # 双路监控:同时监听配置文件变化 和 CCMR 进程状态 # ========================================================== restart_reason="" CHANGE_FLAG="/tmp/.ccmr_config_changed" if [[ "$USE_INOTIFY" == "true" ]]; then # inotify 模式:后台 watcher 监听文件变化 # 检测到变化后写入标记文件,主循环轮询检测 rm -f "$CHANGE_FLAG" ( mkdir -p "$(dirname "$CCMR_CONFIG_FILE")" inotifywait -q -m --format '%f' \ -e modify,attrib,move,delete \ "$(dirname "$CCMR_CONFIG_FILE")" 2>/dev/null \ | while read -r fname; do if [[ "$fname" == "ccmr.env" ]]; then touch "$CHANGE_FLAG" exit 0 fi done ) & WATCHER_PID=$! fi # 轮询 loop:检测 CCMR 进程存活 和 文件变化标记 # 如果 inotify 检测到变化,标记文件会在下一次轮询前被写入 while kill -0 "$CCMR_PID" 2>/dev/null; do sleep "$CCMR_RELOAD_INTERVAL" if [[ -f "$CHANGE_FLAG" ]]; then restart_reason="config_file_changed" break fi done # 如果 CCMR 已退出且不是文件变化导致,标记为进程崩溃 if [[ -z "$restart_reason" ]] && ! kill -0 "$CCMR_PID" 2>/dev/null; then restart_reason="process_crashed" fi # 清理:停止 inotify watcher 和标记文件 if [[ -n "${WATCHER_PID:-}" ]]; then kill "$WATCHER_PID" 2>/dev/null || true wait "$WATCHER_PID" 2>/dev/null || true fi rm -f "$CHANGE_FLAG" # 输出重启原因 case "$restart_reason" in config_file_changed) echo "[CCMR-WRAPPER] Config file changed. Restarting gateway..." ;; process_crashed) echo "[CCMR-WRAPPER] CCMR process exited unexpectedly (PID: ${CCMR_PID}). Restarting..." ;; *) echo "[CCMR-WRAPPER] Monitoring loop exited (unknown reason). Restarting..." ;; esac # 确保旧进程已停止 if kill -0 "$CCMR_PID" 2>/dev/null; then echo "[CCMR-WRAPPER] Stopping CCMR (PID: ${CCMR_PID})..." kill "$CCMR_PID" 2>/dev/null || true sleep 1 if kill -0 "$CCMR_PID" 2>/dev/null; then kill -9 "$CCMR_PID" 2>/dev/null || true fi wait "$CCMR_PID" 2>/dev/null || true echo "[CCMR-WRAPPER] CCMR stopped" fi # 文件变化时立即重启,否则等一小段时间 if [[ "$restart_reason" != "config_file_changed" ]]; then echo "[CCMR-WRAPPER] Restarting in ${CCMR_RELOAD_INTERVAL}s..." sleep "$CCMR_RELOAD_INTERVAL" fi done