#!/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))