fe / scripts /ccmr-wrapper.sh
GGSheng's picture
fix: 强制推送更新 backup.py 修复逻辑
d992d38 verified
#!/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