#!/bin/bash set -u # --- 配置变量 --- WORK_DIR="/home/dckj/SpectraRust" CMD_PATH="/usr/bin/claude" CMD_PROMPT="使用 codegraph-guide skill 继续执行重构任务。" # 状态文件 PHASE_FILE="${WORK_DIR}/.f2r_phase" COMPLETE_FILE="${WORK_DIR}/.f2r_complete" RATE_LIMIT_FILE="${WORK_DIR}/.f2r_rate_limit" # 内容:退避到期 epoch 秒 FAIL_COUNT_FILE="${WORK_DIR}/.f2r_fail_count" # 内容:连续失败次数 TASKS_FILE="${WORK_DIR}/.f2r_tasks" LOCK_FILE="${WORK_DIR}/.f2r.lock" # 退避参数(秒) BACKOFF_529=900 # 529 模型过载(临时性):15 分钟短退避 BACKOFF_429_FALLBACK=3600 # 429 无法解析重置时间时:默认 1 小时 BACKOFF_MODEL_ERR=1800 # 模型不存在:30 分钟 BACKOFF_CIRCUIT=7200 # 连续失败触发熔断:2 小时 MAX_CONSEC_FAIL=6 # 连续失败熔断阈值 # 日志(export TZ 确保子命令 / date 一致用 UTC+8) export TZ="Asia/Shanghai" LOG_FILE="${WORK_DIR}/logs/claude_$(date +%Y%m%d_%H%M%S).log" CRON_LOG="${WORK_DIR}/logs/cron.log" CRON_LOG_MAX=5242880 # cron.log 归档阈值:5MB log() { echo "[$(date '+%F %T')] $*"; } # --- 1. 环境检查 --- if [ ! -d "$WORK_DIR" ]; then log "❌ 错误: 工作目录不存在: $WORK_DIR" exit 1 fi if [ ! -x "$CMD_PATH" ]; then log "❌ 错误: 命令不存在或不可执行: $CMD_PATH" exit 1 fi # --- 2. 完成检测 --- if [ -f "$COMPLETE_FILE" ]; then log "✅ 重构已标记为完成 ($(cat "$COMPLETE_FILE" 2>/dev/null)),跳过。如需重启请删除 ${COMPLETE_FILE}" exit 0 fi # --- 3. 并发锁(flock,无竞态,替代 pgrep 检测)--- exec 200>"$LOCK_FILE" if ! flock -n 200; then log "⚠️ 已有实例在运行,跳过。" exit 0 fi # --- 4. 峰时段(UTC+8 14:00~18:00)禁用执行 --- CURRENT_HOUR=$(date +%H) if [ "$CURRENT_HOUR" -ge 14 ] && [ "$CURRENT_HOUR" -lt 18 ]; then log "⏰ 高峰期 14:00~18:00 (UTC+8),跳过。" exit 0 fi # --- 5. 限流退避(epoch 秒)--- if [ -f "$RATE_LIMIT_FILE" ]; then LIMIT_UNTIL=$(cat "$RATE_LIMIT_FILE" 2>/dev/null) NOW_EPOCH=$(date +%s) if [[ "$LIMIT_UNTIL" =~ ^[0-9]+$ ]] && [ "$NOW_EPOCH" -lt "$LIMIT_UNTIL" ]; then REMAINING=$(( (LIMIT_UNTIL - NOW_EPOCH) / 60 )) log "⏳ 退避中,还需 ${REMAINING} 分钟(至 $(date -d "@$LIMIT_UNTIL" '+%F %T')),跳过。" exit 0 else rm -f "$RATE_LIMIT_FILE" log "🔓 退避已到期,继续执行。" fi fi # --- 6. cron.log 轮转(超过阈值则归档,不删)--- if [ -f "$CRON_LOG" ]; then CRON_SIZE=$(wc -c < "$CRON_LOG" 2>/dev/null || echo 0) if [ "${CRON_SIZE:-0}" -gt "$CRON_LOG_MAX" ]; then mv "$CRON_LOG" "${CRON_LOG}.$(date +%Y%m%d_%H%M%S).bak" log "📦 cron.log 超过 ${CRON_LOG_MAX}B,已归档。" fi fi # --- 7. 启动 claude --- cd "$WORK_DIR" || { log "❌ 无法进入 ${WORK_DIR}"; exit 1; } nohup "$CMD_PATH" --permission-mode bypassPermissions --print "$CMD_PROMPT" \ < /dev/null > "$LOG_FILE" 2>&1 & CURRENT_PID=$! # --print 同步,等待结束 wait "$CURRENT_PID" 2>/dev/null EXIT_CODE=$? LOG_SIZE=$(wc -c < "$LOG_FILE" 2>/dev/null || echo 0) # --- 8. 错误判定 + 退避 --- # 写入退避到期 epoch set_backoff() { # $1=秒 $2=原因 local secs="$1" reason="$2" local until_epoch until_epoch=$(( $(date +%s) + secs )) echo "$until_epoch" > "$RATE_LIMIT_FILE" log "🔒 ${reason},退避 ${secs}s(至 $(date -d "@$until_epoch" '+%F %T'))。" } # 连续失败计数 +1,超阈值熔断 bump_fail() { # $1=原因 local reason="$1" n n=$(cat "$FAIL_COUNT_FILE" 2>/dev/null || echo 0) n=$(( n + 1 )) echo "$n" > "$FAIL_COUNT_FILE" log "❌ 失败 #${n}:${reason} | 退出码 ${EXIT_CODE} | 日志 ${LOG_SIZE}B" log " 日志路径: ${LOG_FILE}" if [ "$n" -ge "$MAX_CONSEC_FAIL" ]; then set_backoff "$BACKOFF_CIRCUIT" "连续失败 ${n} 次触发熔断" echo 0 > "$FAIL_COUNT_FILE" # 熔断后清零,避免反复触发 fi } # 异常小/缺失日志:claude 未正常产出,直接计失败(避免被误判为成功) if [ "${LOG_SIZE:-0}" -le 50 ]; then bump_fail "日志异常小或缺失(${LOG_SIZE}B)" exit 0 fi # 识别错误类型(优先按日志特征,再按退出码) ERR_TYPE="" if grep -qE "限额将在|使用上限|429[^0-9]" "$LOG_FILE" 2>/dev/null; then ERR_TYPE="429" elif grep -q "529 \[" "$LOG_FILE" 2>/dev/null; then ERR_TYPE="529" elif grep -q "模型不存在" "$LOG_FILE" 2>/dev/null; then ERR_TYPE="model_err" elif [ "$EXIT_CODE" -ne 0 ]; then ERR_TYPE="exit_nonzero" fi case "$ERR_TYPE" in 429) # 用量上限:尽量解析重置时间,否则用默认长退避 RESET_TIME=$(grep -oP '限额将在 \K[\d-]+ [\d:]+' "$LOG_FILE" 2>/dev/null | head -1) if [ -n "$RESET_TIME" ]; then RESET_EPOCH=$(date -d "$RESET_TIME" +%s 2>/dev/null) if [ -n "$RESET_EPOCH" ]; then echo "$RESET_EPOCH" > "$RATE_LIMIT_FILE" log "🔴 429 用量上限,退避至 $(date -d "@$RESET_EPOCH" '+%F %T')(重置于 ${RESET_TIME})。" else set_backoff "$BACKOFF_429_FALLBACK" "429 重置时间解析失败" fi else set_backoff "$BACKOFF_429_FALLBACK" "429 无重置时间" fi bump_fail "429 用量上限" ;; 529) # 模型过载:临时性,短退避(区别于 429 的长退避) set_backoff "$BACKOFF_529" "529 模型过载" bump_fail "529 模型过载" ;; model_err) set_backoff "$BACKOFF_MODEL_ERR" "模型不存在" bump_fail "模型不存在" ;; exit_nonzero) bump_fail "claude 非零退出" ;; *) # 真成功:清零失败计数 echo 0 > "$FAIL_COUNT_FILE" log "✅ 会话完成 | PID ${CURRENT_PID} | 退出码 ${EXIT_CODE} | 日志 ${LOG_SIZE}B" log " 日志路径: ${LOG_FILE}" # 若本次创建了完成标记,提示一下 if [ -f "$COMPLETE_FILE" ]; then log "🎯 检测到 ${COMPLETE_FILE},重构已完成。" fi ;; esac exit 0