feat: upgrade客服多店铺分流、批量报价与稳定性防护

This commit is contained in:
2026-02-28 18:52:31 +08:00
parent c39840fe15
commit 46143be86c
16 changed files with 1329 additions and 37 deletions

104
utils/metrics_tracker.py Normal file
View File

@@ -0,0 +1,104 @@
# -*- coding: utf-8 -*-
"""
轻量指标采集器
- 事件落盘到 JSONL
- 提供近 N 小时聚合,给 /api/metrics 使用
"""
from __future__ import annotations
import json
from datetime import datetime, timedelta
from pathlib import Path
from typing import Dict, Any
ROOT = Path(__file__).resolve().parent.parent
METRICS_FILE = ROOT / "config" / ".runtime_metrics.jsonl"
def _now_iso() -> str:
return datetime.now().isoformat(timespec="seconds")
def emit(event: str, **fields: Any):
"""记录一条事件,失败不抛异常。"""
try:
METRICS_FILE.parent.mkdir(parents=True, exist_ok=True)
payload = {"ts": _now_iso(), "event": event}
payload.update(fields or {})
with METRICS_FILE.open("a", encoding="utf-8") as f:
f.write(json.dumps(payload, ensure_ascii=False) + "\n")
except Exception:
pass
def _iter_recent(hours: int = 24):
if not METRICS_FILE.exists():
return []
cutoff = datetime.now() - timedelta(hours=hours)
items = []
try:
with METRICS_FILE.open("r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
try:
obj = json.loads(line)
ts = datetime.fromisoformat(obj.get("ts", ""))
except Exception:
continue
if ts >= cutoff:
items.append(obj)
except Exception:
return []
return items
def get_runtime_summary(hours: int = 24) -> Dict[str, Any]:
"""近 N 小时运行指标。"""
rows = _iter_recent(hours=hours)
counts: Dict[str, int] = {}
for r in rows:
e = r.get("event", "unknown")
counts[e] = counts.get(e, 0) + 1
inbound = counts.get("inbound_msg", 0)
transfer = counts.get("transfer_to_human", 0)
quote = counts.get("quote_generated", 0)
ai_fail = counts.get("ai_call_failed", 0)
gemini_req = counts.get("gemini_request", 0)
no_image = counts.get("gemini_no_image", 0)
return {
"window_hours": hours,
"counts": counts,
"rates": {
"transfer_rate": round((transfer / inbound) * 100, 2) if inbound else 0.0,
"quote_rate": round((quote / inbound) * 100, 2) if inbound else 0.0,
"ai_fail_rate": round((ai_fail / inbound) * 100, 2) if inbound else 0.0,
"no_image_rate": round((no_image / gemini_req) * 100, 2) if gemini_req else 0.0,
},
}
def get_dashboard(hours: int = 24) -> Dict[str, Any]:
"""业务+运行看板聚合。"""
runtime = get_runtime_summary(hours=hours)
try:
from db.deal_outcome_db import get_daily_summary
daily = get_daily_summary()
deal_ok = int(daily.get("成交数", 0))
deal_fail = int(daily.get("未成交数", 0))
total = deal_ok + deal_fail
conversion = round((deal_ok / total) * 100, 2) if total else 0.0
except Exception:
deal_ok = deal_fail = 0
conversion = 0.0
return {
"runtime": runtime,
"business": {
"deal_success": deal_ok,
"deal_fail": deal_fail,
"conversion_rate": conversion,
},
}