From c08f76f315e21997c556a1e93cb57c5c76316e94 Mon Sep 17 00:00:00 2001 From: OoO Date: Tue, 19 May 2026 00:49:06 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A3=9C=20PPT=20AiderHeal=20=E5=9F=B7?= =?UTF-8?q?=E8=A1=8C=E7=8B=80=E6=85=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TODO_NEXT_STEPS.txt | 1 + config.py | 2 +- routes/admin_observability_routes.py | 38 ++++++++++- templates/admin/ppt_audit_history.html | 19 ++++++ tests/test_admin_observability_routes.py | 64 ++++++++++++++++++ web/static/css/page-ppt-audit-history.css | 80 +++++++++++++++++++++++ web/static/js/observability-charts.js | 42 ++++++++++++ 7 files changed, 242 insertions(+), 4 deletions(-) diff --git a/TODO_NEXT_STEPS.txt b/TODO_NEXT_STEPS.txt index 712870c..e533cd3 100644 --- a/TODO_NEXT_STEPS.txt +++ b/TODO_NEXT_STEPS.txt @@ -4,6 +4,7 @@ ================================================================================ 【已完成】 + - V10.221 補 `/observability/ppt_audit_history` AiderHeal 背景任務可見性:正在修復中的簡報會顯示於產線頁,並提供 JSON 狀態端點讓派工後即時刷新,避免重新整理後不知道是否已在修。 - V10.218 補 `/observability/ppt_audit_history` AiderHeal 去重鎖:同一份簡報已在背景修復時,再次點擊會回「已在執行中」,避免重複開 SSH / 模型 / git 修復流程。 - V10.217 讓 `/observability/ppt_audit_history` 的 AiderHeal 派工改為非阻塞背景任務:頁面立即回「已排入」,修復工作在背景執行,避免瀏覽器與 Gunicorn worker 等 SSH、模型與 git push 到超時。 - V10.216 修正 `/observability/ppt_audit_history` 的 AiderHeal 派工斷點:失敗簡報即使只有 `issues_found` 診斷摘要也能一鍵送修,並修正 `execute_code_fix` 參數與 dict 回傳解析,避免按鈕 400/500。 diff --git a/config.py b/config.py index 5697f01..ee7c6a0 100644 --- a/config.py +++ b/config.py @@ -320,7 +320,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.220" +SYSTEM_VERSION = "V10.221" LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log') public_url = PUBLIC_URL # 用於模板顯示 diff --git a/routes/admin_observability_routes.py b/routes/admin_observability_routes.py index 4e52f82..5e8e5ed 100644 --- a/routes/admin_observability_routes.py +++ b/routes/admin_observability_routes.py @@ -36,7 +36,12 @@ admin_observability_bp = Blueprint( ) _PPT_AIDER_HEAL_LOCK = threading.Lock() -_PPT_AIDER_HEAL_ACTIVE = set() +_PPT_AIDER_HEAL_ACTIVE = {} + + +def _list_ppt_aider_heal_active_jobs(): + with _PPT_AIDER_HEAL_LOCK: + return [dict(job) for job in _PPT_AIDER_HEAL_ACTIVE.values()] # ───────────────────────────────────────────────────────────────────────────── @@ -1839,16 +1844,27 @@ def ppt_audit_trigger_aider_heal(): 'issue_summary': issue_summary[:500], } heal_key = pptx_filename or diagnosis[:160] or 'manual' + queued_at = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + active_job = { + 'key': heal_key, + 'pptx_filename': pptx_filename, + 'target_file': 'services/ppt_generator.py', + 'queued_at': queued_at, + 'diagnosis': diagnosis[:160], + } with _PPT_AIDER_HEAL_LOCK: if heal_key in _PPT_AIDER_HEAL_ACTIVE: + existing_job = dict(_PPT_AIDER_HEAL_ACTIVE.get(heal_key) or active_job) return jsonify({ 'ok': True, 'status': 'already_running', 'action': 'CODE_FIX', 'message': '這份簡報的 AiderHeal 已在背景執行中,請等 Telegram/Gitea/CD 結果回報。', 'target_file': 'services/ppt_generator.py', + 'active_count': len(_PPT_AIDER_HEAL_ACTIVE), + 'job': existing_job, }), 202 - _PPT_AIDER_HEAL_ACTIVE.add(heal_key) + _PPT_AIDER_HEAL_ACTIVE[heal_key] = active_job def _heal_worker(): try: @@ -1871,7 +1887,7 @@ def ppt_audit_trigger_aider_heal(): ) finally: with _PPT_AIDER_HEAL_LOCK: - _PPT_AIDER_HEAL_ACTIVE.discard(heal_key) + _PPT_AIDER_HEAL_ACTIVE.pop(heal_key, None) thread_key = ''.join(ch for ch in pptx_filename if ch.isalnum())[:24] or 'manual' threading.Thread( @@ -1886,11 +1902,24 @@ def ppt_audit_trigger_aider_heal(): 'action': 'CODE_FIX', 'message': 'AiderHeal 已排入背景執行;完成後會由 Telegram/Gitea/CD 結果回報。', 'target_file': 'services/ppt_generator.py', + 'active_count': len(_list_ppt_aider_heal_active_jobs()), + 'job': active_job, }), 202 except Exception as e: return jsonify({'ok': False, 'error': f'{type(e).__name__}: {str(e)[:200]}'}), 500 +@admin_observability_bp.route('/ppt_audit/aider_heal_status') +@login_required +def ppt_audit_aider_heal_status(): + jobs = _list_ppt_aider_heal_active_jobs() + return jsonify({ + 'ok': True, + 'active_count': len(jobs), + 'jobs': jobs, + }) + + @admin_observability_bp.route('/ppt_audit/generate_missing', methods=['POST']) @login_required def ppt_audit_generate_missing(): @@ -3134,6 +3163,7 @@ def ppt_audit_history(): for item in files if item.get('file_exists') and item.get('is_valid_ppt') and item.get('name') ][:10] + aider_heal_active_jobs = _list_ppt_aider_heal_active_jobs() return render_template( 'admin/ppt_audit_history.html', @@ -3160,6 +3190,8 @@ def ppt_audit_history(): vision_audit_filenames=vision_audit_filenames, issue_items=issue_items, issue_digest=issue_digest, + aider_heal_active_jobs=aider_heal_active_jobs, + aider_heal_active_count=len(aider_heal_active_jobs), error=error, ) diff --git a/templates/admin/ppt_audit_history.html b/templates/admin/ppt_audit_history.html index 96e28ec..a125e5a 100644 --- a/templates/admin/ppt_audit_history.html +++ b/templates/admin/ppt_audit_history.html @@ -66,6 +66,25 @@ {% endif %} {% if error %}
{{ error }}
{% endif %} +
+
+ AiderHeal +
+ {% if aider_heal_active_count %}AiderHeal 執行中 · {{ aider_heal_active_count }}{% else %}AiderHeal 待命{% endif %} + {% if aider_heal_active_count %}修復完成後會由 Telegram/Gitea/CD 回報。{% else %}有問題的審核紀錄可直接一鍵派工。{% endif %} +
+
+
+ {% for job in aider_heal_active_jobs[:3] %} +
+ {{ job.pptx_filename or 'manual' }} + {{ job.queued_at }} + {{ job.diagnosis }} +
+ {% endfor %} +
+
+
diff --git a/tests/test_admin_observability_routes.py b/tests/test_admin_observability_routes.py index 3b9a29d..16c417d 100644 --- a/tests/test_admin_observability_routes.py +++ b/tests/test_admin_observability_routes.py @@ -524,6 +524,7 @@ def test_ppt_audit_trigger_aider_heal_accepts_issue_summary(client, monkeypatch) from services import aider_heal_executor as svc captured = {} + mod._PPT_AIDER_HEAL_ACTIVE.clear() def fake_execute_code_fix(**kwargs): captured.update(kwargs) @@ -563,6 +564,7 @@ def test_ppt_audit_trigger_aider_heal_accepts_issue_summary(client, monkeypatch) assert 'ocbot_daily_20260517.pptx' in captured['error_message'] assert '圖表被切掉' in captured['error_message'] assert captured['context']['issue_summary'] == 'S1: 圖表被切掉:右側圖例超出邊界' + assert mod._PPT_AIDER_HEAL_ACTIVE == {} def test_ppt_audit_trigger_aider_heal_dedupes_same_file(client, monkeypatch): @@ -610,6 +612,68 @@ def test_ppt_audit_trigger_aider_heal_dedupes_same_file(client, monkeypatch): mod._PPT_AIDER_HEAL_ACTIVE.clear() +def test_ppt_audit_aider_heal_status_reports_active_jobs(client, monkeypatch): + """背景 AiderHeal 已派工時,狀態端點要能讓頁面重新整理後看見執行中。""" + from routes import admin_observability_routes as mod + from services import aider_heal_executor as svc + + def fake_execute_code_fix(**_kwargs): + return {'success': True, 'message': '不應在此測試執行'} + + class HoldingThread: + def __init__(self, target, **_kwargs): + self.target = target + + def start(self): + return None + + monkeypatch.setattr(svc, 'execute_code_fix', fake_execute_code_fix) + monkeypatch.setattr(mod.threading, 'Thread', HoldingThread) + mod._PPT_AIDER_HEAL_ACTIVE.clear() + + payload = { + 'pptx_filename': 'ocbot_daily_20260517.pptx', + 'issue_summary': 'S1: 圖表被切掉:右側圖例超出邊界', + } + first = client.post('/observability/ppt_audit/trigger_aider_heal', json=payload) + status = client.get('/observability/ppt_audit/aider_heal_status') + + try: + assert first.status_code == 202 + data = status.get_json() + assert status.status_code == 200 + assert data['ok'] is True + assert data['active_count'] == 1 + assert data['jobs'][0]['pptx_filename'] == 'ocbot_daily_20260517.pptx' + assert data['jobs'][0]['target_file'] == 'services/ppt_generator.py' + assert '圖表被切掉' in data['jobs'][0]['diagnosis'] + finally: + mod._PPT_AIDER_HEAL_ACTIVE.clear() + + +def test_ppt_audit_history_shows_active_aider_heal_jobs(client): + """PPT 產線頁要直接顯示正在背景修復的檔案。""" + from routes import admin_observability_routes as mod + + mod._PPT_AIDER_HEAL_ACTIVE.clear() + mod._PPT_AIDER_HEAL_ACTIVE['ocbot_daily_20260517.pptx'] = { + 'key': 'ocbot_daily_20260517.pptx', + 'pptx_filename': 'ocbot_daily_20260517.pptx', + 'target_file': 'services/ppt_generator.py', + 'queued_at': '2026-05-18 14:42:00', + 'diagnosis': 'S1: 圖表被切掉:右側圖例超出邊界', + } + try: + r = client.get('/observability/ppt_audit_history') + html = r.get_data(as_text=True) + assert r.status_code == 200 + assert 'AiderHeal 執行中' in html + assert 'ocbot_daily_20260517.pptx' in html + assert '圖表被切掉' in html + finally: + mod._PPT_AIDER_HEAL_ACTIVE.clear() + + # ────────────────────────────────────────────────────────────────────────── # /observability/host_health # ────────────────────────────────────────────────────────────────────────── diff --git a/web/static/css/page-ppt-audit-history.css b/web/static/css/page-ppt-audit-history.css index ffb395d..af553d0 100644 --- a/web/static/css/page-ppt-audit-history.css +++ b/web/static/css/page-ppt-audit-history.css @@ -73,6 +73,81 @@ font-weight: var(--momo-font-weight-bold, 700); } +.ppt-aider-status { + display: grid; + grid-template-columns: minmax(260px, 0.36fr) minmax(0, 0.64fr); + gap: var(--momo-space-3, 12px); + align-items: stretch; + margin-top: var(--momo-space-3, 12px); + padding: var(--momo-space-3, 12px); + border: 1px solid rgba(184, 121, 47, 0.32); + border-radius: var(--momo-radius-lg, 8px); + background: + radial-gradient(circle, rgba(184, 121, 47, 0.1) 1px, transparent 1.2px), + rgba(255, 248, 239, 0.72); + background-size: 10px 10px, auto; +} + +.ppt-aider-status.is-empty { + display: none; +} + +.ppt-aider-status-main { + display: flex; + align-items: center; + gap: var(--momo-space-3, 12px); +} + +.ppt-aider-status-main strong, +.ppt-aider-status-main small { + display: block; +} + +.ppt-aider-status-main strong { + color: var(--obs-ink); + font-size: var(--momo-text-body, 14px); + font-weight: var(--momo-font-weight-black, 800); +} + +.ppt-aider-status-main small { + color: var(--obs-muted); +} + +.ppt-aider-job-list { + display: grid; + gap: var(--momo-space-2, 8px); +} + +.ppt-aider-job { + display: grid; + grid-template-columns: minmax(180px, 0.42fr) minmax(130px, 0.22fr) minmax(0, 0.36fr); + gap: var(--momo-space-2, 8px); + align-items: center; + min-height: 42px; + padding: var(--momo-space-2, 8px); + border: 1px solid rgba(86, 64, 48, 0.12); + border-radius: var(--momo-radius-md, 6px); + background: rgba(255, 255, 255, 0.54); +} + +.ppt-aider-job code, +.ppt-aider-job span, +.ppt-aider-job small { + min-width: 0; + overflow-wrap: anywhere; +} + +.ppt-aider-job code { + color: var(--obs-ink); + font-family: var(--momo-font-mono); +} + +.ppt-aider-job span, +.ppt-aider-job small { + color: var(--obs-muted); + font-size: var(--momo-text-caption, 12px); +} + .ppt-command { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); @@ -952,6 +1027,7 @@ body.ppt-preview-open { } .ppt-diagnostic-strip, + .ppt-aider-status, .ppt-health-board, .ppt-pipeline-layout { grid-template-columns: 1fr; @@ -1027,4 +1103,8 @@ body.ppt-preview-open { .ppt-preview-frame-wrap iframe { min-height: calc(100vh - 168px); } + + .ppt-aider-job { + grid-template-columns: 1fr; + } } diff --git a/web/static/js/observability-charts.js b/web/static/js/observability-charts.js index ba16cdb..e54dba2 100644 --- a/web/static/js/observability-charts.js +++ b/web/static/js/observability-charts.js @@ -604,6 +604,9 @@ statusNode.classList.remove('is-working'); statusNode.textContent = data.message || 'AiderHeal 已排入背景執行,完成後會由 Telegram/Gitea/CD 結果回報。'; } + if (window.refreshPptAiderHealStatus) { + window.refreshPptAiderHealStatus(); + } } catch (error) { console.warn('ppt_audit_trigger_aider_heal_failed', error); if (triggerButton) { @@ -617,6 +620,45 @@ } }; + function renderPptAiderHealStatus(payload) { + const panel = document.querySelector('[data-ppt-aider-status]'); + if (!panel) return; + const jobs = Array.isArray(payload && payload.jobs) ? payload.jobs : []; + const count = Number((payload && payload.active_count) || jobs.length || 0); + panel.classList.toggle('is-empty', count <= 0); + const title = panel.querySelector('[data-ppt-aider-status-title]'); + const meta = panel.querySelector('[data-ppt-aider-status-meta]'); + const list = panel.querySelector('[data-ppt-aider-job-list]'); + if (title) title.textContent = count > 0 ? `AiderHeal 執行中 · ${count}` : 'AiderHeal 待命'; + if (meta) { + meta.textContent = count > 0 + ? '修復完成後會由 Telegram/Gitea/CD 回報。' + : '有問題的審核紀錄可直接一鍵派工。'; + } + if (list) { + list.innerHTML = jobs.slice(0, 3).map(job => ` +
+ ${escapeHtml(job.pptx_filename || 'manual')} + ${escapeHtml(job.queued_at || '')} + ${escapeHtml(job.diagnosis || '')} +
+ `).join(''); + } + } + + window.refreshPptAiderHealStatus = async function refreshPptAiderHealStatus() { + try { + const response = await fetch('/observability/ppt_audit/aider_heal_status', { + headers: { 'Accept': 'application/json' } + }); + const data = await response.json(); + if (!response.ok || !data.ok) return; + renderPptAiderHealStatus(data); + } catch (error) { + console.warn('ppt_audit_aider_heal_status_failed', error); + } + }; + function initPptAutoGeneration() { const panel = document.querySelector('[data-ppt-auto-generation]'); const pageStatus = document.querySelector('[data-ppt-auto-status]');