補 PPT AiderHeal 執行狀態
All checks were successful
CD Pipeline / deploy (push) Successful in 1m5s

This commit is contained in:
OoO
2026-05-19 00:49:06 +08:00
parent 83fd05d614
commit c08f76f315
7 changed files with 242 additions and 4 deletions

View File

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

View File

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

View File

@@ -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,
)

View File

@@ -66,6 +66,25 @@
{% endif %}
{% if error %}<div class="alert alert-warning mt-3"><strong><i class="fas fa-triangle-exclamation me-1"></i></strong>{{ error }}</div>{% endif %}
<section class="ppt-aider-status{% if not aider_heal_active_count %} is-empty{% endif %}" data-ppt-aider-status aria-live="polite">
<div class="ppt-aider-status-main">
<span class="ppt-run-status is-planned"><i class="fas fa-wrench me-1" aria-hidden="true"></i>AiderHeal</span>
<div>
<strong data-ppt-aider-status-title>{% if aider_heal_active_count %}AiderHeal 執行中 · {{ aider_heal_active_count }}{% else %}AiderHeal 待命{% endif %}</strong>
<small data-ppt-aider-status-meta>{% if aider_heal_active_count %}修復完成後會由 Telegram/Gitea/CD 回報。{% else %}有問題的審核紀錄可直接一鍵派工。{% endif %}</small>
</div>
</div>
<div class="ppt-aider-job-list" data-ppt-aider-job-list>
{% for job in aider_heal_active_jobs[:3] %}
<div class="ppt-aider-job">
<code>{{ job.pptx_filename or 'manual' }}</code>
<span>{{ job.queued_at }}</span>
<small>{{ job.diagnosis }}</small>
</div>
{% endfor %}
</div>
</section>
<section class="ppt-toolbar">
<div class="d-flex align-items-center gap-2">
<a class="btn btn-sm btn-outline-secondary" href="{{ url_for('admin_observability.ppt_audit_history', month=prev_month_label, report_type=report_type) }}">

View File

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

View File

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

View File

@@ -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 => `
<div class="ppt-aider-job">
<code>${escapeHtml(job.pptx_filename || 'manual')}</code>
<span>${escapeHtml(job.queued_at || '')}</span>
<small>${escapeHtml(job.diagnosis || '')}</small>
</div>
`).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]');