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