From 69ccf8029b5cdf4f6772e8c90837686911c7b7f0 Mon Sep 17 00:00:00 2001 From: OoO Date: Mon, 4 May 2026 13:44:08 +0800 Subject: [PATCH] =?UTF-8?q?feat(p29):=20=E9=A0=90=E7=AE=97=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E9=A0=81=20+=20PPT=20vision=20=E6=AD=B7=E5=8F=B2?= =?UTF-8?q?=E9=A0=81=20=E2=80=94=20=E5=AE=8C=E6=88=90=206=20=E5=80=8B=20ad?= =?UTF-8?q?min=20=E8=A7=80=E6=B8=AC=E9=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 承接 Phase 27/28(48b8fda)剩 2 個前端頁: 1. /admin/budget — 預算編輯器 - GET: ai_call_budgets × 當月 spent 即時對比 + throttle 狀態 - POST /admin/budget/update/: AJAX 編輯 budget_usd / alert_pct - 不需 restart 立即生效(cost_throttle hourly cron 自動讀新值) - ratio ≥80% 黃 / ≥110% 紅 / throttled 標 ⚠️ THROTTLED 2. /admin/ppt_audit_history — PPT 視覺審核歷史 - 掃 reports/ 過去 7 日 .pptx 檔(檔名/大小/修改時間) - 顯示 PPT_VISION_ENABLED 狀態(true=daily 22:00 cron 自動跑) - 手動觸發 SOP 提示(SSH 188 跑單檔審核) 完工里程碑:6 個 admin 頁 + 1 個導覽 - /admin/ai_calls (Phase 27) - /admin/promotion_review (Phase 27) - /admin/quality_trend (Phase 28) - /admin/host_health (Phase 28) - /admin/budget (Phase 29) ← 新增 - /admin/ppt_audit_history (Phase 29) ← 新增 Operation Ollama-First v5.0 — 前端互補互動系列收官 --- routes/admin_observability_routes.py | 140 +++++++++++++++++++++++++ templates/admin/budget.html | 110 +++++++++++++++++++ templates/admin/ppt_audit_history.html | 58 ++++++++++ 3 files changed, 308 insertions(+) create mode 100644 templates/admin/budget.html create mode 100644 templates/admin/ppt_audit_history.html diff --git a/routes/admin_observability_routes.py b/routes/admin_observability_routes.py index 0ec70f8..97a2bbc 100644 --- a/routes/admin_observability_routes.py +++ b/routes/admin_observability_routes.py @@ -261,6 +261,146 @@ def quality_trend_dashboard(): ) +# ───────────────────────────────────────────────────────────────────────────── +# /admin/budget — Phase 29 預算管理 + 手動 throttle +# ───────────────────────────────────────────────────────────────────────────── + +@admin_observability_bp.route('/budget') +def budget_dashboard(): + """ai_call_budgets 編輯 + 當月 spent 即時對比""" + from datetime import datetime as _dt + today = _dt.now() + month_start = _dt(today.year, today.month, 1) + + session = get_session() + try: + budgets = session.execute( + sa_text(""" + SELECT id, period, provider, budget_usd, alert_pct, updated_at + FROM ai_call_budgets + ORDER BY period, provider NULLS FIRST + """), + ).fetchall() + + spent_rows = session.execute( + sa_text(""" + SELECT provider, COALESCE(SUM(cost_usd), 0) AS spent + FROM ai_calls + WHERE called_at >= :ms + GROUP BY provider + """), + {'ms': month_start}, + ).fetchall() + spent_map = {r[0]: float(r[1] or 0) for r in spent_rows} + + # throttle 狀態 + throttle_state = {} + try: + from services.cost_throttle_service import get_throttle_state + throttle_state = get_throttle_state() + except Exception: + pass + + rows = [] + for b in budgets: + provider = b[2] # 可能 None(全供應商總額) + spent = spent_map.get(provider, 0.0) if provider else sum(spent_map.values()) + budget_usd = float(b[3] or 0) + ratio = (spent / budget_usd) if budget_usd > 0 else 0 + rows.append({ + 'id': b[0], 'period': b[1], 'provider': provider or '(all)', + 'budget_usd': budget_usd, 'alert_pct': int(b[4] or 80), + 'spent': spent, 'ratio': ratio, + 'throttled': throttle_state.get(provider, {}).get('throttled', False) if provider else False, + 'updated_at': b[5].strftime('%Y-%m-%d %H:%M') if b[5] else '-', + }) + + return render_template('admin/budget.html', rows=rows, error=None) + except Exception as e: + return render_template('admin/budget.html', rows=[], + error=f'查詢失敗: {type(e).__name__}: {str(e)[:200]}') + finally: + session.close() + + +@admin_observability_bp.route('/budget/update/', methods=['POST']) +def budget_update(budget_id: int): + """更新 budget_usd / alert_pct""" + try: + new_budget = float(request.json.get('budget_usd')) + new_alert = int(request.json.get('alert_pct', 80)) + if new_budget <= 0 or not (1 <= new_alert <= 100): + return jsonify({'ok': False, 'error': 'invalid range'}), 400 + + session = get_session() + try: + session.execute( + sa_text(""" + UPDATE ai_call_budgets + SET budget_usd = :b, alert_pct = :a, updated_at = NOW() + WHERE id = :id + """), + {'b': new_budget, 'a': new_alert, 'id': budget_id}, + ) + session.commit() + return jsonify({'ok': True}) + finally: + session.close() + except Exception as e: + return jsonify({'ok': False, 'error': str(e)[:200]}), 500 + + +# ───────────────────────────────────────────────────────────────────────────── +# /admin/ppt_audit_history — Phase 29 PPT 視覺審核歷史 +# ───────────────────────────────────────────────────────────────────────────── + +@admin_observability_bp.route('/ppt_audit_history') +def ppt_audit_history(): + """掃 reports/ 目錄列近 7 日 .pptx 檔 + 即時跑 audit(如已啟用)""" + import os + reports_dir = 'reports' + files = [] + error = None + + try: + if not os.path.isdir(reports_dir): + error = f'{reports_dir} 目錄不存在' + else: + cutoff = __import__('time').time() - 7 * 86400 + for f in os.listdir(reports_dir): + if not f.lower().endswith('.pptx'): + continue + full = os.path.join(reports_dir, f) + try: + mtime = os.path.getmtime(full) + if mtime >= cutoff: + files.append({ + 'name': f, + 'size_kb': round(os.path.getsize(full) / 1024, 1), + 'mtime': __import__('datetime').datetime.fromtimestamp(mtime).strftime('%Y-%m-%d %H:%M'), + 'mtime_ts': mtime, + }) + except OSError: + continue + files.sort(key=lambda x: x['mtime_ts'], reverse=True) + except Exception as e: + error = f'{type(e).__name__}: {str(e)[:200]}' + + # PPT vision 啟用狀態 + try: + from services.ppt_vision_service import is_ppt_vision_enabled + vision_enabled = is_ppt_vision_enabled() + except Exception: + vision_enabled = False + + return render_template( + 'admin/ppt_audit_history.html', + files=files, + vision_enabled=vision_enabled, + error=error, + ) + + # ───────────────────────────────────────────────────────────────────────────── # /admin/host_health — 三主機 + MCP 健康度 # ───────────────────────────────────────────────────────────────────────────── diff --git a/templates/admin/budget.html b/templates/admin/budget.html new file mode 100644 index 0000000..26590fa --- /dev/null +++ b/templates/admin/budget.html @@ -0,0 +1,110 @@ +{% extends "base.html" %} + +{% block title %}Budget Manager{% endblock %} + +{% block content %} +
+

💰 Budget Manager + ai_call_budgets × 當月 spent 即時對比 +

+ + {% if error %}
⚠️ {{ error }}
{% endif %} + +

+ 依 ADR-028 預算 + Phase 20 cost_throttle:每小時 cron 檢查當月 spent, + 線性外推月底成本超 110% → 自動 throttle(claude→gemini fallback)。 + 手動編輯 budget 後立即生效(不需 restart)。 +

+ + + + + + + + + + + + + {% for r in rows %} + = 0.8 %}class="table-warning"{% endif %}> + + + + + + + + + + + {% else %} + + {% endfor %} + +
PeriodProviderSpent (USD)Budget (USD)Alert %Ratio狀態Last Update動作
{{ r.period }}{{ r.provider }}${{ "%.2f"|format(r.spent) }} + + + + + + {{ "%.0f"|format(r.ratio * 100) }}% + + + {% if r.throttled %} + ⚠️ THROTTLED + {% elif r.ratio >= 0.8 %} + ⚠ 接近上限 + {% else %} + ✅ 正常 + {% endif %} + {{ r.updated_at }} + +
無預算資料(先跑 migrations/025)
+ +

+ 🤖 Operation Ollama-First v5.0 / Phase 29 — Budget Manager + | AI Calls + | Host Health + | Promotion Review +

+
+ + +{% endblock %} diff --git a/templates/admin/ppt_audit_history.html b/templates/admin/ppt_audit_history.html new file mode 100644 index 0000000..af0ffaa --- /dev/null +++ b/templates/admin/ppt_audit_history.html @@ -0,0 +1,58 @@ +{% extends "base.html" %} + +{% block title %}PPT Audit History{% endblock %} + +{% block content %} +
+

🔍 PPT 視覺審核歷史 + reports/ 過去 7 日 .pptx +

+ + {% if error %}
⚠️ {{ error }}
{% endif %} + +
+ PPT_VISION_ENABLED: + {% if vision_enabled %} + ✅ 已啟用 — daily 22:00 cron 自動跑 minicpm-v 視覺檢查,有 issues 推 Telegram + {% else %} + ⏸ 未啟用 — 設 PPT_VISION_ENABLED=true + 188 安裝 LibreOffice 即生效 + {% endif %} +
+ + + + + + + + + + {% for f in files %} + + + + + + + {% else %} + + {% endfor %} + +
檔名大小 (KB)修改時間動作
{{ f.name }}{{ f.size_kb }}{{ f.mtime }} + audit cron 22:00 自動跑 +
過去 7 日無 PPT 生成
+ +

+ 審核結果:有 issues 才推 Telegram(避免靜默無問題洗版)。 + 手動觸發單檔審核需 SSH 188 跑: + python3 -c "from services.ppt_vision_service import ppt_vision_service; print(ppt_vision_service.check_ppt_file('reports/xxx.pptx'))" +

+ +

+ 🤖 Operation Ollama-First v5.0 / Phase 29 — PPT Audit History + | AI Calls + | Host Health + | Budget +

+
+{% endblock %}