diff --git a/fam-core/src/fam_core/db_layer.py b/fam-core/src/fam_core/db_layer.py index cb99c48..fe90442 100644 --- a/fam-core/src/fam_core/db_layer.py +++ b/fam-core/src/fam_core/db_layer.py @@ -268,6 +268,74 @@ def get_sync_people() -> List[Dict]: conn.close() +def upsert_sync_model_calls(rows: List[Dict]) -> int: + """批量 upsert Oracle 传来的 model_calls 增量(幂等,重复覆盖)。""" + if not rows: + return 0 + conn = get_conn() + n = 0 + try: + cur = conn.cursor() + for r in rows: + cur.execute( + """INSERT INTO sync_model_calls + (id, provider, model, video_id, filename, started_at, + duration_sec, success, error, created_at, synced_at) + VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s, NOW()) + ON DUPLICATE KEY UPDATE + provider=VALUES(provider), + model=VALUES(model), + video_id=VALUES(video_id), + filename=VALUES(filename), + started_at=VALUES(started_at), + duration_sec=VALUES(duration_sec), + success=VALUES(success), + error=VALUES(error), + created_at=VALUES(created_at), + synced_at=NOW()""", + (r.get('id'), r.get('provider'), r.get('model'), + r.get('video_id'), r.get('filename'), r.get('started_at'), + r.get('duration_sec') or 0, 1 if r.get('success') else 0, + (r.get('error') or '')[:500], r.get('created_at'))) + n += 1 + conn.commit() + return n + finally: + conn.close() + + +def get_sync_model_calls(limit: int = 200) -> List[Dict]: + """最近模型调用记录(前端统计展示)。""" + conn = get_conn() + try: + cur = conn.cursor(pymysql.cursors.DictCursor) + cur.execute( + "SELECT id, provider, model, video_id, filename, started_at, " + "duration_sec, success, error, created_at " + "FROM sync_model_calls ORDER BY id DESC LIMIT %s", (limit,)) + return cur.fetchall() + finally: + conn.close() + + +def get_sync_model_calls_stats() -> Dict: + """模型调用统计:按 provider+model 聚合成功/失败/平均耗时。""" + conn = get_conn() + try: + cur = conn.cursor(pymysql.cursors.DictCursor) + cur.execute( + """SELECT provider, model, + SUM(CASE WHEN success=1 THEN 1 ELSE 0 END) AS ok_cnt, + SUM(CASE WHEN success=0 THEN 1 ELSE 0 END) AS fail_cnt, + ROUND(AVG(duration_sec), 1) AS avg_duration, + MAX(created_at) AS last_call + FROM sync_model_calls + GROUP BY provider, model ORDER BY provider, model""") + return cur.fetchall() + finally: + conn.close() + + def get_sync_named_members() -> List[str]: """已命名成员的真名列表(供 UI 下拉 / 快捷选择)。""" conn = get_conn() diff --git a/fam-core/src/fam_core/oracle_sync.py b/fam-core/src/fam_core/oracle_sync.py index 8e80783..90e6af1 100644 --- a/fam-core/src/fam_core/oracle_sync.py +++ b/fam-core/src/fam_core/oracle_sync.py @@ -83,21 +83,23 @@ class OracleSync: videos = data.get('videos', []) or [] events = data.get('events', []) or [] people = data.get('people', []) or [] + model_calls = data.get('model_calls', []) or [] server_time = data.get('server_time', '') or '' n_videos = db_layer.upsert_sync_videos(videos) n_events = db_layer.upsert_sync_events(events) n_people = db_layer.upsert_sync_people(people) + n_calls = db_layer.upsert_sync_model_calls(model_calls) if server_time: db_layer.set_sync_cursor(server_time) self._last_sync_at = datetime.now() self._last_error = None - self._last_count = (n_videos, n_events, n_people) + self._last_count = (n_videos, n_events, n_people, n_calls) logger.info( f"同步完成: videos+{n_videos} events+{n_events} people+{n_people} " - f"since={since!r} -> server_time={server_time}") + f"model_calls+{n_calls} since={since!r} -> server_time={server_time}") return True # ------------------------------------------------------------------ diff --git a/fam-ui/src/app.py b/fam-ui/src/app.py index 6520ae4..bf48d92 100644 --- a/fam-ui/src/app.py +++ b/fam-ui/src/app.py @@ -317,7 +317,7 @@ st.sidebar.markdown( # 顶部横向导航 page = st.segmented_control( "功能页面", - ["🕒 事件时间轴", "💬 AI 对话", "📝 对话历史", "👤 人物管理", "📈 统计图表"], + ["🕒 事件时间轴", "💬 AI 对话", "📝 对话历史", "👤 人物管理", "📈 统计图表", "🤖 模型统计"], default="🕒 事件时间轴", label_visibility="collapsed" ) or "🕒 事件时间轴" @@ -901,3 +901,83 @@ elif page == "📈 统计图表": finally: if conn: conn.close() + +# ============================================================ +# 模型统计页(云端模型识别成功/失败统计) +# ============================================================ +elif page == "🤖 模型统计": + page_header('🤖', '云端模型统计', '请求时间 · 耗时 · 成功/失败 · 失败原因') + + conn = None + try: + conn = get_db_conn() + cursor = conn.cursor() + + # ---- 按模型聚合:成功/失败/成功率/平均耗时 ---- + st.markdown('