fix(aiops): ADR-092 三修 — Playbook enum崩潰 + Telegram永久靜默 + 採納失敗 + AI自健診
All checks were successful
CD Pipeline / build-and-deploy (push) Successful in 13m33s

B1 playbook_service.py: evolver setattr傳str而非PlaybookStatus enum
  → _pg_upsert playbook.status.value炸(163次/48h),修:update_with_validation強制enum轉型

B2 approval_db.py + webhooks.py: find_by_fingerprint PENDING誤收斂
  → PENDING≠Telegram已發;修:成功push後mark tg_sent:{fingerprint} Redis(24h TTL)
  → find_by_fingerprint debounce窗外PENDING必須Redis確認才收斂

drift_adopt_service.py: telegram_gateway呼叫adopt_drift(report_id)但方法不存在
  → 新增adopt_drift()包裝:從DB載入DriftReport後委派adopt(),修復採納失敗

B3 ai_slo_watchdog_job.py + main.py: AI無法感知自身故障(MASTER §1.1盲區)
  → 新增每15分鐘自健診:W-1 SLO違反 W-2 TG靜默偵測 W-3 飛輪成功率
  → 任一異常→TYPE-8M send_meta_alert;Redis去重1h

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-04-20 20:00:06 +08:00
parent 1744b1e923
commit 156a52f807
6 changed files with 230 additions and 0 deletions

View File

@@ -20,6 +20,7 @@ from uuid import UUID
import structlog
from sqlalchemy import and_, or_, select, update
from src.core.redis_client import get_redis
from src.core.trust_engine import classify_risk, get_required_signatures
from src.db.base import get_db_context
from src.db.models import ApprovalRecord, TimelineEvent
@@ -330,10 +331,45 @@ class ApprovalDBService:
hit_count=record.hit_count,
status=record.status.value if hasattr(record.status, 'value') else record.status,
)
# 2026-04-20 ogt + Claude Opus 4.7: ADR-092 tg_sent Redis 驗證
# PENDING 記錄不代表 Telegram 已發送(可能因網路/Token錯誤而靜默失敗
# 僅在 debounce 窗口外的 PENDING 收斂時,必須確認 Redis 有 tg_sent 標記
within_debounce = record.created_at >= cutoff_time
if not within_debounce:
try:
r = get_redis()
tg_confirmed = await r.exists(f"tg_sent:{fingerprint}")
except Exception as _re:
tg_confirmed = False
logger.warning("tg_sent_redis_check_failed", fingerprint=fingerprint, error=str(_re))
if not tg_confirmed:
logger.warning(
"fingerprint_pending_no_tg_confirmation",
fingerprint=fingerprint,
approval_id=str(record.id),
created_at=record.created_at.isoformat(),
)
return None # 視為新告警,重新發送 Telegram
return approval_record_to_request(record)
return None
async def mark_telegram_confirmed(self, fingerprint: str, ttl: int = 86400) -> None:
"""
2026-04-20 ogt + Claude Opus 4.7: ADR-092
記錄 Telegram 已成功發送,防止 PENDING 誤收斂造成永久靜默。
TTL 與 PENDING_TTL_HOURS 對齊24h
"""
try:
r = get_redis()
await r.setex(f"tg_sent:{fingerprint}", ttl, "1")
logger.debug("tg_sent_marked", fingerprint=fingerprint, ttl=ttl)
except Exception as e:
logger.warning("tg_sent_mark_failed", fingerprint=fingerprint, error=str(e))
async def increment_hit_count(
self,
approval_id: UUID,

View File

@@ -59,6 +59,18 @@ class DriftAdoptService:
self._repo = settings.GITEA_REPO_NAME
self._k8s_dir = pathlib.Path("k8s")
async def adopt_drift(self, report_id: str) -> dict:
"""
2026-04-20 ogt + Claude Opus 4.7: Telegram 按鈕呼叫入口
從 DB 載入 DriftReport 後委派給 adopt()。
telegram_gateway._handle_drift_action 呼叫此方法。
"""
from src.repositories.drift_repository import get_drift_repository
report = await get_drift_repository().get(report_id)
if not report:
return {"success": False, "message": f"Report {report_id} not found"}
return await self.adopt(report)
async def adopt(self, report: "DriftReport", field_description: str = "") -> dict:
"""
將漂移寫回 Git建立 branch + commit + PR

View File

@@ -537,8 +537,22 @@ class PlaybookService:
return None
# 應用更新
# 2026-04-20 ogt + Claude Opus 4.7: setattr 不觸發 Pydantic validation
# Evolver 傳入 PlaybookStatus.DEPRECATED.valuestr "deprecated"
# → _pg_upsert playbook.status.value 炸:'str' has no attribute 'value'
# 修Enum 欄位強制轉型,防止 str 混入 Playbook 物件
for field, value in update_data.items():
if value is not None and hasattr(playbook, field):
if field == "status" and isinstance(value, str) and not isinstance(value, PlaybookStatus):
try:
value = PlaybookStatus(value)
except ValueError:
pass
elif field == "source" and isinstance(value, str) and not isinstance(value, PlaybookSource):
try:
value = PlaybookSource(value)
except ValueError:
pass
setattr(playbook, field, value)
return await self._repository.update(playbook)