91 lines
3.2 KiB
Python
Executable file
91 lines
3.2 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""collector.py · Qoder CN 用量采集器
|
|
扫会话日志聚合每模型调用量,输出 ~/.stellar-gauge/usage.json。
|
|
只读宿主日志,不写宿主任何东西。
|
|
"""
|
|
import json, os, re, glob, time
|
|
from datetime import datetime, timedelta
|
|
|
|
LOGS_ROOT = os.path.expanduser(
|
|
"~/Library/Application Support/QoderCN/logs")
|
|
STATE_FILE = os.path.expanduser("~/.stellar-gauge/usage.json")
|
|
OFFSETS_FILE = os.path.expanduser("~/.stellar-gauge/offsets.json")
|
|
|
|
RE_CTX = re.compile(
|
|
r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+).*Context usage update: "
|
|
r"\{.*?\"usedTokens\":(\d+)")
|
|
RE_MODEL = re.compile(r"applyModelRuntimeMeta: model=([A-Za-z0-9_.-]+)")
|
|
|
|
# Qoder 内部别名 → 展示名
|
|
ALIAS = {
|
|
"qmodel_38max": "Qwen3.8-Max",
|
|
"qmodel_37max": "Qwen3.7-Max",
|
|
"dsmodel_v4pro": "DeepSeek-v4-Pro",
|
|
}
|
|
|
|
|
|
def load(path, default):
|
|
try:
|
|
with open(path) as f:
|
|
return json.load(f)
|
|
except Exception:
|
|
return default
|
|
|
|
|
|
def scan():
|
|
offsets = load(OFFSETS_FILE, {})
|
|
usage = load(STATE_FILE, {"requests": {}, "tokens": {},
|
|
"daily": {}, "meta": {}})
|
|
now = datetime.now()
|
|
for f in glob.glob(os.path.join(LOGS_ROOT, "*", "questWindow", "agent.log")):
|
|
try:
|
|
size = os.path.getsize(f)
|
|
off = offsets.get(f, 0)
|
|
if size < off: # 日志轮转
|
|
off = 0
|
|
if size == off:
|
|
continue
|
|
with open(f, errors="ignore") as fh:
|
|
fh.seek(off)
|
|
chunk = fh.read()
|
|
offsets[f] = size
|
|
except Exception:
|
|
continue
|
|
cur_model = "unknown"
|
|
prev_used = None
|
|
for line in chunk.splitlines():
|
|
m = RE_MODEL.search(line)
|
|
if m:
|
|
cur_model = ALIAS.get(m.group(1), m.group(1))
|
|
continue
|
|
m = RE_CTX.search(line)
|
|
if m:
|
|
ts, used = m.group(1), int(m.group(2))
|
|
# 增量估算:两次上报差值近似为本轮新增 token
|
|
delta = used - prev_used if prev_used else min(used, 8000)
|
|
prev_used = used
|
|
day = ts[:10]
|
|
usage["requests"][cur_model] = \
|
|
usage["requests"].get(cur_model, 0) + 1
|
|
usage["tokens"][cur_model] = \
|
|
usage["tokens"].get(cur_model, 0) + max(delta, 0)
|
|
d = usage["daily"].setdefault(day, {})
|
|
d[cur_model] = d.get(cur_model, 0) + max(delta, 0)
|
|
# 只留 14 天
|
|
keep = (now - timedelta(days=14)).strftime("%Y-%m-%d")
|
|
usage["daily"] = {k: v for k, v in usage["daily"].items() if k >= keep}
|
|
usage["meta"] = {"updated_at": now.isoformat(timespec="seconds"),
|
|
"note": "token 为本地增量估算,非计费 Credits"}
|
|
tmp = STATE_FILE + ".tmp"
|
|
with open(tmp, "w") as f:
|
|
json.dump(usage, f, ensure_ascii=False, indent=1)
|
|
os.replace(tmp, STATE_FILE)
|
|
with open(OFFSETS_FILE, "w") as f:
|
|
json.dump(offsets, f)
|
|
return usage
|
|
|
|
|
|
if __name__ == "__main__":
|
|
u = scan()
|
|
print(json.dumps({"requests": u["requests"],
|
|
"meta": u["meta"]}, ensure_ascii=False, indent=1))
|