補 PPT AiderHeal 去重鎖
All checks were successful
CD Pipeline / deploy (push) Successful in 1m5s

This commit is contained in:
OoO
2026-05-19 00:35:11 +08:00
parent 02682c81ed
commit e60707cdfb
5 changed files with 67 additions and 2 deletions

View File

@@ -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` 拆成投影片、問題類型、問題文字與回放入口,新增「視覺問題追蹤」面板,讓問題簡報能直接定位與預覽。

View File

@@ -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 # 用於模板顯示

View File

@@ -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(

View File

@@ -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
# ──────────────────────────────────────────────────────────────────────────

View File

@@ -596,7 +596,9 @@
throw new Error(data.error || data.message || '觸發失敗');
}
if (triggerButton) {
triggerButton.innerHTML = '<i class="fas fa-check me-1"></i>已排入';
triggerButton.innerHTML = data.status === 'already_running'
? '<i class="fas fa-clock me-1"></i>執行中'
: '<i class="fas fa-check me-1"></i>已排入';
}
if (statusNode) {
statusNode.classList.remove('is-working');