feat(wave6-8): P2.1 fusion + P2.2 governance + P2.4 consensus + Wave 7/8 BLOCKER 修復

承接 Wave 6/7/8 多 engineer 在 agent 限額前完成的代碼,補 commit 解 production
HEAD 隱性 import error(decision_fusion 已被 decision_manager 引用但檔案 untracked)。

新增(後端核心):
- decision_fusion.py (562 行) — P2.1 方法 III(OpenClaw + Hermes + Elephant 三 LLM 融合)
- aiops_timeline.py + aiops_timeline_service.py — critic B4 修復
  /api/v1/aiops/timeline endpoint,DB 存取抽到 service 層遵守 leWOOOgo 積木化
- migrations/p2_decision_fusion_columns.sql + rollback — approval_records fusion 欄位

修改(後端整合):
- decision_manager.py — fusion 三斷鏈修補(critic B1+B2+B3):
  · B1: 寫 _evidence_snapshot_ref 到 token.proposal_data
  · B2: fusion 前計算 complexity_score 並寫 token
  · B3: fusion composite 寫 token.proposal_data["decision_fusion"]
- auto_approve.py — fusion + consensus 認識(critic B3+B5):
  · composite > 0.7 → auto_execute_eligible bypass min_confidence
  · source=consensus_engine + score>=0.6 → 規則可信路徑
- consensus_engine.py — db-fix _save_consensus 重用 agent_sessions
- governance_agent.py — db-fix _alert PG 寫入 ai_governance_events
- approval_db.py — fusion 3 欄位 + 2 partial index + CheckConstraint
- db/models.py — schema 對齊 migration
- core/config.py — vuln #1 修復:OLLAMA_URL/_FALLBACK_URL field_validator
  拒絕公網 IP + 外部域名,僅允許私網/loopback/K8s SVC 白名單
- core/feature_flags.py — P2 fusion + consensus flags
- main.py — governance_agent lifespan 啟動
- failover_alerter.py — Wave8-X2: in-memory dedup fallback(Redis 拒絕後不 fail-open)
- ollama_*.py — metrics 整合 + recovery 改善
- auto_repair_service.py — verifier 接線

新增(測試 2438 行):
- test_decision_fusion.py / test_governance_agent.py / test_consensus_integration.py
- test_p2_db_fixes.py / test_wave8_fusion_fixes.py
- test_config_url_validation.py(vuln #1 12 tests)
- test_failover_alerter.py +Wave8-X2 in-memory dedup 補測

驗收: 116 tests pass (decision_fusion + wave8_fusion + config_url + consensus +
                      governance + p2_db_fixes + failover_alerter)

Conflict resolution:
- 3 檔(config.py + auto_approve.py + decision_manager.py)git stash pop 衝突
  保留 stashed (engineer 最終版),補回 ValueError 「公網 IP」字樣對齊 test

Note: 此 commit 解 production HEAD 隱性 import error
仍未修: vuln #4 prompt injection / debugger B14 quota fail-closed
       / B25-B26 drain_pending_tasks / B8 governance fail alert

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Multiple Engineers (Wave 6/7/8) <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-04-27 08:10:28 +08:00
parent b0bf3783e4
commit cc547736ab
34 changed files with 4205 additions and 25 deletions

View File

@@ -31,6 +31,11 @@ class FailoverAlerter:
def __init__(self, redis_client=None) -> None:
# telegram_gateway 從 singleton 取不注入lifespan 已確保初始化)
self._redis = redis_client
# 2026-04-27 Wave8-X2 by Claude — alerter dedup fail-open 修復
# Redis 不可用時改用 in-memory dedup避免同一事件狂發 Telegram
# 限制:同 process 內生效;重啟後記憶清空(可接受,重啟本身就是罕見事件)
self._memory_dedup: dict[str, float] = {}
self._memory_dedup_max_size = 1000
async def alert_failover(self, event: dict[str, Any]) -> None:
"""111 故障切換到 Gemini/188 — 10min dedup"""
@@ -145,18 +150,35 @@ class FailoverAlerter:
Redis SET NX EX 防止重複告警。
True = 第一次應送出False = 已送過(跳過)。
fail-openRedis 不可用時允許送出(不阻擋通知)
2026-04-25 P1.5 by Claude Engineer-D — Telegram dedup 鐵律 10min/24h TTL
2026-04-27 Wave8-X2 by Claude — dedup fail-open 修復
原行為Redis 不可用 → return True → 每次都發 → Telegram 轟炸
新行為Redis 不可用時降級到 in-memory dedup同 process 內限流)
Redis 恢復後自動優先走 Redisin-memory 只在 except 分支觸發)
"""
if self._redis is None:
return True # fail-open
try:
ok = await self._redis.set(f"{key}:dedup", "1", ex=ttl, nx=True)
return bool(ok)
except Exception as e:
logger.warning("dedup_check_failed", error=str(e))
return True # fail-open
# 優先嘗試 Redis
if self._redis is not None:
try:
ok = await self._redis.set(f"{key}:dedup", "1", ex=ttl, nx=True)
return bool(ok)
except Exception as e:
logger.warning("dedup_redis_failed_using_memory", error=str(e))
# Redis 失敗 → 降級到 in-memory fail-open
# In-memory fallback dedupRedis 不可用時,或 redis_client=None 時)
import time
now = time.time()
# GC超過容量上限時清除過期 entry防 dict 無限成長
if len(self._memory_dedup) >= self._memory_dedup_max_size:
self._memory_dedup = {
k: v for k, v in self._memory_dedup.items() if now - v < ttl
}
last_sent = self._memory_dedup.get(key, 0.0)
if now - last_sent < ttl:
return False # dedup 命中,跳過
self._memory_dedup[key] = now
return True
# -------------------------------------------------------------------------
# 發送(透過 TelegramGateway singleton