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);
});
});