File size: 7,810 Bytes
62f6d9a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 | #!/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
|