diff --git a/TODO_NEXT_STEPS.txt b/TODO_NEXT_STEPS.txt index 763de9b..00a63d5 100644 --- a/TODO_NEXT_STEPS.txt +++ b/TODO_NEXT_STEPS.txt @@ -4,6 +4,7 @@ ================================================================================ 【已完成】 + - 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。 - V10.215 強化 `/observability/ppt_audit_history` 視覺問題追蹤:將 `issues_found` 拆成投影片、問題類型、問題文字與回放入口,新增「視覺問題追蹤」面板,讓問題簡報能直接定位與預覽。 diff --git a/config.py b/config.py index 48b7a00..2f2c4b8 100644 --- a/config.py +++ b/config.py @@ -320,7 +320,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.217" +SYSTEM_VERSION = "V10.218" 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 a212854..4e52f82 100644 --- a/routes/admin_observability_routes.py +++ b/routes/admin_observability_routes.py @@ -35,6 +35,9 @@ admin_observability_bp = Blueprint( url_prefix='/observability', ) +_PPT_AIDER_HEAL_LOCK = threading.Lock() +_PPT_AIDER_HEAL_ACTIVE = set() + # ───────────────────────────────────────────────────────────────────────────── # /observability/overview — Phase 45 總覽(單頁聚合 6 項 KPI) @@ -1835,6 +1838,17 @@ def ppt_audit_trigger_aider_heal(): 'triggered_by': 'admin_observability', 'issue_summary': issue_summary[:500], } + heal_key = pptx_filename or diagnosis[:160] or 'manual' + with _PPT_AIDER_HEAL_LOCK: + if heal_key in _PPT_AIDER_HEAL_ACTIVE: + return jsonify({ + 'ok': True, + 'status': 'already_running', + 'action': 'CODE_FIX', + 'message': '這份簡報的 AiderHeal 已在背景執行中,請等 Telegram/Gitea/CD 結果回報。', + 'target_file': 'services/ppt_generator.py', + }), 202 + _PPT_AIDER_HEAL_ACTIVE.add(heal_key) def _heal_worker(): try: @@ -1855,6 +1869,9 @@ def ppt_audit_trigger_aider_heal(): "[PPTAudit] AiderHeal 背景任務失敗 | file=%s", pptx_filename or '-', ) + finally: + with _PPT_AIDER_HEAL_LOCK: + _PPT_AIDER_HEAL_ACTIVE.discard(heal_key) thread_key = ''.join(ch for ch in pptx_filename if ch.isalnum())[:24] or 'manual' threading.Thread( diff --git a/tests/test_admin_observability_routes.py b/tests/test_admin_observability_routes.py index 9de65f4..3b9a29d 100644 --- a/tests/test_admin_observability_routes.py +++ b/tests/test_admin_observability_routes.py @@ -565,6 +565,51 @@ def test_ppt_audit_trigger_aider_heal_accepts_issue_summary(client, monkeypatch) assert captured['context']['issue_summary'] == 'S1: 圖表被切掉:右側圖例超出邊界' +def test_ppt_audit_trigger_aider_heal_dedupes_same_file(client, monkeypatch): + """同一份 PPT 已在背景修復時,重複按鈕不應重開第二條 AiderHeal。""" + from routes import admin_observability_routes as mod + from services import aider_heal_executor as svc + + calls = [] + + def fake_execute_code_fix(**kwargs): + calls.append(kwargs) + return { + 'success': True, + 'action': 'CODE_FIX', + 'message': '不應在此測試執行', + 'commit_sha': None, + 'reverted': False, + } + + 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) + second = client.post('/observability/ppt_audit/trigger_aider_heal', json=payload) + + try: + assert first.status_code == 202 + assert first.get_json()['status'] == 'queued' + assert second.status_code == 202 + assert second.get_json()['status'] == 'already_running' + assert calls == [] + finally: + mod._PPT_AIDER_HEAL_ACTIVE.clear() + + # ────────────────────────────────────────────────────────────────────────── # /observability/host_health # ────────────────────────────────────────────────────────────────────────── diff --git a/web/static/js/observability-charts.js b/web/static/js/observability-charts.js index 6ad8b9b..ba16cdb 100644 --- a/web/static/js/observability-charts.js +++ b/web/static/js/observability-charts.js @@ -596,7 +596,9 @@ throw new Error(data.error || data.message || '觸發失敗'); } if (triggerButton) { - triggerButton.innerHTML = '已排入'; + triggerButton.innerHTML = data.status === 'already_running' + ? '執行中' + : '已排入'; } if (statusNode) { statusNode.classList.remove('is-working');