diff --git a/TODO_NEXT_STEPS.txt b/TODO_NEXT_STEPS.txt index e61beb7..763de9b 100644 --- a/TODO_NEXT_STEPS.txt +++ b/TODO_NEXT_STEPS.txt @@ -4,6 +4,7 @@ ================================================================================ 【已完成】 + - 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。 - V10.215 強化 `/observability/ppt_audit_history` 視覺問題追蹤:將 `issues_found` 拆成投影片、問題類型、問題文字與回放入口,新增「視覺問題追蹤」面板,讓問題簡報能直接定位與預覽。 - V10.213 補 `/observability/ppt_audit_history` 視覺 QA 診斷摘要:審核歷史與 Action Queue 直接顯示 `ppt_audit_results.issues_found` 的投影片問題,讓「有問題」可追查,不再只剩問題數或空白錯誤欄。 diff --git a/config.py b/config.py index 63b1310..48b7a00 100644 --- a/config.py +++ b/config.py @@ -320,7 +320,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.216" +SYSTEM_VERSION = "V10.217" 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 ed58bbd..a212854 100644 --- a/routes/admin_observability_routes.py +++ b/routes/admin_observability_routes.py @@ -18,6 +18,7 @@ Operation Ollama-First v5.0 / Phase 27 — Admin Observability Dashboard """ import logging +import threading from datetime import datetime, timedelta from flask import Blueprint, render_template, request, jsonify, send_file, url_for from sqlalchemy import text as sa_text @@ -1811,7 +1812,8 @@ def ppt_audit_trigger_aider_heal(): 結果會 git push 到 main 觸發 CD 自動部署。 """ try: - from services.aider_heal_executor import execute_code_fix + from services import aider_heal_executor + data = request.get_json(silent=True) or {} error_msg = (data.get('error_msg') or '').strip() issue_summary = (data.get('issue_summary') or '').strip() @@ -1833,19 +1835,41 @@ def ppt_audit_trigger_aider_heal(): 'triggered_by': 'admin_observability', 'issue_summary': issue_summary[:500], } - result = execute_code_fix( - error_type='ppt_vision_audit_failure', - error_message=error_message, - target_file='services/ppt_generator.py', - context=context, - ) + + def _heal_worker(): + try: + result = aider_heal_executor.execute_code_fix( + error_type='ppt_vision_audit_failure', + error_message=error_message, + target_file='services/ppt_generator.py', + context=context, + ) + logger.info( + "[PPTAudit] AiderHeal 背景任務完成 | file=%s | ok=%s | message=%s", + pptx_filename or '-', + bool(result.get('success')), + (result.get('message') or '')[:160], + ) + except Exception: + logger.exception( + "[PPTAudit] AiderHeal 背景任務失敗 | file=%s", + pptx_filename or '-', + ) + + thread_key = ''.join(ch for ch in pptx_filename if ch.isalnum())[:24] or 'manual' + threading.Thread( + target=_heal_worker, + daemon=True, + name=f"ppt-aider-heal-{thread_key}", + ).start() + return jsonify({ - 'ok': bool(result.get('success')), - 'action': result.get('action'), - 'message': result.get('message') or '已派出 AiderHeal', - 'commit_sha': result.get('commit_sha'), - 'reverted': bool(result.get('reverted')), - }) + 'ok': True, + 'status': 'queued', + 'action': 'CODE_FIX', + 'message': 'AiderHeal 已排入背景執行;完成後會由 Telegram/Gitea/CD 結果回報。', + 'target_file': 'services/ppt_generator.py', + }), 202 except Exception as e: return jsonify({'ok': False, 'error': f'{type(e).__name__}: {str(e)[:200]}'}), 500 diff --git a/tests/test_admin_observability_routes.py b/tests/test_admin_observability_routes.py index 572e941..9de65f4 100644 --- a/tests/test_admin_observability_routes.py +++ b/tests/test_admin_observability_routes.py @@ -520,6 +520,7 @@ def test_ppt_audit_run_vision_queues_background_audit(client, monkeypatch): def test_ppt_audit_trigger_aider_heal_accepts_issue_summary(client, monkeypatch): """視覺 QA failed 常只有 issues_found;AiderHeal 應可吃診斷摘要派工。""" + from routes import admin_observability_routes as mod from services import aider_heal_executor as svc captured = {} @@ -536,6 +537,15 @@ def test_ppt_audit_trigger_aider_heal_accepts_issue_summary(client, monkeypatch) monkeypatch.setattr(svc, 'execute_code_fix', fake_execute_code_fix) + class ImmediateThread: + def __init__(self, target, **_kwargs): + self.target = target + + def start(self): + self.target() + + monkeypatch.setattr(mod.threading, 'Thread', ImmediateThread) + r = client.post( '/observability/ppt_audit/trigger_aider_heal', json={ @@ -545,8 +555,9 @@ def test_ppt_audit_trigger_aider_heal_accepts_issue_summary(client, monkeypatch) ) data = r.get_json() - assert r.status_code == 200 + assert r.status_code == 202 assert data['ok'] is True + assert data['status'] == 'queued' assert captured['error_type'] == 'ppt_vision_audit_failure' assert captured['target_file'] == 'services/ppt_generator.py' assert 'ocbot_daily_20260517.pptx' in captured['error_message'] diff --git a/web/static/js/observability-charts.js b/web/static/js/observability-charts.js index 5a3d370..6ad8b9b 100644 --- a/web/static/js/observability-charts.js +++ b/web/static/js/observability-charts.js @@ -573,23 +573,45 @@ } }; - window.triggerAiderHeal = async function triggerAiderHeal(pptxFilename, errorMsg) { + window.triggerAiderHeal = async function triggerAiderHeal(pptxFilename, errorMsg, triggerButton) { if (!confirm(`觸發 AiderHeal 自動修復?\n\n檔案:${pptxFilename}\n錯誤:${(errorMsg || '').substring(0, 200)}`)) return; + const statusNode = document.querySelector('[data-ppt-auto-status]'); + const originalHtml = triggerButton ? triggerButton.innerHTML : ''; + if (triggerButton) { + triggerButton.disabled = true; + triggerButton.innerHTML = '派工中'; + } + if (statusNode) { + statusNode.classList.add('is-working'); + statusNode.textContent = `${pptxFilename || 'PPT'} 正在排入 AiderHeal 背景修復。`; + } try { const response = await postJson('/observability/ppt_audit/trigger_aider_heal', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ pptx_filename: pptxFilename, error_msg: errorMsg || '' }) + body: JSON.stringify({ pptx_filename: pptxFilename, issue_summary: errorMsg || '' }) }); const data = await response.json(); - if (data.ok) { - alert(`✅ AiderHeal 已派出\n動作:${data.action || '—'}\n訊息:${data.message || ''}`); - } else { - alert(`❌ ${data.error || data.message || '觸發失敗'}`); + if (!response.ok || !data.ok) { + throw new Error(data.error || data.message || '觸發失敗'); + } + if (triggerButton) { + triggerButton.innerHTML = '已排入'; + } + if (statusNode) { + statusNode.classList.remove('is-working'); + statusNode.textContent = data.message || 'AiderHeal 已排入背景執行,完成後會由 Telegram/Gitea/CD 結果回報。'; } } catch (error) { console.warn('ppt_audit_trigger_aider_heal_failed', error); - alert('操作暫時無法完成,請稍後再試或查看系統日誌。'); + if (triggerButton) { + triggerButton.disabled = false; + triggerButton.innerHTML = originalHtml; + } + if (statusNode) { + statusNode.classList.remove('is-working'); + statusNode.textContent = 'AiderHeal 派工失敗,請稍後再試或查看系統日誌。'; + } } }; @@ -666,7 +688,7 @@ if (button.dataset.bound === '1') return; button.dataset.bound = '1'; button.addEventListener('click', () => { - window.triggerAiderHeal(button.dataset.pptFilename || '', button.dataset.pptError || ''); + window.triggerAiderHeal(button.dataset.pptFilename || '', button.dataset.pptError || '', button); }); });