fix(aiops): ADR-092 三修 — Playbook enum崩潰 + Telegram永久靜默 + 採納失敗 + AI自健診
All checks were successful
CD Pipeline / build-and-deploy (push) Successful in 13m33s
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:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -537,8 +537,22 @@ class PlaybookService:
|
||||
return None
|
||||
|
||||
# 應用更新
|
||||
# 2026-04-20 ogt + Claude Opus 4.7: setattr 不觸發 Pydantic validation
|
||||
# Evolver 傳入 PlaybookStatus.DEPRECATED.value(str "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)
|
||||
|
||||
Reference in New Issue
Block a user