# Skill 02: leWOOOgo Backend Core
# 後端核心引擎守護者

> **管轄範圍**: `apps/api/` (FastAPI, Python)
> **觸發條件**: 任何 `.py` 檔案修改

---

## 文件資訊

| 欄位 | 值 |
|------|-----|
| **版本** | v2.4 |
| **建立日期** | 2026-03-20 (台北) |
| **建立者** | Claude Code |
| **最後修改** | 2026-03-31 21:00 (台北) |
| **修改者** | Claude Code |

### 變更紀錄

| 版本 | 日期 | 執行者 | 變更內容 |
|------|------|--------|----------|
| v1.0 | 2026-03-20 | Claude Code | 初始建立 |
| v1.1 | 2026-03-25 | Claude Code | 新增 MCP Tool 實作標準 |
| v1.2 | 2026-03-25 | Claude Code | 加入文件資訊區塊 |
| v1.3 | 2026-03-26 | Claude Code | 🔴🔴🔴 新增積木化強制執行章節 (32 項違規審計後) |
| v1.4 | 2026-03-26 | Claude Code | 📊 新增 Langfuse LLMOps 整合章節 (Phase 15.1) |
| v1.5 | 2026-03-26 | Claude Code | 🔴🔴 新增 UnitOfWork + Saga Pattern 章節 (ADR-027) |
| v1.6 | 2026-03-26 | Claude Code | 🚨 新增 Telegram 去重機制章節 (告警轟炸事故教訓) |
| v1.7 | 2026-03-26 | Claude Code | 🤖 新增 ADR-030 智能自動修復章節 (5 個新服務) |
| v1.8 | 2026-03-28 | Claude Code | ✅ Phase 16 首席架構師驗收 50/50 OUTSTANDING |
| v1.9 | 2026-03-28 | Claude Code | 🦞 新增 Phase 19 Terminal SSE 後端整合章節 |
| v2.0 | 2026-03-29 | Claude Code | 🔴 Phase D-G P0 修正: 新增 LearningRepository (積木化合規) |
| v2.1 | 2026-03-29 | Claude Code | 🛡️ 新增 ADR-038/039 OpenClaw 安全網章節 (Wave 1 部署) |
| v2.2 | 2026-03-30 | Claude Code | 🎯 新增 Phase 19.4 API 整合章節 (Approval + K8s 真實 API) |
| v2.3 | 2026-03-30 | Claude Code | 🤖 新增 AI Fallback 順序章節 (NVIDIA 優先仲裁) |
| v2.4 | 2026-03-31 | Claude Code | 🏛️ Phase 22 首席架構師審查通過 (Mock違規+分層修復全部完成) |
| v2.5 | 2026-04-01 | Claude Code | ♻️ Phase R-R2 完成 (legacy -971行) + R-R2.1 P0/P1修復 + ADR-046 型別統一 |
| v2.6 | 2026-04-08 | Claude Code | 🛡️ Sprint 5.1 Data Safety Guardrails — Service Registry 模式 + 審查修正鐵律 |
| v2.7 | 2026-04-09 | Claude Sonnet 4.6 | 🔧 ADR-066 批准執行閉環修復 — Nemotron tool→kubectl_command 回填鐵律 |
| v2.8 | 2026-04-10 | Claude Sonnet 4.6 | 🚀 ADR-068 飛輪冷啟動修復鐵律 — affected_services/Router層業務邏輯/Jaccard豁免/embedding持久化 |

---

## 核心約束 (Six Iron Laws)

### 1. Async-First (非同步優先)

```python
# ✅ 正確
async def get_health() -> HealthResponse:
    result = await check_services()
    return result

# ❌ 禁止
def get_health() -> HealthResponse:  # 同步函數
    result = check_services()
    return result
```

### 2. CORS 白名單鎖定 (無 Wildcard)

```python
# ✅ 正確 (config.py)
CORS_ORIGINS: list[str] = [
    "https://awoooi.wooo.work",
    "http://localhost:3000",
]

# ❌ 絕對禁止
CORS_ORIGINS = ["*"]
```

### 3. Pydantic 強型別驗證

```python
# ✅ 正確
class SignalPayload(BaseModel):
    source: str
    severity: Literal["P0", "P1", "P2", "P3"]
    message: str

# ❌ 禁止
def process_signal(data: dict):  # 無型別驗證
    pass
```

### 4. structlog 結構化日誌

```python
# ✅ 正確
import structlog
logger = structlog.get_logger()
logger.info("signal_processed", signal_id=signal.id, severity=signal.severity)

# ❌ 禁止
print(f"Signal {signal.id} processed")
import logging  # 原生 logging
```

### 5. 台北時區鐵律 (禁止 UTC)

```python
# ❌ 禁止 - UTC 時區
from datetime import UTC
datetime.now(UTC)
datetime.utcnow()

# ✅ 正確 - 台北時區工具
from src.utils.timezone import now_taipei, now_taipei_iso
now_taipei()           # datetime with +08:00
now_taipei_iso()       # '2026-03-25T02:08:04+08:00'
```

**Memory 參考**: `feedback_timezone_taipei.md`

### 6. ADR-013 Google Style Docstring

**強制場景**: 安全相關、複雜邏輯、外部依賴、危險操作

```python
def restart_pod(namespace: str, pod_name: str) -> bool:
    """重啟指定 Pod。

    Args:
        namespace: K8s 命名空間
        pod_name: Pod 名稱 (不含 hash 後綴)

    Returns:
        True 表示重啟成功

    Raises:
        K8sPermissionError: 缺少 delete pods 權限

    Warning:
        此操作會導致短暫服務中斷
    """
```

---

## OTEL 可觀測性 (P0 核心)

### 強制注入追蹤

```python
# 所有 API 請求必須自動產生 traces
# 目標端點: http://192.168.0.188:24317 (SigNoz OTEL Collector)

# 環境變數 (必須設定)
OTEL_ENABLED=true
OTEL_EXPORTER_OTLP_ENDPOINT=http://192.168.0.188:24317
OTEL_SERVICE_NAME=awoooi-api
```

---

## Langfuse LLMOps (Phase 15.1)

> **用途**: LLM 呼叫追蹤、成本監控、Prompt 版本管理
> **端點**: http://192.168.0.110:3100

### 環境變數

```bash
LANGFUSE_URL=http://192.168.0.110:3100
LANGFUSE_PUBLIC_KEY=pk-lf-xxx  # 從 K8s Secret
LANGFUSE_SECRET_KEY=sk-lf-xxx  # 從 K8s Secret
```

### 整合模式 (待實作)

```python
from langfuse import Langfuse

langfuse = Langfuse(
    public_key=settings.LANGFUSE_PUBLIC_KEY,
    secret_key=settings.LANGFUSE_SECRET_KEY,
    host=settings.LANGFUSE_URL
)

# 包裝 LLM 呼叫
with langfuse.trace(name="openclaw_decision") as trace:
    generation = trace.generation(
        name="ollama_call",
        model="llama3.2:3b",
        input=prompt,
    )
    result = await _call_ollama(prompt)
    generation.end(output=result)
```

**Memory 參考**: `reference_langfuse_credentials.md`

---

## 機密管理 (嚴禁硬編碼)

```python
# ✅ 正確 (從環境變數讀取)
settings.GEMINI_API_KEY
settings.CLAUDE_API_KEY
settings.WEBHOOK_HMAC_SECRET

# ❌ 絕對禁止
API_KEY = "sk-xxx..."  # 硬編碼機密
```

---

## 強制驗收程序 (Mandatory Validation)

### 修改任何 `.py` 後必須執行:

```bash
# Step 1: 語法檢查
cd apps/api && python -m py_compile src/core/config.py

# Step 2: 本地啟動測試 (MOCK_MODE)
docker run --rm -e MOCK_MODE=true -p 8888:8000 awoooi-api-test

# Step 3: Health Check
curl -sf http://localhost:8888/api/v1/health
```

### 驗收標準

| 檢查項目 | 通過條件 |
|----------|----------|
| py_compile | 無 SyntaxError |
| Docker Run | Application startup complete |
| Health API | HTTP 200, status: healthy/degraded |

---

## 常見陷阱 (Known Pitfalls)

1. **Pydantic v2 Field 命名**: 禁止底線開頭 (`_field_name`)
2. **JSON Array 環境變數**: ConfigMap 中使用 `'["a","b"]'` 格式
3. **httpx 超時**: 預設 90 秒，長時間 AI 調用需調整

---

## 🚨 Redis Worker 專屬連線 (2026-03-23 教訓)

> **事故**: Worker 使用 API 共用 Redis 連線池 (socket_timeout=5s)，與 XREADGROUP BLOCK 5000ms 衝突，導致每 5 秒 TimeoutError

### 鐵律: Worker 必須獨立連線池

```python
# ✅ 正確: Worker 專屬長連線
async def init_worker_redis_pool() -> redis.Redis:
    return redis.from_url(
        settings.REDIS_URL,
        socket_timeout=None,  # 無限等待 (XREADGROUP 阻塞)
        socket_connect_timeout=10.0,
    )

# ❌ 禁止: Worker 共用 API 短超時連線
redis_client = get_redis()  # socket_timeout=5s
await redis_client.xreadgroup(block=5000)  # 必定超時
```

### 檢查清單

| 項目 | 正確值 |
|------|--------|
| API Redis socket_timeout | 5.0 秒 |
| Worker Redis socket_timeout | None (無限) |
| XREADGROUP block | 5000ms |

---

## 🚨 SQLite 禁止令 (2026-03-23 教訓)

> **事故**: Worker 與 API Pod 各自使用本地 SQLite，導致「腦分裂」與 `no such table: incidents` 錯誤

### 鐵律: 絕對禁止 SQLite，強制 PostgreSQL

```python
# ✅ 正確: 使用 DATABASE_URL (PostgreSQL)
def get_engine() -> AsyncEngine:
    database_url = settings.DATABASE_URL  # postgresql+asyncpg://...

    # 禁止 SQLite 守衛
    if "sqlite" in database_url.lower():
        logger.error("sqlite_forbidden")
        database_url = "postgresql+asyncpg://awoooi:xxx@192.168.0.188:5432/awoooi_prod"

    return create_async_engine(database_url, ...)

# ❌ 禁止: 使用 SQLITE_DATABASE_URL
settings.SQLITE_DATABASE_URL  # 已廢棄，禁止使用
```

### 生產環境連線

| 項目 | 值 |
|------|-----|
| 主機 | 192.168.0.188 |
| Port | 5432 |
| Database | awoooi_prod |
| 連線字串前綴 | `postgresql+asyncpg://` |

---

## 🚨 函數重命名全域搜尋 (2026-03-23 教訓)

> **事故**: `init_redis()` 重命名為 `init_redis_pool()`，但 Worker standalone 模式遺漏更新，導致 ImportError

### 鐵律: 重命名前必須全域搜尋

```bash
# 修改函數名稱前，必須執行:
grep -rn "old_function_name" apps/api/src/

# 確認所有呼叫點都已更新後，才能提交
```

### 高風險區域

- `workers/signal_worker.py` standalone `_main()` 函數
- `main.py` lifespan 初始化
- 測試檔案 `tests/`

---

## 🧱 leWOOOgo Memory Providers (Phase 6.4d - 2026-03-23)

> **新架構**: 雙層記憶體 (Working + Episodic)

### 記憶層級

| 層級 | Provider | 儲存 | TTL |
|------|----------|------|-----|
| Working Memory | `RedisMemoryProvider` | Redis | 7 天 |
| Episodic Memory | `PgMemoryProvider` | PostgreSQL | 永久 |
| 雙層整合 | `DualMemoryProvider` | 兩者同步 | - |

### 使用方式

```python
from lewooogo_data.providers import (
    RedisMemoryProvider,
    PgMemoryProvider,
    DualMemoryProvider,
    init_redis_pool,
    init_pg_engine,
)

# 初始化連線池 (啟動時執行)
await init_redis_pool()
await init_pg_engine()

# 建立 Provider
from your_models import Incident

# 單層使用
redis_memory = RedisMemoryProvider(Incident, key_prefix="incidents")
pg_memory = PgMemoryProvider(Incident)

# 雙層使用 (推薦)
dual_memory = DualMemoryProvider(Incident, key_prefix="incidents")

# CRUD 操作
await dual_memory.save("inc-001", incident)
data = await dual_memory.load("inc-001")  # Working 優先，Episodic 備援
```

### 鐵律

| 規則 | 說明 |
|------|------|
| TTL 必須設定 | Redis 所有 key 必須有 TTL，禁止無限累積 |
| 雙層同步 | 寫入時 Working + Episodic 同步 |
| 優雅降級 | Redis 斷線不影響主流程 |
| 禁止直接存取 | 所有記憶體操作必須透過 Provider |

### 檔案位置

```
packages/lewooogo-data/src/lewooogo_data/
├── interfaces/
│   └── memory_provider.py    # IMemoryProvider, IDualMemoryProvider
└── providers/
    ├── redis_memory.py       # RedisMemoryProvider
    ├── pg_memory.py          # PgMemoryProvider
    └── dual_memory.py        # DualMemoryProvider
```

---

## 🔴🔴 UnitOfWork + Saga Pattern (ADR-027)

> **用途**: 確保 Incident-Approval 雙層寫入原子性
> **批准日期**: 2026-03-26
> **Memory**: `feedback_incident_approval_sync.md`, `project_incident_approval_sync.md`

### 問題背景

Incident 和 Approval 的建立涉及兩層儲存 (PostgreSQL + Redis)，必須確保：
1. 兩者同時成功或同時失敗
2. 狀態變更時雙向同步

### 核心模式

```python
# ✅ 正確: 使用 IncidentApprovalService
from src.services.incident_approval_service import IncidentApprovalService

async def handle_alert(data: AlertData):
    service = IncidentApprovalService(session_factory, redis_client)
    incident, approval = await service.create_with_approval(
        incident_data, approval_data
    )

# ❌ 禁止: 分別建立 Incident 和 Approval
incident = await incident_repo.create(...)  # PostgreSQL
approval = await approval_repo.create(...)  # Redis 可能失敗，導致孤兒 Incident
```

### UnitOfWork 模式

```python
from src.core.unit_of_work import UnitOfWork

async with UnitOfWork(session_factory) as uow:
    # 所有操作在同一事務中
    incident = await self.incident_repo.create(uow.session, data)
    approval = await self.approval_repo.create(uow.session, data)

    # Redis 寫入 (事務外，需 Saga 補償)
    try:
        await self._write_to_redis(incident, approval)
    except RedisError:
        await uow.rollback()  # Saga 補償
        raise
```

### 鐵律

| 規則 | 說明 |
|------|------|
| 禁止單獨建立 | Incident/Approval 必須透過 IncidentApprovalService |
| 狀態同步 | Approval 變更時必須同步 Incident |
| TTL 統一 | 使用 `src/core/constants.py` 定義的 TTL |
| Redis 失敗回滾 | 使用 Saga Pattern 補償 PostgreSQL 事務 |

### 檔案位置

```
apps/api/src/
├── core/
│   ├── unit_of_work.py        # 事務管理
│   └── constants.py           # TTL 常量
└── services/
    └── incident_approval_service.py  # 原子同步服務
```

---

## 🚨 Telegram 去重機制 (2026-03-26 事故教訓)

> **事故**: Telegram 告警轟炸，同樣告警重複發送數十次，ID 格式 `INC-INC-INC-2026`
> **根因**: Phase 6.5 修改導致每次 poll 建立新 decision → 觸發 Telegram

### 鐵律 1: Telegram 發送必須去重

```python
# decision_manager.py - _push_to_telegram_on_decision()
async def _push_to_telegram_on_decision(self, incident, decision):
    # 去重檢查 (10 分鐘 TTL)
    dedup_key = f"telegram_sent:{incident.incident_id}"
    if await redis_client.get(dedup_key):
        logger.debug("telegram_push_skipped_dedup", incident_id=incident.incident_id)
        return

    # 發送 Telegram...

    # 設定去重 key
    await redis_client.set(dedup_key, "1", ex=600)
```

### 鐵律 2: INC- 前綴禁止重複

```python
# ❌ 禁止: 不檢查直接加前綴
incident_id = f"INC-{self.approval_id[:8].upper()}"

# ✅ 正確: 檢查後再加
if self.approval_id.upper().startswith("INC-"):
    incident_id = self.approval_id.upper()
else:
    incident_id = f"INC-{self.approval_id[:8].upper()}"
```

### 檢查清單

| 項目 | 正確做法 |
|------|----------|
| Telegram 去重 | Redis key `telegram_sent:{id}` + 600 秒 TTL |
| INC- 前綴 | 發送前檢查是否已有前綴 |
| Decision 建立 | 返回現有 decision 時不發送 Telegram |

---

## 🧰 MCP Tool 實作規範 (Phase 13.2)

> **目標**: 將 Mock MCP Tool 升級為真實系統連接
> **優先級**: P0 最優先

### MCP Bridge 位置

```
apps/api/src/plugins/mcp/
├── __init__.py
├── mcp_bridge.py      # 核心橋樑
└── tools/             # 新增目錄
    ├── signoz_tool.py     # #79 SignOz MCP
    ├── kubernetes_tool.py # #80 Kubernetes MCP
    └── postgres_tool.py   # #81 PostgreSQL MCP
```

### Tool 實作模板

```python
from src.plugins.mcp.mcp_bridge import MCPTool, MCPToolResult

class SignOzTool:
    """SignOz 監控查詢 Tool"""

    async def query_traces(
        self,
        service_name: str,
        start_time: datetime,
        end_time: datetime,
    ) -> MCPToolResult:
        """查詢服務 Traces

        Args:
            service_name: 服務名稱 (awoooi-api, awoooi-web)
            start_time: 查詢起始時間 (台北時區)
            end_time: 查詢結束時間

        Returns:
            MCPToolResult with trace data

        Warning:
            SignOz API 有速率限制，避免高頻查詢
        """
        # 實作邏輯...
```

### Tool 實作鐵律

| 規則 | 說明 |
|------|------|
| Privacy Shield | 所有 Tool 調用必須經過 RehydrationEngine |
| 錯誤處理 | 外部系統失敗不影響主流程 |
| 日誌脫敏 | 禁止 log 敏感參數 (IP/Token) |
| 超時設定 | 每個 Tool 必須設定合理 timeout |
| 台北時區 | 所有時間參數必須使用 +8 時區 |

### Tool 狀態

| Tool | 狀態 | 說明 |
|------|------|------|
| Kubernetes | 🟡 Mock | #80 待實作真實 kubectl |
| Database | 🟡 Mock | #81 待連接 PostgreSQL |
| **SignOz** | ❌ 缺失 | #79 **P0** 查詢 Trace/Logs |
| **維運手冊 RAG** | ❌ 缺失 | #84 Markdown 向量化 |

---

## 🔴🔴🔴 積木化強制執行 (2026-03-26 審計後新增)

> **審計結果**: 32 項違規，3 週開發工時浪費
> **Memory**: `feedback_lewooogo_modular_enforcement.md`

### 修改前必問 5 題

1. **這個邏輯是否已存在於 packages/？** → 用 packages，禁止重寫
2. **Router 是否只做 HTTP 轉發？** → 禁止 Router 直接存取 Redis/DB
3. **Service 是否依賴 Interface？** → 必須用 Protocol/ABC
4. **是否可被其他模組重用？** → 可重用邏輯放 packages/
5. **是否遵循依賴注入？** → 禁止 global singleton

### 禁止清單 (Router 層)

```python
# ❌ 禁止
from src.core.redis_client import get_redis  # 應透過 Service
from src.db.base import get_session          # 應透過 Repository
LUA_SCRIPT = """..."""                        # 應放在 Repository
```

### 正確架構層次

```
api/v1/*.py (Router) → services/*.py (Service) → packages/lewooogo-*/ (積木)
```

### 違規時

1. **停止修改，通報統帥**
2. **必須修改違規檔案 → 同時修復違規**
3. **新增功能 → 放在正確層次**

---

## 🤖 ADR-030 智能自動修復系統 (2026-03-26)

> **架構審查**: 4/5⭐ 通過 (首席架構師簽核)
> **Memory**: `project_adr030_architecture_review.md`

### 新增服務清單

| 服務 | 行數 | 職責 | 層次 |
|------|------|------|------|
| `k8s_diagnostics.py` | 654 | K8s 診斷資料收集 | Service |
| `diagnosis_aggregator.py` | 590 | 多源診斷整合 | Service |
| `playbook_rag.py` | 624 | RAG 向量搜尋 | Service |
| `auto_approve.py` | 391 | 自動執行策略 | Service |
| `learning_service.py` | 550+ | 持續學習迴圈 + 修復推薦 | Service |
| `learning_repository.py` | 200 | 學習數據 Redis 持久化 | Repository |

### 流程圖

```
Incident → Expert 分類 → 診斷收集 → RAG 匹配
    │
    ├─ AutoApprovePolicy (條件判斷)
    │   ├─ YES → 自動執行 → LearningService
    │   └─ NO  → Telegram 人工審核
    │
    └─ 執行完成 → 信任度調整 + Playbook 統計
```

### ⚠️ 已知技術債 (P1 需本週修復)

| 嚴重度 | 檔案 | 違規內容 |
|--------|------|----------|
| **P1** | `playbook_rag.py:29` | Service 直接 import Redis |
| **P1** | `playbook_rag.py:156` | 自建 httpx.AsyncClient |

**修復方式**: 改用 Repository 層 + Lifespan 管理的 Client

---

## 🛡️ ADR-038/039 OpenClaw 安全網 (2026-03-29)

> **架構審查**: Wave 1 194/200 (97%) OUTSTANDING
> **Memory**: `project_current_status.md`
> **ADR**: ADR-038 (推理併發治理) + ADR-039 (全域修復熔斷)

### 鐵律：所有 OpenClaw 呼叫必須雙層保護

```python
from src.core.circuit_breaker import get_openclaw_guard

async def call_openclaw_analyzer(error_context: dict) -> ErrorAnalysisResult | None:
    guard = get_openclaw_guard()

    # Layer 1: Circuit Breaker 快速失敗
    if guard.is_circuit_open():
        logger.warning("openclaw_circuit_open", metrics=guard.get_metrics())
        return None

    # Layer 2: Concurrency Semaphore 限流 (最多 3 並發)
    async with guard.semaphore:
        try:
            async with httpx.AsyncClient() as client:
                response = await client.post(...)
                guard.record_success()
                return result
        except Exception:
            guard.record_failure()  # 連續 5 次失敗 → 斷路 60s
            return None
```

### 🔴 全域修復冷卻 (ADR-039)

**鐵律**: 任何自動修復前必須呼叫 `check_global_repair_cooldown()`

```python
from src.services.global_repair_cooldown import (
    check_global_repair_cooldown,
    record_global_repair_action,
)

# ✅ 正確
async def execute_auto_repair(action: str, target: str):
    if not await check_global_repair_cooldown():
        logger.warning("global_repair_cooldown_active")
        return None

    result = await _do_repair(action, target)
    await record_global_repair_action(action, target)
    return result
```

### StatefulSet 黑名單

| 服務 | 說明 | 狀態 |
|------|------|------|
| `postgres` | 資料庫 | 🔴 永遠禁止自動修復 |
| `redis` | 快取/訊息 | 🔴 永遠禁止自動修復 |
| `clickhouse` | 分析資料庫 | 🔴 永遠禁止自動修復 |
| `minio` | 物件儲存 | 🔴 永遠禁止自動修復 |
| `etcd` | 叢集狀態 | 🔴 永遠禁止自動修復 |

**允許修復**: `awoooi-api`, `awoooi-web`, `awoooi-worker` (無狀態服務)

### Worker XCLAIM 機制

```python
# Signal Worker 必須實作孤兒訊息回收
# 閒置超過 60s 的訊息會被 XCLAIM 回收

# 相關配置:
XCLAIM_IDLE_THRESHOLD_MS = 60_000  # 60 秒
XCLAIM_BATCH_SIZE = 10             # 每批最多 10 條
terminationGracePeriodSeconds: 90  # K8s 優雅關機
Python stop() timeout: 75          # 比 K8s 少 15s 緩衝
```

---

## 🤖 AI Fallback 順序 (2026-03-30 首席架構師 94/100)

> **ConfigMap**: `AI_FALLBACK_ORDER: '["nvidia","gemini","ollama","claude"]'`
> **審查結果**: P0 修復後 85/100 → 最終 94/100

### 🔴 鐵律：Nemotron/Gemini Tool Call 必須回填 kubectl_command (ADR-066)

**背景**: 幾個月來批准按鈕完全無效，因為 Nemotron tool 結果未傳播到執行鏈路。

```python
# ✅ 正確 — openclaw.py 必須回填
_tools = proposal["nemotron_tools"]
if _tools:
    _t = _tools[0]
    if _t["tool"] == "restart_deployment":
        proposal["kubectl_command"] = f"kubectl rollout restart deployment/{_deploy} -n {_ns}"
    elif _t["tool"] == "delete_pod":
        proposal["kubectl_command"] = f"kubectl delete pod {_pod} -n {_ns}"
    elif _t["tool"] == "scale_deployment":
        proposal["kubectl_command"] = f"kubectl scale deployment/{_deploy} --replicas={_replicas} -n {_ns}"

# ✅ 正確 — proposal_service 優先用 kubectl_command
_kubectl = llm_proposal.get("kubectl_command", "").strip()
action = _kubectl if _kubectl else llm_proposal["action"]

# ❌ 禁止 — 只存 nemotron_tools[] 不回填 kubectl_command
proposal["nemotron_tools"] = result.get("tools", [])
# (缺少回填 → parse_operation_from_action → None → SKIP)
```

**為何重要**: `execute_approved_action` 靠 `parse_operation_from_action(approval.action)` 決定執行什麼。若 action 是中文標題或 "未知操作"，解析失敗，靜默跳過，UI 卻顯示「已批准」。

**檢查清單**:
- [ ] 新增 Tool Call 工具時，同步更新 openclaw.py 的回填邏輯
- [ ] 測試批准後 `audit_logs` 有寫入記錄
- [ ] 批准後 Telegram 有收到 reply 狀態訊息

---

### 鐵律：NVIDIA Nemotron 優先仲裁

```python
# models.json 配置
{
    "fallback_order": ["nvidia", "gemini", "ollama", "claude"],
    "tool_calling_fallback_order": ["nvidia", "gemini", "claude"]
}

# Rate Limiter 檢查 (openclaw.py)
if provider in ("nvidia", "gemini", "claude"):
    allowed, reason = await rate_limiter.check_and_increment(provider)
```

### Ollama 模型中央化 (D1, ADR-067, 2026-04-11)

**禁止**在 Service 層 hardcode Ollama 模型名稱。**必須**使用：

```python
from src.services.model_registry import get_model
model = get_model("ollama", "purpose_key")
```

| purpose key | 預設模型 | 服務 |
|------------|---------|------|
| drift_summary | qwen2.5:7b-instruct | drift_narrator_service |
| drift_intent | qwen2.5:7b-instruct | drift_interpreter |
| log_anomaly | deepseek-r1:14b | log_summary_service |
| code_review | qwen2.5-coder:7b | local_code_review_service |
| image_analysis | llava:latest | image_analysis_service |
| nemoclaw | deepseek-r1:14b | decision_manager |
| playbook_draft | qwen2.5:7b-instruct | decision_manager |
| embedding | nomic-embed-text | embedding_service, knowledge_service |

模型切換：只改 `apps/api/models.json`，重啟 Pod，不改代碼。

### 各 Provider 特性

| Provider | 成本 | 特性 | 用途 |
|----------|------|------|------|
| NVIDIA | $0 | 83.3% Tool Calling 精準度 | **優先仲裁** |
| Gemini | ~$0.001/1K | 平衡型 | 備援 |
| Ollama | $0 | 本地 CPU，高延遲 | 隱私優先 |
| Claude | ~$0.008/1K | 最強推理 | 最後備援 |

### Rate Limiter 配置

```python
# ai_rate_limiter.py
"nvidia": {"rpm": 5, "daily_requests": 100, "daily_tokens": 50_000}
"gemini": {"rpm": 10, "daily_requests": 500, "daily_tokens": 100_000}
"claude": {"rpm": 5, "daily_requests": 200, "daily_tokens": 50_000}
```

---

## 🦞 Phase 19 Terminal SSE 後端整合 (2026-03-28)

> **審查結果**: 首席架構師審查 47/50 (P0-P2 全部修復)
> **Memory**: `project_phase19_omni_terminal.md`
> **ADR**: ADR-031 (SSE 架構) + ADR-032 (GenUI 渲染)

### SSE 後端架構

```
POST /api/v1/terminal/intent → 建立 Session
GET  /api/v1/terminal/stream/{session_id} → SSE 串流
POST /api/v1/terminal/approval/{approval_id}/execute → 執行授權
GET  /api/v1/terminal/sessions/{session_id}/history → 歷史
```

### 依賴注入模式 (P0 修復)

```python
# ✅ 正確: FastAPI Depends 模式
from typing import Annotated
from fastapi import Depends
from src.services.terminal_service import TerminalService, get_terminal_service

TerminalServiceDep = Annotated[TerminalService, Depends(get_terminal_service)]

@router.post("/intent")
async def submit_intent(
    request: TerminalIntentRequest,
    service: TerminalServiceDep,  # 每次請求新實例
):
    session_id = await service.process_intent(request)

# ❌ 禁止: Global Singleton
_instance = TerminalService()  # 違反 DI 原則
```

### SSE 狀態機 (7-State 設計，10/10 分)

| 狀態 | 說明 | 轉換 |
|------|------|------|
| IDLE | 初始狀態 | → CONNECTING |
| CONNECTING | 建立連線 | → CONNECTED / ERROR |
| CONNECTED | 正常運作 | → STREAMING / ERROR |
| STREAMING | 接收數據 | → CONNECTED / COMPLETED |
| COMPLETED | 串流結束 | → IDLE |
| ERROR | 錯誤狀態 | → RECONNECTING |
| RECONNECTING | 重新連線 | → CONNECTING |

### GenUI 事件格式

```python
# SSE 事件類型
class SSEEventType(str, Enum):
    THINKING = "thinking"      # AI 思考中
    TEXT = "text"              # 文字回應
    GENUI = "genui"            # GenUI 組件
    APPROVAL = "approval"      # 待授權操作
    COMPLETE = "complete"      # 串流結束
    ERROR = "error"            # 錯誤

# GenUI 事件格式
{
    "event": "genui",
    "data": {
        "component": "ApprovalCard",
        "props": {
            "approvalId": "APR-xxx",
            "riskLevel": "high",
            "kubectl": "kubectl delete pod xxx"
        },
        "position": "inline"
    }
}
```

### 鐵律

| 規則 | 說明 |
|------|------|
| DI 模式 | 所有 Service 必須用 `Depends()` 注入，禁止 Singleton |
| Session TTL | Redis 5 分鐘 TTL |
| 重連機制 | 指數退避 + Last-Event-ID 續接 |
| Ghost Payload | 最小化：只傳 `current_page` + `entity_id` |

### Phase 19.4 API 整合模式 (2026-03-30)

> **首席架構師審查**: 94/100 OUTSTANDING
> **測試覆蓋**: 60 測試全部通過

#### 正確做法 - 透過 DI 取得 Service

```python
# ✅ 正確: 透過 DI 取得 ApprovalDBService
from src.services.approval_db import get_approval_service

async def _handle_approval_action(self, publisher, topic, ...):
    approval_service = get_approval_service()
    pending_approvals = await approval_service.get_pending_approvals()

    # 渲染 ApprovalCard
    await self._publish_render_ui(publisher, topic, "ApprovalCard", {
        "approvalId": str(approval.id)[:8].upper(),
        "riskLevel": approval.risk_level.value.upper(),
        # ...
    })
```

#### K8s 狀態查詢

```python
# ✅ 正確: 處理 K8s 不可用情況
from src.services.k8s_diagnostics import _get_k8s_client

async def _handle_status_query(self, publisher, topic, ...):
    client = await _get_k8s_client()
    if not client:
        await self._publish_thought(publisher, topic, "System", "K8s 連線失敗")
        return

    v1 = client.CoreV1Api()
    pods = await v1.list_namespaced_pod(namespace="awoooi")
```

#### 錯誤處理鐵律

```python
# ✅ 正確: 完整錯誤處理 + structlog
try:
    result = await service.some_operation()
except Exception as e:
    logger.error("operation_failed", error=str(e))
    await self._publish_tool_call(publisher, topic, "Tool", "failed",
        result={"error": str(e)[:100]}  # 截斷避免敏感資訊
    )
```

---

---

## Sprint 5.1 Service Registry 模式（ADR-062）

### 有狀態服務分級鐵律

所有自動修復決策必須先查詢 `ops/config/service-registry.yaml`：

```python
from src.services.service_registry import StatefulLevel, get_service_registry

registry = get_service_registry()
level = registry.get_stateful_level(service_name)

if level == StatefulLevel.BLOCK:
    # 直接拒絕，不進入 AI 分析
    return AutoRepairDecision(can_auto_repair=False, blocked_by="SERVICE_REGISTRY_BLOCK")
```

### Guardrail 失敗的保守原則

```python
# ✅ 正確：失敗時 block（保守，優先安全）
except Exception as e:
    logger.error("guardrail_check_failed", error=str(e))
    return AutoRepairDecision(can_auto_repair=False, blocked_by="GUARDRAIL_ERROR")

# ❌ 錯誤：失敗時放行（穿透 BLOCK 保護）
except Exception as e:
    logger.error(...)
    pass  # 繼續執行 — 違反安全原則！
```

### 新 Service 的標準樣板（首席審查教訓）

每個新建 Service **必須全部符合**：

```python
import structlog  # ✅ 不是 import logging
from src.utils.timezone import now_taipei  # ✅ 不是 datetime.now(UTC)

logger = structlog.get_logger(__name__)  # ✅ structlog

_client: MyClient | None = None

def get_my_client() -> MyClient:      # ✅ singleton
    global _client
    if _client is None:
        _client = MyClient()
    return _client

def set_my_client(c: MyClient) -> None:  # ✅ DI setter（測試注入）
    global _client
    _client = c
```

所有通知方法必須包覆 try/except，失敗只 log 不拋出：

```python
async def send_xxx_notification(self, ...) -> None:
    try:
        text = ...
        await self.send_notification(text)
    except Exception as e:
        logger.error("xxx_notify_failed", error=str(e))  # ✅ 不拋出
```

---

## 告警規則引擎 (ADR-064, 2026-04-09)

**模組**: `apps/api/src/services/alert_rule_engine.py`  
**配置**: `apps/api/alert_rules.yaml`

### 規則匹配

```python
from src.services.alert_rule_engine import match_rule
result = match_rule(alert_context)  # dict | None
# result["rule_id"] == "generic_fallback" → AI 自動學習
```

### AI 自動規則學習

命中 `generic_fallback` 時，在上層 **async** 方法觸發：

```python
asyncio.create_task(auto_generate_rule(
    alert_context,
    ollama_url=settings.OLLAMA_URL,      # DI 注入
    model=settings.OPENCLAW_DEFAULT_MODEL,
    gemini_api_key=getattr(settings, "GEMINI_API_KEY", ""),
))
```

⚠️ **禁止在 sync 方法中呼叫 asyncio.get_event_loop()** — 必須在 async 上下文用 `asyncio.create_task()`

### Priority 體系

| 範圍 | 用途 |
|------|------|
| 1–499 | 手寫規則（不被 AI 覆蓋） |
| 500–890 | AI 自動生成規則 |
| 999 | generic_fallback 通用兜底 |

### get_incident_type() — incident_type 三層推斷 (I1, 2026-04-11)

```python
from src.services.alert_rule_engine import get_incident_type
incident_type = get_incident_type(alertname)
# Layer 1: YAML rule.incident_type（需明確設定）
# Layer 2: ALERTNAME_TO_TYPE 靜態 dict（src/constants/alert_types.py，56 筆）
# Layer 3: "custom" 兜底
```

**禁止**：使用 `ALERTNAME_TO_TYPE.get(alertname, "custom")` 直接在 Router 層存取靜態 dict。
**必須**：呼叫 `get_incident_type()` 讓 YAML 規則有機會優先匹配。

**YAML rule.id ≠ incident_type**（命名空間不同）。YAML 無 `incident_type` 欄位時自動 fall through Layer 2。

### 多 Pod 限制（ADR-064 L1/L2）

`_generating` set 進程級去重，多 Pod 可能重複生成。新規則 append 後只有寫入 Pod 立即生效，其他 Pod 需重啟。

### DI 要求

`auto_generate_rule()` 透過參數接收 ollama/gemini 設定，**禁止** 在函式內 `from src.core.config import settings`。

---

## 🚀 自動修復飛輪鐵律 (ADR-068, 2026-04-10)

> **背景**: 25 個 AUTO_REPAIR_TRIGGERED 全部 NO_MATCH — 五個根因同時存在

### 1. affected_services 提取鐵律

**禁止**將 `target_resource`（可能是 IP:port 或 alertname）直接填入 `affected_services`。

```python
# ❌ 絕對禁止（污染 Jaccard 匹配）
affected_services = [target_resource]  # 可能是 "192.168.0.188:9100" 或 "HostHighCpuLoad"

# ✅ 正確 — 語意提取（在 incident_service.py）
affected_services = extract_affected_services(labels, target_resource)
# 優先序: component > job(非基礎設施) > pod(deployment name) > clean target > []
```

### 2. Signal alert_name 鐵律

```python
# ❌ 禁止 — alert_name="custom" 讓 Redis index 查詢命中零
alert_name = alert_type  # "custom"

# ✅ 正確 — 用真實 alertname label
alert_name = alertname or alert_type  # "HostHighCpuLoad"
```

### 3. Router 層業務邏輯鐵律

`create_incident_for_approval` 等含 Severity 映射、Signal 建立、Incident 建立的函數**必須**在 Service 層：

```
# ✅ 正確位置
apps/api/src/services/incident_service.py  ← create_incident_for_approval()
                                           ← extract_affected_services()

# ❌ 錯誤位置（已修正）
apps/api/src/api/v1/webhooks.py            ← 業務邏輯不屬 Router
```

### 4. Jaccard 空集合豁免鐵律

通用型基礎設施 Playbook（`affected_services=[]`，`severity_range=[]`）代表適用所有情境，**不能**因空集合被 Jaccard 打成 0：

```python
# apps/api/src/utils/similarity.py — 豁免規則
"affected_services": 1.0 if not pattern_b.affected_services else jaccard(...)
"severity": 1.0 if not pattern_b.severity_range or overlap else 0.0
```

### 5. Playbook alertname 變體鐵律

Playbook 的 `symptom_pattern.alert_names` 必須包含所有真實世界 alertname 變體：

```yaml
# apps/api/alert_rules.yaml — 每條規則都要加足變體
- id: high_cpu
  match:
    alertname:
      - HighCPUUsage          # Prometheus 規則名
      - HostHighCpuLoad       # node-exporter 變體
      - CPUThrottlingHigh     # K8s 變體
```

### 6. Embedding 持久化鐵律

Playbook 向量**必須**同時存入 Redis（熱快取）和 `playbook_embeddings`（pgvector 持久化），防止重啟後冷啟動斷層：

```python
# main.py lifespan 啟動時（非阻塞）
asyncio.create_task(ensure_playbook_embeddings_indexed())
```

Repository 層負責格式化：
```python
# ✅ 正確 — PlaybookEmbeddingRepository.upsert()
vec_str = "[" + ",".join(str(float(x)) for x in embedding) + "]"  # pgvector 安全格式

# ❌ 禁止 — str(embedding) 可能輸出帶空格的格式
```

---

## 參考文檔

- `apps/api/src/core/config.py`: 設定中心
- `apps/api/src/main.py`: FastAPI 應用入口
- `apps/api/src/plugins/mcp/mcp_bridge.py`: MCP Bridge 核心
- `apps/api/alert_rules.yaml`: 告警規則配置（新增規則只改這裡）
- `packages/lewooogo-data/`: 記憶體 Provider 積木
- `packages/lewooogo-brain/`: AI 引擎積木
- `memory/feedback_lewooogo_modular_enforcement.md`: 積木化強制執行鐵律
- `memory/feedback_incident_approval_sync.md`: Incident-Approval 同步鐵律
- ADR-001: MCP Protocol 採用
- ADR-005: BFF 閘道架構
- ADR-006: AI 備援策略
- ADR-008: Python 模組化獨立積木架構
- ADR-027: Incident-Approval 同步架構 (UnitOfWork + Saga)
- ADR-064: Alert Rule Engine — YAML 驅動 + AI 自動學習
- ADR-068: 飛輪冷啟動斷層修復 — affected_services/Jaccard/Embedding 四階段系統性根治
