feat: 合入火山免费额度自动消耗任务(每日23点洗过期包防扣费)

- 新增 volc_consume.py:消耗任务模块,直连 Ark 洗 9 天内过期资源包
  (旧 calculate_plan 均分×1.1/24h冲刺保留 + 双 clamp:2M 采集上限、余额−BUFFER 防扣费)
- volc_limit_sync.py:限额 v2 简化(stable_limit = usage+balance−BUFFER,删除峰值状态机 ~90 行),
  HARD_DAILY_LIMITS 统一 2M(pre-block 不滞后,1.8M 折扣作废),
  新增 _maybe_trigger_daily_consume(北京时间 23:00 ± 抖动,SETNX 去重,延迟导入)
- docker-compose 挂载 volc_consume.py;AGENTS.md/文档同步
This commit is contained in:
vibe-code
2026-08-11 13:15:24 +00:00
parent 2cc7c7d08b
commit 96b81d52c4
5 changed files with 630 additions and 98 deletions
+65 -86
View File
@@ -3,6 +3,8 @@ import sys
import time
import json
import hmac
import math
import random
import hashlib
import datetime
import requests
@@ -146,13 +148,10 @@ MONITORED_MODEL_ALIASES = {}
# Format: "clean-model-name": limit_in_tokens
# Or with hash: "clean-model-name:hash": limit_in_tokens
HARD_DAILY_LIMITS = {
# Specific Limit for Doubao Code
"doubao-seed-code-preview-251028": 4_800_000,
# Specific Limits for Doubao Seed 2.0 models on HUOSHAN_BASE_URL
"doubao-seed-2-0-pro-260215:ba0383d3": 1_900_000,
"doubao-seed-2-0-lite-260215:ba0383d3": 1_900_000,
# Default Limit for all other models (Universal Fallback)
"__DEFAULT__": 1_800_000,
# 火山免费额度规则(2026-08):每模型每日采集上限 2M,次日按采集量发放资源包。
# 请求前闸门(async_pre_call_hook)已不滞后 → 无需打折(旧 1.8M/480 万为 post-hoc 时代遗产)。
# 统一 2M,余额相关的动态限额由 stable_limit(usage+balance−BUFFER)表达。
"__DEFAULT__": 2_000_000,
}
# ==========================================
@@ -973,8 +972,12 @@ def process_packages_breakdown(packages):
return model_counts
def active_monitor_loop():
# Config Discovery
def discover_monitored_models():
"""
🔎 从 config.yaml 自动发现纳入监控的火山模型,填充 MONITORED_MODEL_PAIRS /
MONITORED_MODEL_ALIASES。返回监控模型名 set。
独立函数化:volc_consume.py 手动运行(无守护线程)时也可复用。
"""
global MONITORED_MODEL_PAIRS, MONITORED_MODEL_ALIASES
MONITORED_MODEL_PAIRS.clear()
MONITORED_MODEL_ALIASES.clear()
@@ -1107,6 +1110,50 @@ def active_monitor_loop():
except Exception as e:
print(f"Config Auto-discovery failed / 配置自动发现失败: {e}", flush=True)
return MONITORED_MODELS
def _maybe_trigger_daily_consume():
"""
🕰️ 每日北京时间 23:00 触发免费额度消耗任务(洗过期资源包)。
- consume:last_run:{北京日期} 去重(SETNX,当天只触发一次)
- daemon 线程执行,不阻塞主循环;线程内 0-10 分钟随机抖动,避免固定 23:00 被风控识别
- 延迟 import volc_consume,避免模块级循环 import
"""
if not redis_client:
return
try:
now = time.time()
beijing_ts = now + (8 * 3600)
beijing_hour = time.gmtime(beijing_ts).tm_hour
if beijing_hour != 23:
return
date_str = time.strftime("%Y-%m-%d", time.gmtime(beijing_ts))
lock_key = f"consume:last_run:{date_str}"
if redis_client.set(lock_key, "1", nx=True, ex=172800) is None:
return # 今天已触发过
log("🚀 触发每日消耗任务(洗过期资源包防退费)...", level="INFO")
def _run():
try:
time.sleep(random.randint(0, 600))
import volc_consume # 延迟导入,避免模块级循环 import
volc_consume.run_consume_all()
except Exception as e:
log(f"❌ 消耗任务异常: {e}\n{traceback.format_exc()}", level="ERROR")
t = threading.Thread(target=_run, daemon=True)
t.start()
except Exception as e:
log(f"❌ 消耗任务调度失败: {e}", level="ERROR")
def active_monitor_loop():
# Config Discovery
MONITORED_MODELS = discover_monitored_models()
if not redis_client:
print(
"❌ Redis not connected. Exiting monitor loop / Redis 未连接,退出监控循环.",
@@ -1142,6 +1189,7 @@ def active_monitor_loop():
while True:
try:
time.sleep(CHECK_INTERVAL)
_maybe_trigger_daily_consume()
now = time.time()
# Enforce Beijing Time for daily operations
@@ -1336,86 +1384,17 @@ def active_monitor_loop():
dynamic_key = f"limit:dynamic:volcengine:remaining:{target}"
# ==========================================
# 🏔️ Daily Peak Lock (First-Sync + Balance-Gated Re-Lock)
# 📏 v2 简化限额:stable_limit = usage + balance − BUFFER
# ==========================================
# 背景:AvailableAmount 是各资源包实例的实时剩余,但火山统计有
# 更新滞后(高用量时段会短暂冻结,实测 20 万 token 消耗内余额不动)。
# 旧逻辑把每次用量增长误判为"余额恢复"(Jump)锁更高峰值,
# 导致 stable_limit 越算越多(当天 399,836 -> 599,511 一路抬升)。
# 修正策略(日内限额单调可控):
# 1. 首次同步锁定当日峰值(= usage + balance 的总配额估计);
# 2. 仅当 balance 自身净上涨(新资源包发放/对账回补,≥50k)
# 时,按新增额度抬高峰值 —— 幅度 = 历史峰值 + 发放增量,
# 绝不把滞后虚高的 usage 一起算进去;
# 3. balance 回落(额度扣减追上用量)时 Drop 收紧;
# 4. usage 增长驱动的 pool 上涨一律不得抬高限额。
# 实测:协作奖励计划包每自然日发放、偶发一天 2 个,故必须有第 2 条;
# 次日 00:00 (CST) 自动重建当日峰值。
# 旧 Peak-Buffer(峰值首锁/门控重锁/DROP,~90 行状态机)为存量
# 资源包场景设计;日返还模型下 balance 每天真实跳涨,锁峰值会把
# 真实发放当异常压住,且比例 margin 在 usage 逼近 balance 时
# 数学失效(允许再烧量 > 剩余余额 → 烧穿扣费)。
# usage + balance = 当日初始可用池,消耗中恒定;返还/过期自然增减。
# BUFFER 覆盖余额统计滞后(实测 20 万内不动)。见 merge-auto-consume.md §4。
total_pool = current_usage + balance
peak_key = f"volc:peak_pool:{today_str}:{target}"
# Short-term memory (Last seen value) - 仅用于日志对比/增量计算
last_pool_key = f"volc:last_seen_pool:{target}"
last_pool_val = redis_client.get(last_pool_key)
last_pool = float(last_pool_val) if last_pool_val else 0.0
last_balance_key = f"volc:last_balance:{target}"
last_balance_val = redis_client.get(last_balance_key)
last_balance = float(last_balance_val) if last_balance_val else 0.0
# Long-term memory (Day's High Water Mark)
old_peak_val = redis_client.get(peak_key)
historical_peak = float(old_peak_val) if old_peak_val else 0.0
current_peak = historical_peak
# 1. INITIALIZATION (First sync of the day): lock once
if historical_peak == 0:
if DEBUG_MODE and last_pool > 0:
print(
f"DEBUG: 📌 Locking Daily Peak ({int(last_pool):,} -> {int(total_pool):,}) - First Sync Today.",
flush=True,
)
current_peak = total_pool
redis_client.setex(peak_key, 172800, int(current_peak))
# 2. RECHARGE (Balance-driven upward re-lock)
# 新资源包发放 / API 对账回补 → balance 自身净上涨 ≥50k。
# 只把"发放增量"加进已锁定的峰值,不重算整个滞后 pool。
elif last_balance > 0 and (balance - last_balance) >= 50000:
grant_delta = int(balance - last_balance)
current_peak = historical_peak + grant_delta
if DEBUG_MODE:
print(
f"DEBUG: 🆕 New Quota Detected (balance {int(last_balance):,} -> {int(balance):,}, +{grant_delta:,}) - Re-locking Daily Peak to {int(current_peak):,}.",
flush=True,
)
redis_client.setex(peak_key, 172800, int(current_peak))
# 3. DROP DETECTION (Resource Expiry / latency catch-up)
# pool 回落时收紧限额;回落之后保持不变,绝不回升。
elif total_pool < historical_peak and historical_peak > 0:
drop_ratio = (
historical_peak - total_pool
) / historical_peak
if DEBUG_MODE:
print(
f"DEBUG: 📉 Pool Drop Detected ({int(historical_peak):,} -> {int(total_pool):,}) - Ratio: {drop_ratio:.1%} - Adjusting Limit Down.",
flush=True,
)
current_peak = total_pool
redis_client.setex(peak_key, 172800, int(current_peak))
# 4. NO USAGE-DRIVEN RE-LOCK: 用量增长(balance 不变)不得抬高限额。
# (旧逻辑的 Bug 就在这里:把滞后虚高当余额恢复。)
# Always update "Last Seen" for next comparison
redis_client.setex(last_pool_key, 86400, int(total_pool))
redis_client.setex(last_balance_key, 86400, int(balance))
# Calculate Stable Limit
safety_margin = max(current_peak * 0.15, 250000)
stable_limit = int(max(0, current_peak - safety_margin))
stable_limit = int(max(0, total_pool - BUFFER))
stable_limit_key = (
f"limit:dynamic:volcengine:stable_limit:{target}"