181 lines
6.1 KiB
Bash
Executable File
181 lines
6.1 KiB
Bash
Executable File
#!/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
|