fix: 限额改 v4 冻结快照,余额红线防扣费
v2 的 stable_limit = usage + balance - BUFFER 依赖余额 API 随消耗实时递减, 实测滞后(08-12 快照 1,558,307 在 usage 涨 45 万期间不动)导致池膨胀、限额 虚高,放行到扣费区(当日超余额 13.8 万已扣费)。 扣费口径(用户确认):是否扣费只取决于当下免费额度,超出即扣费,采集量只 决定明日发放。v4:stable_limit = balance - BUFFER 冻结快照,本地 usage 实时 撞线即拦,等价于本地实时扣减余额;effective = min(2M 配额, stable_limit)。 新增 docs/volc-quota-notes.md 额度机制速查;merge-auto-consume.md 补 §4.5。
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
import os
|
||||
import time
|
||||
import json
|
||||
import hmac
|
||||
import hashlib
|
||||
import datetime
|
||||
import requests
|
||||
import redis
|
||||
import sys
|
||||
|
||||
# ==========================================
|
||||
# 🔧 Config (Hardcoded for Debugging)
|
||||
# ==========================================
|
||||
# Try to load from env, fallback to hardcoded (Matches volc_limit_sync.py defaults)
|
||||
ACCESS_KEY = os.getenv(
|
||||
"VOLC_ACCESS_KEY", "AKLTNGYxZmUyNzNmZjk3NGMyZmEzNjVjZmY4NGM5ZjcxMjU"
|
||||
)
|
||||
SECRET_KEY = os.getenv(
|
||||
"VOLC_SECRET_KEY", "Tm1VNE5XSmlORGs1WXpGbU5EVmtabUUyT1RZeVltTTBNalkxTWpjeE1tTQ=="
|
||||
)
|
||||
HOST = "open.volcengineapi.com"
|
||||
REGION = "cn-beijing"
|
||||
REDIS_HOST = os.getenv(
|
||||
"REDIS_HOST", "redis"
|
||||
) # Assuming running inside container or mapped
|
||||
REDIS_PORT = int(os.getenv("REDIS_PORT", 6379))
|
||||
REDIS_PASSWORD = os.getenv("REDIS_PASSWORD", None)
|
||||
|
||||
print(
|
||||
f"🔧 Config: AK={ACCESS_KEY[:4]}***, SK={SECRET_KEY[:4]}***, Redis={REDIS_HOST}:{REDIS_PORT}"
|
||||
)
|
||||
|
||||
# ==========================================
|
||||
# 🔌 Redis Connection
|
||||
# ==========================================
|
||||
try:
|
||||
print(f"🔌 Connecting to Redis at {REDIS_HOST}:{REDIS_PORT}...")
|
||||
r = redis.Redis(
|
||||
host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, decode_responses=True
|
||||
)
|
||||
r.ping()
|
||||
print("✅ Redis Connected Successfully!")
|
||||
except Exception as e:
|
||||
print(f"❌ Redis Connection Failed: {e}")
|
||||
print("⚠️ Script will continue but cannot check Redis keys.")
|
||||
r = None
|
||||
|
||||
|
||||
# ==========================================
|
||||
# 🗝️ Volcengine Signing Logic
|
||||
# ==========================================
|
||||
def sign(key, msg):
|
||||
return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()
|
||||
|
||||
|
||||
def get_signature_key(key, date_stamp, region_name, service_name):
|
||||
k_date = sign(key.encode("utf-8"), date_stamp)
|
||||
k_region = sign(k_date, region_name)
|
||||
k_service = sign(k_region, service_name)
|
||||
k_signing = sign(k_service, "request")
|
||||
return k_signing
|
||||
|
||||
|
||||
def get_signed_headers(action, version, body_dict, service="billing"):
|
||||
t = datetime.datetime.utcnow()
|
||||
amz_date = t.strftime("%Y%m%dT%H%M%SZ")
|
||||
date_stamp = t.strftime("%Y%m%d")
|
||||
request_body = json.dumps(body_dict)
|
||||
payload_hash = hashlib.sha256(request_body.encode("utf-8")).hexdigest()
|
||||
|
||||
canonical_headers = f"content-type:application/json\nhost:{HOST}\nx-content-sha256:{payload_hash}\nx-date:{amz_date}\n"
|
||||
signed_headers = "content-type;host;x-content-sha256;x-date"
|
||||
canonical_request = f"POST\n/\nAction={action}&Version={version}\n{canonical_headers}\n{signed_headers}\n{payload_hash}"
|
||||
|
||||
credential_scope = f"{date_stamp}/{REGION}/{service}/request"
|
||||
string_to_sign = f"HMAC-SHA256\n{amz_date}\n{credential_scope}\n{hashlib.sha256(canonical_request.encode('utf-8')).hexdigest()}"
|
||||
|
||||
signing_key = get_signature_key(SECRET_KEY, date_stamp, REGION, service)
|
||||
signature = hmac.new(
|
||||
signing_key, string_to_sign.encode("utf-8"), hashlib.sha256
|
||||
).hexdigest()
|
||||
|
||||
authorization_header = f"HMAC-SHA256 Credential={ACCESS_KEY}/{credential_scope}, SignedHeaders={signed_headers}, Signature={signature}"
|
||||
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
"Host": HOST,
|
||||
"X-Date": amz_date,
|
||||
"X-Content-Sha256": payload_hash,
|
||||
"Authorization": authorization_header,
|
||||
}, request_body
|
||||
|
||||
|
||||
# ==========================================
|
||||
# 📦 API Fetch Logic
|
||||
# ==========================================
|
||||
def fetch_all_packages():
|
||||
print("\n🚀 Fetching Volcengine Packages...")
|
||||
action = "ListResourcePackages"
|
||||
version = "2022-01-01"
|
||||
all_packages = []
|
||||
current_token = ""
|
||||
page_count = 0
|
||||
MAX_PAGES = 15 # 🛑 Safety Limit for Debugging
|
||||
seen_tokens = set()
|
||||
|
||||
while True:
|
||||
try:
|
||||
page_count += 1
|
||||
if page_count > MAX_PAGES:
|
||||
print(
|
||||
f"⚠️ Reached MAX_PAGES ({MAX_PAGES}). Stopping fetch to prevent infinite loop."
|
||||
)
|
||||
break
|
||||
|
||||
payload = {
|
||||
"ResourceType": "Package",
|
||||
"MaxResults": "20",
|
||||
"NextToken": current_token,
|
||||
}
|
||||
headers, body = get_signed_headers(action, version, payload)
|
||||
url = f"https://{HOST}/?Action={action}&Version={version}"
|
||||
|
||||
print(f" [Page {page_count}] POST {url} (Token: {current_token[:10]}...)")
|
||||
response = requests.post(url, data=body, headers=headers, timeout=60)
|
||||
|
||||
if response.status_code != 200:
|
||||
print(f"❌ API Error {response.status_code}: {response.text}")
|
||||
break
|
||||
|
||||
data = response.json()
|
||||
|
||||
items = data.get("Result", {}).get("List", [])
|
||||
print(f" Found {len(items)} items in this page.")
|
||||
|
||||
if items:
|
||||
all_packages.extend(items)
|
||||
|
||||
next_token = data.get("Result", {}).get("NextToken", "")
|
||||
|
||||
if not next_token:
|
||||
print(" ✅ No NextToken. Done.")
|
||||
break
|
||||
|
||||
if next_token == current_token:
|
||||
print(" ⚠️ NextToken did not change. Stopping.")
|
||||
break
|
||||
|
||||
if next_token in seen_tokens:
|
||||
print(
|
||||
f" ⚠️ Detected pagination cycle with token {next_token[:10]}... Stopping."
|
||||
)
|
||||
break
|
||||
|
||||
seen_tokens.add(next_token)
|
||||
current_token = next_token
|
||||
time.sleep(0.2)
|
||||
except Exception as e:
|
||||
print(f"❌ Exception fetching packages: {e}")
|
||||
break
|
||||
|
||||
return all_packages
|
||||
|
||||
|
||||
# ==========================================
|
||||
# 📊 Processing Logic
|
||||
# ==========================================
|
||||
VOLC_MODEL_MAP = {
|
||||
"doubao-seed-1-6": ["doubao-seed-1-6-250615", "doubao-seed-1-6-251015"],
|
||||
"doubao-seed-1-8": "doubao-seed-1-8-251228",
|
||||
"doubao-seed-2-0-pro": "doubao-seed-2-0-pro-260215",
|
||||
"doubao-seed-2-0-lite": "doubao-seed-2-0-lite-260215",
|
||||
"doubao-seed-code": "doubao-seed-code-preview-251028",
|
||||
"doubao-seed-translation": "doubao-seed-translation-250915",
|
||||
"deepseek-v3": "deepseek-v3-250324",
|
||||
"deepseek-v3-1": "deepseek-v3-1-terminus",
|
||||
"deepseek-v3-2": "deepseek-v3-2-251201",
|
||||
"deepseek-r1": "deepseek-r1-250528",
|
||||
"kimi-k2": "kimi-k2-thinking-251104",
|
||||
"glm-4_7": "glm-4-7-251222",
|
||||
"glm-4-7": "glm-4-7-251222",
|
||||
}
|
||||
|
||||
SUFFIXES_TO_REMOVE = [
|
||||
"_data_collaboration_resource_pack",
|
||||
"协作奖励计划资源包",
|
||||
"免费在线推理资源包",
|
||||
"免费在线推理包",
|
||||
"资源包",
|
||||
]
|
||||
|
||||
|
||||
def normalize_volc_name(raw_name):
|
||||
if not raw_name:
|
||||
return "unknown"
|
||||
clean_name = raw_name
|
||||
for suffix in sorted(SUFFIXES_TO_REMOVE, key=len, reverse=True):
|
||||
if clean_name.endswith(suffix):
|
||||
clean_name = clean_name.replace(suffix, "")
|
||||
break
|
||||
clean_name = clean_name.replace("_", "-").replace(".", "-")
|
||||
clean_name = clean_name.lower().strip()
|
||||
return VOLC_MODEL_MAP.get(clean_name, clean_name)
|
||||
|
||||
|
||||
def analyze_packages(packages):
|
||||
print("\n🔍 Analyzing Packages...")
|
||||
for pkg in packages:
|
||||
raw_name = pkg.get("ConfigurationName", "Unknown")
|
||||
raw_code = pkg.get("ConfigurationCode", "")
|
||||
spec_code = pkg.get("SpecificationCode", "")
|
||||
|
||||
status = pkg.get("Status", "")
|
||||
if status == "Effective":
|
||||
avail = pkg.get("AvailableAmount", 0)
|
||||
else:
|
||||
avail = 0
|
||||
|
||||
unit = pkg.get("Unit", "token")
|
||||
|
||||
target_raw = raw_code if raw_code else raw_name
|
||||
norm_name = normalize_volc_name(target_raw)
|
||||
|
||||
print(f"📦 Pkg: Name='{raw_name}', Code='{raw_code}', Spec='{spec_code}', Status='{status}'")
|
||||
print(f" -> Target='{target_raw}' -> Norm='{norm_name}'")
|
||||
print(f" -> Avail={avail} {unit} (Effective: {status == 'Effective'})")
|
||||
|
||||
if r:
|
||||
# Check Redis Key that SHOULD be set
|
||||
key = f"limit:dynamic:volcengine:remaining:{norm_name}"
|
||||
val = r.get(key)
|
||||
print(f" -> Redis Key '{key}': {val}")
|
||||
print("-" * 40)
|
||||
|
||||
|
||||
# ==========================================
|
||||
# ▶️ Main Execution
|
||||
# ==========================================
|
||||
if __name__ == "__main__":
|
||||
packages = fetch_all_packages()
|
||||
analyze_packages(packages)
|
||||
print("\n✅ Done.")
|
||||
Reference in New Issue
Block a user