# source 后用:dsh_with_fallback <命令...>,命令里的 @PATCH@ 是 DeepSeek 补丁的插入点。 # 先走默认模型(GLM Coding Plan);它额度用尽/限流且没产出答案时,改用 DeepSeek 重跑, # 并写 $DSH_FB_STATE=冷却截止 epoch:冷却期内直接走 DeepSeek,冷却过后下一次调用先探测 GLM,恢复了就自动切回。 # 错误码依据智谱文档 docs.bigmodel.cn/cn/api/api-code(均为 HTTP 429): # 额度类 1308/1310/1316-1321(响应带 next_flush_time=限额重置时间)、1309 套餐到期、1113 欠费 # → 冷却到 next_flush_time(+60s);解析不到时间 = 至少 5 小时(MIN_COOLDOWN) # 瞬时类 1302 速率限制 / 1305 模型过载(文档:稍后重试)→ 只冷却 60s,别因瞬时 429 锁 5 小时 # ponytail: 只在「没产出答案」时重跑(防止 admin 任务已执行过工具又被重放一遍); # 错误码没覆盖到的 RATE_LIMIT/429 按瞬时类处理,上限=未知的长期限流会每 60s 白打一次 GLM; # next_flush_time 的确切格式文档没写,按「秒/毫秒时间戳、或日期时间串(无时区=北京时间)」解析,真机见到后再收紧。 DSH_FB_STATE="${DSH_FB_STATE:-$HOME/dsh/.glm_down_until}" DSH_FB_MIN_COOLDOWN="${DSH_FB_MIN_COOLDOWN:-18000}" # 额度类查不到重置时间时的最短冷却=5 小时 DSH_FB_MARGIN="${DSH_FB_MARGIN:-60}" # 在报错给的重置时间上多等一点 DSH_FB_SHORT="${DSH_FB_SHORT:-60}" # 瞬时限流的冷却秒数 DSH_FB_PATCH="${DSH_FB_PATCH:-$HOME/dsh/patches/deepseek.yml}" _N='(^|[^0-9])'; _M='([^0-9]|$)' # 错误码前后不能是数字(macOS grep 不支持 \b) DSH_FB_QUOTA="${DSH_FB_QUOTA:-QUOTA|next_flush_time|使用上限|${_N}(1308|1310|1309|1113|131[6-9]|132[01])${_M}}" DSH_FB_TRANSIENT="${DSH_FB_TRANSIENT:-RATE_LIMIT|HTTP_429|${_N}(1302|1305)${_M}}" # $1=stderr 文件 → 输出额度类冷却截止 epoch _dsh_fb_until() { local now t; now=$(date +%s) t=$(python3 - "$1" "$now" <<'PY' import re, sys, calendar txt, now = open(sys.argv[1], errors="replace").read(), int(sys.argv[2]) DT = r"20\d\d-\d\d-\d\d[ T]\d\d:\d\d(?::\d\d)?(?:Z|[+-]\d\d:?\d\d)?" def dt(v): # 日期时间串 → epoch;无时区按北京时间 m = re.search(r"(20\d\d)-(\d\d)-(\d\d)[ T](\d\d):(\d\d)(?::(\d\d))?(Z|[+-]\d\d:?\d\d)?", v) if not m: return 0 y, mo, d, h, mi, s = (int(x or 0) for x in m.groups()[:6]) z, off = m.group(7), 8 * 3600 if z == "Z": off = 0 elif z: off = (1 if z[0] == "+" else -1) * (int(z[1:3]) * 3600 + int(z[-2:]) * 60) return calendar.timegm((y, mo, d, h, mi, s)) - off t = 0 k = re.search(r"next_flush_time\W{0,4}(\d{9,14})", txt) # 数字时间戳(秒或毫秒) if k: t = int(k.group(1)); t = t // 1000 if t > 10**11 else t if not t: k = re.search(r"next_flush_time\W{0,4}(" + DT + ")", txt) # 日期时间串 if k: t = dt(k.group(1)) if not t: m = re.findall(DT, txt) # 文案里的「将在 … 重置」 if m: t = dt(m[-1]) if not t: r = re.search(r"retry[- ]after\D{0,3}(\d+)", txt, re.I) if r: t = now + int(r.group(1)) if now < t <= now + 8 * 86400: print(t) PY ) if [ -n "$t" ]; then echo $(( t + DSH_FB_MARGIN )); else echo $(( now + DSH_FB_MIN_COOLDOWN )); fi } dsh_with_fallback() { local x pid rc down=0 glm=() ds=() out err for x in "$@"; do if [ "$x" = "@PATCH@" ]; then ds+=(--patch "$DSH_FB_PATCH"); else glm+=("$x"); ds+=("$x"); fi done [ -r "$DSH_FB_STATE" ] && down=$(cat "$DSH_FB_STATE" 2>/dev/null || echo 0) export TMPDIR="${PREFIX:-/data/data/com.termux/files/usr}/tmp" # Android 环境里 TMPDIR 常被预设成不可写的 /data/local/tmp,必须覆盖 out=$(mktemp) && err=$(mktemp) || exec "${glm[@]}" # 建不了临时文件就退回原行为,别让切换逻辑弄坏主链路 trap 'kill "$pid" 2>/dev/null' TERM INT # 不再 exec,转发信号,超时强杀仍能杀到 dsh if [ "$(date +%s)" -ge "${down:-0}" ]; then "${glm[@]}" >"$out" 2>"$err" & pid=$!; wait "$pid"; rc=$? if [ "$rc" -ne 0 ] && [ ! -s "$out" ]; then if grep -qE "$DSH_FB_QUOTA" "$err"; then down=$(_dsh_fb_until "$err") elif grep -qE "$DSH_FB_TRANSIENT" "$err"; then down=$(( $(date +%s) + DSH_FB_SHORT )) else down=""; fi if [ -n "$down" ]; then echo "$down" > "$DSH_FB_STATE" echo "[fallback] GLM 额度/限流,切 DeepSeek 至 epoch $down" >&2 "${ds[@]}" >"$out" 2>"$err" & pid=$!; wait "$pid"; rc=$? fi fi else "${ds[@]}" >"$out" 2>"$err" & pid=$!; wait "$pid"; rc=$? fi cat "$err" >&2; cat "$out"; rm -f "$out" "$err"; return "$rc" }