feat(监控): 实时服务状态界面 - Oracle 新增 service_activity 活动表(只保留7天,写入时清理)与 /api/oracle/activity 接口(队列实时状态/当前处理视频/模型调用/rclone/人物合并/最近50条活动);VideoQueue 打点+当前处理跟踪+status();PersonService 打点;rclone_sync.sh 同步结果写活动表;fam-ui 新增'🖥 服务状态'页(状态卡+活动时间流,直连 Oracle 实时拉取)

This commit is contained in:
ericwyuan
2026-08-21 15:52:36 +08:00
parent da7631976d
commit 0d0a7f6ef8
7 changed files with 311 additions and 9 deletions

View File

@@ -329,7 +329,7 @@ st.sidebar.markdown(
# 顶部横向导航
page = st.segmented_control(
"功能页面",
["🕒 事件时间轴", "💬 AI 对话", "📝 对话历史", "👤 人物管理", "📈 统计图表", "🤖 模型统计"],
["🕒 事件时间轴", "💬 AI 对话", "📝 对话历史", "👤 人物管理", "📈 统计图表", "🤖 模型统计", "🖥 服务状态"],
default="🕒 事件时间轴",
label_visibility="collapsed"
) or "🕒 事件时间轴"
@@ -1032,3 +1032,159 @@ elif page == "🤖 模型统计":
finally:
if conn:
conn.close()
# ============================================================
# 服务状态页(实时查看各服务在做什么:队列/模型/rclone/人物/NAS 同步)
# ============================================================
elif page == "🖥 服务状态":
page_header('🖥', '服务状态', '各服务实时动态 · 最近 7 天活动记录')
c1, c2, c3 = st.columns([1, 1, 5])
with c1:
if st.button("🔄 刷新", use_container_width=True):
st.rerun()
with c2:
st.caption("自动每 5 分钟更新页面(点击刷新立即更新)")
# ---- 拉取 Oracle 实时状态 + NAS 同步状态 ----
act_data = None
act_err = None
if _oracle_url and _oracle_token:
try:
r = requests.get(
f"{_oracle_url}/api/oracle/activity?token={_oracle_token}",
timeout=15)
if r.status_code == 200:
act_data = r.json()
else:
act_err = f"Oracle activity HTTP {r.status_code}"
except Exception as e:
act_err = f"连接 Oracle 失败: {e}"
else:
act_err = "未配置 oracle_url / oracle_token"
nas_sync = None
try:
rr = requests.get(f"{_core_url}/api/status", timeout=10)
if rr.status_code == 200:
nas_sync = rr.json().get('sync', {})
except Exception:
pass
if act_err:
st.warning(act_err)
if act_data is None and nas_sync is None:
st.markdown('<div class="fam-empty">暂时无法获取服务状态</div>', unsafe_allow_html=True)
else:
# ---- 服务状态卡 ----
st.markdown('<div style="font-size:15px;font-weight:700;color:#f1f5f9;'
'margin:6px 0 12px 0;">各服务当前状态</div>', unsafe_allow_html=True)
q = (act_data or {}).get('queue') or {}
dbinfo = (act_data or {}).get('db') or {}
cur_vid = q.get('current')
cur_txt = ''
if cur_vid:
cur_txt = f"#{cur_vid.get('video_id')} {esc((cur_vid.get('filename') or '')[-42:])}"
by_status = dbinfo.get('by_status') or {}
st.markdown(
f'<div class="stat-row">'
f'<div class="stat-card"><div class="s-val">{"运行中" if q.get("running") else "停止"}</div>'
f'<div class="s-label">FAM-Edge 队列</div></div>'
f'<div class="stat-card"><div class="s-val">{q.get("queued", 0)}</div>'
f'<div class="s-label">排队中</div></div>'
f'<div class="stat-card ok"><div class="s-val">{by_status.get("done", 0)}</div>'
f'<div class="s-label">已完成</div></div>'
f'<div class="stat-card warn"><div class="s-val">{by_status.get("pending", 0)}</div>'
f'<div class="s-label">待处理</div></div>'
f'<div class="stat-card err"><div class="s-val">{by_status.get("failed", 0)}</div>'
f'<div class="s-label">失败</div></div>'
f'</div>', unsafe_allow_html=True)
# 详细状态行
qs = q.get('stats') or {}
st.markdown(
f'<div style="font-size:12px;color:#8b93a7;margin:8px 0 4px 0;">'
f'▶ 正在处理:<b style="color:#7dd3fc;">{cur_txt}</b>'
f'<span style="margin-left:14px;">生产者已入队 {qs.get("produced", 0)} · '
f'成功 {qs.get("consumed_ok", 0)} · 失败 {qs.get("consumed_fail", 0)}</span></div>',
unsafe_allow_html=True)
# 服务卡rclone / 人物 / NAS 同步 / 模型
def _svc_card(icon, name, val, sub, color='#cbd5e1'):
return (
f'<div style="flex:1;min-width:180px;background:#0f172a;border:1px solid #1e293b;'
f'border-radius:12px;padding:12px 14px;margin:6px 6px 6px 0;">'
f'<div style="font-size:12px;color:#64748b;">{icon} {esc(name)}</div>'
f'<div style="font-size:13px;color:{color};margin-top:6px;line-height:1.5;">{val}</div>'
f'<div style="font-size:11px;color:#475569;margin-top:4px;">{esc(sub)}</div>'
f'</div>')
rclone = (act_data or {}).get('rclone') or {}
person = (act_data or {}).get('person') or {}
cards = []
# rclone
if rclone:
cards.append(_svc_card('🔄', 'rclone 同步',
f"{esc((rclone.get('detail') or '')[:70])}",
f"最近 {(rclone.get('ts') or '')[:19]}", '#4ade80'))
else:
cards.append(_svc_card('🔄', 'rclone 同步', '暂无记录', ''))
# 人物合并
if person:
cards.append(_svc_card('👤', '人物合并',
f"{esc((person.get('action') or ''))}",
f"{(person.get('ts') or '')[:19]} · {esc((person.get('detail') or '')[:46])}", '#c084fc'))
else:
cards.append(_svc_card('👤', '人物合并', '暂无记录', ''))
# NAS 同步
if nas_sync:
cnt = nas_sync.get('last_count') or [0, 0, 0, 0]
cards.append(_svc_card('📡', 'NAS 同步',
f"游标 {esc(str(nas_sync.get('cursor') or '')[:19])}",
f"最近 {(str(nas_sync.get('last_sync_at') or ''))[:19]} · 增量 V{cnt[0]} E{cnt[1]} P{cnt[2]} M{cnt[3]}",
'#fbbf24'))
else:
cards.append(_svc_card('📡', 'NAS 同步', '不可达', ''))
# 模型
mcs = (act_data or {}).get('model_calls') or []
if mcs:
m0 = mcs[0]
cards.append(_svc_card('🧠', '云端模型',
f"最近:{esc(m0.get('model') or '')} {'' if m0.get('success') else '' + esc((m0.get('error') or '')[:30])}",
f"{(m0.get('started_at') or '')[:19]} · 耗时 {round(m0.get('duration_sec') or 0, 1)}s · 近5次 成功{sum(1 for m in mcs if m.get('success'))}/{len(mcs)}",
'#4ade80'))
else:
cards.append(_svc_card('🧠', '云端模型', '暂无调用', ''))
st.markdown(
f'<div style="display:flex;flex-wrap:wrap;gap:2px;">{"".join(cards)}</div>',
unsafe_allow_html=True)
# ---- 活动时间流 ----
acts = (act_data or {}).get('activities') or []
st.markdown('<div style="font-size:15px;font-weight:700;color:#f1f5f9;'
'margin:24px 0 12px 0;">最近活动</div>', unsafe_allow_html=True)
if not acts:
st.markdown('<div class="fam-empty">暂无活动记录(服务刚启动?)</div>',
unsafe_allow_html=True)
else:
_svc_badge = {
'queue': ('<span class="bdg" style="background:#0e2233;border:1px solid #164e63;color:#7dd3fc;">队列</span>'),
'rclone': ('<span class="bdg" style="background:#0c1f17;border:1px solid #14532d;color:#4ade80;">同步</span>'),
'person': ('<span class="bdg" style="background:#1f122e;border:1px solid #4a1d96;color:#c084fc;">人物</span>'),
}
items = []
for a in acts[:50]:
badge = _svc_badge.get(a.get('service'), f"<span class='bdg'>{esc(a.get('service'))}</span>")
ts = str(a.get('ts') or '')[:19]
items.append(
f'<div class="ev-item" style="margin-bottom:5px;">'
f'<div class="ev-time" style="min-width:110px;">{esc(ts)}</div>'
f'<div class="ev-card" style="padding:6px 10px;">{badge}'
f'<span style="font-weight:600;color:#e2e8f0;margin:0 6px;">{esc(a.get("action"))}</span>'
f'<span style="font-size:12px;color:#94a3b8;">{esc(a.get("detail") or "")}</span>'
f'</div></div>')
st.markdown(
f'<div class="ev-list">{"".join(items)}</div>', unsafe_allow_html=True)
st.caption(f'{len(acts)} 条记录(活动日志仅保留最近 7 天)')