docs: ADR-037 + 監控架構提案 + Runbooks

- ADR-037 監控增強架構
- MONITORING_MASTER_PLAN 主計畫
- MASTER_EXECUTION_SCHEDULE 執行排程
- Phase D/E/Worker HPA Runbooks

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
OG T
2026-03-29 16:04:08 +08:00
parent 237fb64a81
commit 89e05e6ea2
13 changed files with 6462 additions and 0 deletions

View File

@@ -0,0 +1,336 @@
# RunBook: E2E Playwright CI 定期排程設定
> **類型**: 操作型 RunBook
> **優先級**: 🔴 P0
> **建立**: 2026-03-29 12:38 (台北)
> **建立者**: Antigravity
> **工時預估**: 30 分鐘
> **前置條件**: Playwright 測試可在本機 `pnpm test:e2e` 成功執行
---
## 背景與現況
### 🔍 精確現況診斷
**已有的 12 個 E2E 測試檔案**`apps/web/tests/e2e/`
| 測試檔案 | 測試範圍 |
|---------|---------|
| `dashboard-acceptance.spec.ts` | 首頁 Dashboard 驗收 |
| `multisig-security.spec.ts` | 多重簽核安全性 |
| `approval-card-verify.spec.ts` | 簽核卡片驗證 |
| `phase11-conversational.spec.ts` | 對話式 Phase 11 功能 |
| `phase19-production-verification.spec.ts` | Phase 19 生產驗證 |
| `action-log.spec.ts` | 行動日誌 |
| `cpo102-visual.spec.ts` | 視覺截圖測試 |
| `visual-armor-upgrade.spec.ts` | 視覺升級驗證 |
| `debug-error.spec.ts` | 錯誤頁面 |
| `rbac-screenshot.spec.ts` | RBAC 截圖驗證 |
| `phase4-final-demo.spec.ts` | Phase 4 Demo |
| `phase4-timeline.spec.ts` | Phase 4 時間軸 |
**缺口**`playwright.config.ts` 已有配置,但 `.github/workflows/` 中**無定期執行排程**。
---
## Step 1: 建立 E2E 定期排程 Workflow
建立 `.github/workflows/e2e-weekly.yaml`
```yaml
name: 🎭 E2E Playwright 週期驗收
on:
# 每週一凌晨 02:30 執行(台北時間,即 UTC 18:30 週日)
schedule:
- cron: '30 18 * * 0'
# 允許手動觸發
workflow_dispatch:
inputs:
environment:
description: '測試環境 URL'
required: false
default: 'https://192.168.0.120:32335'
test_suite:
description: '測試套件 (all / smoke / visual)'
required: false
default: 'all'
jobs:
e2e-test:
name: 🎭 E2E Playwright (${{ matrix.browser }})
runs-on: [self-hosted, harbor] # 使用 .110 的 GitHub Runner
strategy:
fail-fast: false # 一個瀏覽器失敗不影響其他
matrix:
browser: [chromium, firefox]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Install Playwright Browsers
run: pnpm --filter web exec playwright install --with-deps ${{ matrix.browser }}
- name: 🎭 Run E2E Tests
run: |
cd apps/web
pnpm exec playwright test \
--project=${{ matrix.browser }} \
--reporter=html \
${SUITE_FILTER}
env:
SUITE_FILTER: ${{ github.event.inputs.test_suite == 'smoke' && '--grep @smoke' || '' }}
BASE_URL: ${{ github.event.inputs.environment || 'http://192.168.0.120:32335' }}
CI: true
- name: 📁 Upload Test Report
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report-${{ matrix.browser }}-${{ github.run_id }}
path: apps/web/playwright-report/
retention-days: 14
- name: 📸 Upload Screenshots on Failure
uses: actions/upload-artifact@v4
if: failure()
with:
name: e2e-screenshots-${{ matrix.browser }}-${{ github.run_id }}
path: apps/web/test-results/
retention-days: 7
- name: 🚨 Notify Telegram on Failure
if: failure()
run: |
curl -s -X POST "https://api.telegram.org/bot${TG_BOT_TOKEN}/sendMessage" \
-d chat_id="${TG_CHAT_ID}" \
-d parse_mode="HTML" \
-d text="🎭 <b>E2E 週期測試失敗</b>
瀏覽器:${{ matrix.browser }}
觸發:${{ github.event_name }}
時間:$(TZ='Asia/Taipei' date '+%Y-%m-%d %H:%M:%S')
報告:${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
🔍 請立即調查 UI 回歸問題"
env:
TG_BOT_TOKEN: ${{ secrets.OPENCLAW_TG_BOT_TOKEN }}
TG_CHAT_ID: ${{ secrets.OPENCLAW_TG_CHAT_ID }}
# 視覺截圖比對工作(獨立 job只在排程時執行
visual-regression:
name: 📸 視覺回歸比對
runs-on: [self-hosted, harbor]
if: github.event_name == 'schedule' # 只在定期排程時執行
needs: e2e-test
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install pnpm & deps
run: |
pnpm install --frozen-lockfile
pnpm --filter web exec playwright install --with-deps chromium
- name: 📸 Generate Visual Snapshots
run: |
cd apps/web
pnpm exec playwright test \
--project=chromium \
--grep @visual \
--reporter=html \
--update-snapshots=missing # 新截圖自動建立 baseline
env:
BASE_URL: 'http://192.168.0.120:32335'
CI: true
- name: 📁 Upload Visual Snapshots
uses: actions/upload-artifact@v4
with:
name: visual-snapshots-${{ github.run_id }}
path: apps/web/tests/e2e/__snapshots__/
retention-days: 30
- name: 🚨 Notify Telegram on Visual Regression
if: failure()
run: |
curl -s -X POST "https://api.telegram.org/bot${TG_BOT_TOKEN}/sendMessage" \
-d chat_id="${TG_CHAT_ID}" \
-d parse_mode="HTML" \
-d text="📸 <b>視覺回歸測試失敗</b>
AWOOOI 前端出現視覺變化!
時間:$(TZ='Asia/Taipei' date '+%Y-%m-%d %H:%M:%S')
請統帥審查截圖比對報告:
${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
env:
TG_BOT_TOKEN: ${{ secrets.OPENCLAW_TG_BOT_TOKEN }}
TG_CHAT_ID: ${{ secrets.OPENCLAW_TG_CHAT_ID }}
```
---
## Step 2: 標記現有測試為 Smoke / Visual 類別
```typescript
// 在各 spec 檔案第一行測試加上標籤:
// dashboard-acceptance.spec.ts核心功能標記為 @smoke
test.describe('Dashboard Acceptance @smoke', () => { ... });
// cpo102-visual.spec.ts視覺截圖標記為 @visual
test.describe('Visual Regression @visual', () => {
test('CPO-102 首頁視覺', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveScreenshot('dashboard-baseline.png', {
fullPage: true,
threshold: 0.02 // 允許 2% 像素差異
});
});
});
```
**建議標記方式**
| 標籤 | 包含測試 | 執行頻率 |
|------|---------|---------|
| `@smoke` | dashboard, approval, action-log | 每次 CD 後 + 每週 |
| `@visual` | cpo102-visual, visual-armor | 只在每週排程 |
| (無標籤) | 所有其他測試 | 只在每週排程 |
---
## Step 3: 部署並驗證
```bash
# 1. 提交 Workflow 檔案
git add .github/workflows/e2e-weekly.yaml
git commit -m "feat(ci): add weekly E2E Playwright schedule with Telegram failure notification"
git push origin main
# 2. 手動觸發測試(確認 Workflow 運作正常)
gh workflow run e2e-weekly.yaml \
-f environment="http://192.168.0.120:32335" \
-f test_suite="smoke"
# 3. 監控 Workflow 執行
gh run watch
# 4. 確認 Telegram Bot 收到失敗通知(刻意讓一個測試失敗)
```
---
## 驗收標準
| 項目 | 通過條件 |
|------|---------|
| Workflow 存在 | `.github/workflows/e2e-weekly.yaml` 成功 push |
| 手動觸發正常 | `gh workflow run` 可執行且完成 |
| Smoke 測試通過 | `@smoke` 標籤測試全部 PASS |
| 失敗通知正常 | Telegram Bot 收到失敗訊息 |
| 報告上傳 | GitHub Actions Artifacts 中有 `playwright-report-*` |
---
## ⚠️ 架構安全補丁2026-03-29 更新,部署前必讀)
> 來源:`ARCHITECTURAL_RISK_WAR_GAME.md` 深度沙盤推演,代碼確認級別
### 補丁 1playwright.config.ts 必須加入 ignoreHTTPSErrors
**問題**:內網 K3s 使用自簽憑證Self-signed certPlaywright 連接 `https://192.168.0.120:32335` 時會遭遇 `NET::ERR_CERT_AUTHORITY_INVALID`,導致**所有測試在第一步就失敗**。
**修復**`apps/web/playwright.config.ts`
```typescript
export default defineConfig({
use: {
baseURL: process.env.BASE_URL || 'http://192.168.0.120:32335',
ignoreHTTPSErrors: true, // 🆕 必須加入,否則自簽憑證全面阻擋
viewport: { width: 1280, height: 720 },
deviceScaleFactor: 1, // 🆕 防止 Retina 螢幕 DPI 差異影響截圖比對
},
expect: {
toHaveScreenshot: {
threshold: 0.05, // 🆕 允許 5% 差異(吸收跨平台字體渲染微差)
maxDiffPixelRatio: 0.05,
},
},
});
```
---
### 補丁 2Visual Baseline 必須在 DockerLinux 環境)中產生
**問題**MacCoreText 渲染)與 GitHub Actions CILinux FreeType 渲染)的字體像素不同。若在 Mac 本機產生 baselineCI 比對時**100% 誤報失敗**。
> 🔴 **絕對禁止在本機 Mac 環境執行 `--update-snapshots`**
**正確的 Baseline 更新流程**
```bash
# 在 Mac 本機執行,但透過 DockerLinux 環境)產生截圖
cd apps/web
docker run --rm \
-v $(pwd):/work -w /work \
-p 3000:3000 \
mcr.microsoft.com/playwright:v1.44.0-jammy \
pnpm exec playwright test \
--update-snapshots \
--project=chromium \
--grep @visual
# Docker 產生的 .png 自動存入 tests/e2e/__snapshots__/
# 提 PR標注 📸 VISUAL_UPDATE
# 統帥視覺審核截圖後方可合併
```
**加入 package.json scripts**
```json
{
"scripts": {
"test:visual": "playwright test --project=chromium --grep @visual",
"test:visual:update": "docker run --rm -v $(pwd):/work -w /work mcr.microsoft.com/playwright:v1.44.0-jammy pnpm exec playwright test --update-snapshots --project=chromium --grep @visual"
}
}
```
---
### 補丁 3CI Threshold 需與本 RunBook Step 2 標籤保持一致
Visual 測試中 `threshold: 0.02`Step 2 示例代碼)與 `playwright.config.ts` 全局設定 `0.05` 會以**個別設定優先**。建議統一為:
```typescript
// 全局playwright.config.ts
threshold: 0.05 // 寬鬆(跨平台環境差異)
// 個別敏感組件(*.spec.ts
await expect(page).toHaveScreenshot('component.png', {
threshold: 0.02 // 嚴格(關鍵組件精確比對)
});
```

View File

@@ -0,0 +1,499 @@
# RunBook: 前端 UI/UX 核心痛點徹底解決方案
> **類型**: 架構設計 + 實施 RunBook
> **優先級**: 🔴 P0 (核心競爭力)
> **建立**: 2026-03-29 12:38 (台北)
> **建立者**: Antigravity
> **核心命題**: 讓 AWOOOI 的前端「讓人看一眼就驚艷,用一次就上癮」
---
## 一、Claude Code 前端弱點診斷
### 1.1 五大結構性弱點
| 弱點 | 後果 | 解法 |
|------|------|------|
| **視覺感知盲區** | 代碼語法正確,但視覺效果差 | Playwright 截圖 + 統帥視覺主控 |
| **CSS 語境失憶** | 改 A 壞 B不知道全局 CSS 影響 | Storybook 隔離組件 + TypeCheck |
| **動畫設計無感** | 150ms 快閃 vs 300ms 遲滯無法感知 | 在 Storybook Story 中定義動畫標準 |
| **設計意圖推斷不足** | Nothing.tech 審美需要視覺參照 | 每個組件附帶規格截圖 |
| **Safari 兼容性盲點** | `backdrop-blur` 在 Safari 有 bug | Playwright multi-browser E2E |
### 1.2 技術債現況(截至 2026-03-29
```
i18n 違規40+ 處TECHNICAL_DEBT_PHASE2.md
shadcn/ui 殘留:已部分廢除,但需全面確認
GenUI Registry只有基礎 5 張卡片,缺少監控類
Knowledge Base頁面空白無後端
Omni-Terminal外殼完整SSE 事件類型未完全對接
```
---
## 二、九大行動詳細實施計畫
### 🔴 行動 1: i18n 閃電清零工時4h
**策略:一次性掃描→批量修復,非逐一處理**
```bash
# Step 1.1: 自動掃描所有硬編碼字串
cd /Users/ogt/awoooi/apps/web
# 掃描 TSX 中的英文硬編碼(排除技術識別符)
grep -rn '"[A-Z][A-Z]' src/ --include="*.tsx" --include="*.ts" | \
grep -v "//.*\"" | \
grep -v "className=" | \
grep -v "href=" | \
grep -v "id=" | \
grep -v "import " > /tmp/en_violations.txt
# 掃描中文硬編碼
grep -rn "\"[^\x00-\x7F]" src/ --include="*.tsx" | \
grep -v "//.*\"" > /tmp/zh_violations.txt
echo "英文違規:$(wc -l < /tmp/en_violations.txt)"
echo "中文違規:$(wc -l < /tmp/zh_violations.txt)"
```
**已知 P0 違規快速修復清單**(根據 TECHNICAL_DEBT_PHASE2.md
```typescript
// ❌ agent/data-pincer.tsx:50-78需修復
// 現在:
const statuses = {
standby: 'STANDBY',
analyzing: 'ANALYZING',
executing: 'EXECUTING',
awaiting: 'AWAITING APPROVAL',
error: 'ERROR'
}
// ✅ 修復後:
const statuses = {
standby: t('status.standby'),
analyzing: t('status.analyzing'),
executing: t('status.executing'),
awaiting: t('status.awaitingApproval'),
error: t('status.error')
}
```
```typescript
// ❌ status-orb.tsx:16-31
// 現在:
const STATUS_TEXT = {
idle: 'Idle',
thinking: 'Thinking',
executing: 'Executing',
awaiting: 'Awaiting Approval'
}
// ✅ 修復後:
const STATUS_TEXT = {
idle: t('status.idle'),
thinking: t('status.thinking'),
executing: t('status.executing'),
awaiting: t('status.awaitingApproval')
}
```
**需要同步更新的字典檔**
```json
// apps/web/messages/zh-TW.json追加
{
"status": {
"idle": "待機",
"thinking": "分析中",
"executing": "執行中",
"awaitingApproval": "等待核准",
"error": "錯誤",
"standby": "待機",
"analyzing": "分析中"
}
}
```
```json
// apps/web/messages/en.json追加
{
"status": {
"idle": "Idle",
"thinking": "Thinking",
"executing": "Executing",
"awaitingApproval": "Awaiting Approval",
"error": "Error",
"standby": "Standby",
"analyzing": "Analyzing"
}
}
```
**驗收指令(必須通過)**
```bash
cd apps/web
# 確認無中文硬編碼
if grep -rn '"[^\x00-\x7F]' src/ --include="*.tsx" | grep -v "//"; then
echo "❌ 仍有中文硬編碼!"
exit 1
else
echo "✅ 中文硬編碼清零"
fi
# TypeScript 編譯驗證workflow 硬規則)
pnpm exec tsc --noEmit
```
---
### 🔴 行動 2: Storybook 組件庫建立工時8h
**這是解決 AI 視覺感知盲區的根本方案**
```bash
# Step 2.1: 安裝 Storybook
cd apps/web
pnpm add -D storybook@latest @storybook/nextjs @storybook/addon-essentials \
@storybook/addon-interactions @storybook/test
npx storybook@latest init --builder webpack5
# Step 2.2: 配置 Nothing.tech 主題
```
**必須上架的 10 個核心組件 Story**
| 組件 | Nothing.tech 規格 | Story 狀態 |
|------|-----------------|-----------|
| `GlassCard` | `bg-white/70 backdrop-blur-[20px] border border-black/[0.06]` | Loading / Content / Error |
| `StatusOrb` | 燈號 + `animate-ping`critical 時)| idle / thinking / executing / critical |
| `ApprovalCard` | 1.0 ConversationalView 風格 | LOW / MEDIUM / HIGH / CRITICAL |
| `OmniTerminal` | VT323 字體 + 綠色游標閃爍 | empty / thinking / streaming / error |
| `HostCard` | CPU/Memory 橫條 + 脈搏點 | healthy / warning / critical |
| `MetricsCard` | 數字大字 + 趨勢箭頭 | up / down / stable |
| `SystemHealthCard` | 燈號矩陣 5x5 | all-healthy / some-warning / critical |
| `FinOpsCard` | 成本分解 + 可省金額 | monthly / quarterly |
| `SLOCard` | 達成率 + 趨勢 | healthy / at-risk / breached |
| `AnomalyFrequencyCard` | 頻率統計 + 升級建議 | normal / repeat / escalate |
**Storybook Story 範例**GlassCard
```typescript
// apps/web/src/components/ui/glass-card.stories.ts
import type { Meta, StoryObj } from '@storybook/react';
import { GlassCard } from './glass-card';
const meta: Meta<typeof GlassCard> = {
title: 'AWOOOI/UI/GlassCard',
component: GlassCard,
parameters: {
// Nothing.tech 白底背景
backgrounds: {
default: 'nothing-white',
values: [{ name: 'nothing-white', value: '#F5F5F0' }],
},
// 規格文件截圖
docs: {
description: {
component: `
Nothing.tech 白玻璃卡片。固定規格:
- bg: bg-white/70
- blur: backdrop-blur-[20px]
- border: border border-black/[0.06]
- radius: rounded-xl
`,
},
},
},
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof GlassCard>;
export const Default: Story = {
args: { children: '玻璃卡片內容' }
};
export const WithCriticalBorder: Story = {
args: {
children: '緊急狀態',
className: 'border-status-critical border-2'
}
};
```
---
### 🔴 行動 3: AI 視覺審查 SOP工時2h 建立,後續 0 工時)
**建立標準作業程序,讓 AI 自主截圖並等待統帥審核:**
```markdown
## AI 前端修改 SOP強制執行
### 修改前
1. 查閱 Storybook 對應組件的規格 Story
2. 確認 Nothing.tech 視覺 Token詳見 tailwind.config.ts
### 修改中
3. 修改代碼
4. 執行 `pnpm exec tsc --noEmit`(語法驗證)
### 修改後(🆕 新增強制步驟)
5. 啟動 Dev Server`pnpm dev`
6. 執行截圖腳本:
```bash
cd apps/web
pnpm exec playwright screenshot \
--browser chromium \
http://localhost:3000 \
docs/screenshots/$(date +%Y%m%d-%H%M)/homepage.png
```
7. 截圖存至 `docs/screenshots/{date}/{component}.png`
8. 在 LOGBOOK 記錄:「已截圖,視覺存檔 docs/screenshots/xxx/yyy.png」
9. 等待統帥視覺審批後方可 commit
```
---
### 🟠 行動 4: Omni-Terminal 後端全接通工時8h
根據 `ADR-031``AWOOOI_AGENTIC_WORKSPACE_ROADMAP.md` 的神經連接藍圖:
#### 4.1 三大 SSE 事件類型定義
```python
# apps/api/src/api/v1/terminal.py
# 擴充現有 SSE 端點
class SSEEventType(str, Enum):
THOUGHT = "thought" # Agent 思考流
TOOL_CALL = "tool_call" # 工具調用(含微動畫觸發信號)
TOOL_RESULT = "tool_result" # 工具結果
RENDER_UI = "render_ui" # 動態渲染 GenUI 組件
STREAM_END = "stream_end" # 思考流結束
# 範例事件:
async def stream_terminal_response(command: str):
# 1. 思考流
yield f"event: thought\ndata: {json.dumps({'text': '[Investigator] 分析指令...'})}\n\n"
# 2. 工具調用(觸發前端 CSS 動畫)
yield f"event: tool_call\ndata: {json.dumps({'tool': 'kubectl_get', 'args': {'resource': 'pod', 'namespace': 'awoooi-prod'}})}\n\n"
# 3. 工具結果
yield f"event: tool_result\ndata: {json.dumps({'pods': [...]})}\n\n"
# 4. 渲染 GenUI 卡片(前端動態載入組件)
yield f"event: render_ui\ndata: {json.dumps({'component': 'SystemHealthCard', 'props': {...}})}\n\n"
# 5. 結束
yield f"event: stream_end\ndata: {json.dumps({'success': True})}\n\n"
```
#### 4.2 前端 SSE 事件處理器(關鍵)
```typescript
// apps/web/src/hooks/useTerminalSSE.ts
// 修改現有 SSE hook增加 tool_call 動畫觸發
const useTerminalSSE = (commandId: string) => {
const [state, setState] = useTerminalStore();
useEffect(() => {
const es = new EventSource(`/api/v1/terminal/stream/${commandId}`);
// 思考流
es.addEventListener('thought', (e) => {
const data = JSON.parse(e.data);
setState(s => ({ ...s, thoughts: [...s.thoughts, data.text] }));
});
// 工具調用 → 觸發微動畫
es.addEventListener('tool_call', (e) => {
const data = JSON.parse(e.data);
setState(s => ({
...s,
activeToolCall: data.tool, // UI 顯示「正在執行 kubectl_get...」
isAnimating: true
}));
});
// GenUI 動態渲染(核心功能!)
es.addEventListener('render_ui', (e) => {
const { component, props } = JSON.parse(e.data);
setState(s => ({
...s,
renderedCards: [...s.renderedCards, { component, props }]
}));
});
es.addEventListener('stream_end', () => {
setState(s => ({ ...s, isStreaming: false, isAnimating: false }));
es.close();
});
return () => es.close();
}, [commandId]);
};
```
---
### 🟠 行動 5: GenUI Registry 擴充工時8h
新增 5 張監控類 GenUI 卡片(詳細規格見 `MONITORING_ARCHITECTURE_DEEP_DIVE.md`
```typescript
// apps/web/src/components/genui/registry.ts
export const GENUI_COMPONENTS = {
// 現有:
'MetricsCard': () => import('./cards/MetricsCard'),
'K8sPodCard': () => import('./cards/K8sPodCard'),
// 🆕 監控類Wave M-3
'SystemHealthCard': () => import('./monitoring/SystemHealthCard'),
'ServiceDetailCard': () => import('./monitoring/ServiceDetailCard'),
'FinOpsCard': () => import('./monitoring/FinOpsCard'),
'SLODashboardCard': () => import('./monitoring/SLODashboardCard'),
'AlertChainStatusCard': () => import('./monitoring/AlertChainStatusCard'),
'AnomalyFrequencyCard': () => import('./monitoring/AnomalyFrequencyCard'),
'MTTRCard': () => import('./monitoring/MTTRCard'),
}
```
---
## 三、前端改善路線圖時間表
```
📅 本週(立即):
[4h] i18n 閃電清零(一次性全修)
[2h] AI 視覺審查 SOP 建立(.awoooi-agent-rules.md 追加)
📅 Week 2-3
[8h] Storybook 10 個核心組件 Story
[8h] Omni-Terminal 後端全接通(三種 SSE 事件)
📅 Week 4-5
[8h] 監控 GenUI 卡片擴充7 張新卡片)
[8h] Nexus 頁面 AI 自治率 UI 組件
📅 Month 2
[16h] Knowledge Base 後端 + 前端完整建設
[8h] Visual Regression Testing CI 整合
📅 Month 3Phase 4 視覺靈魂注入):
[?h] 品牌 3D 資產 + Q 版 OpenClaw
[?h] 全站微動畫升級150ms 快閃標準)
[?h] Nothing.tech 認證級別的設計審計
```
---
## 四、強制驗收標準
每次前端 PR 合併前,必須通過以下全部驗收:
```bash
# 1. TypeScript 無錯誤(前端美學 Workflow 硬規則)
cd apps/web && pnpm exec tsc --noEmit
# 2. i18n 無硬編碼CI 攔截)
[ -z "$(grep -rn '"[^\x00-\x7F]' src/ --include='*.tsx')" ] || exit 1
# 3. Storybook 可正常 build確保組件獨立可用
pnpm storybook build
# 4. E2E Smoke 測試通過
pnpm exec playwright test --grep @smoke
# 5. 截圖存檔AI 執行,統帥視覺審批)
pnpm exec playwright screenshot http://localhost:3000 docs/screenshots/pr-{NUMBER}/homepage.png
# 6. Build 成功(生產環境兼容)
pnpm run build
```
---
## ⚠️ 架構安全補丁2026-03-29 更新,行動 1 開始前必讀)
> 來源:`ARCHITECTURAL_RISK_WAR_GAME.md` 深度沙盤推演,代碼確認級別
### 補丁Feature Freeze 前必須建立 release/v1.x 穩定分支
**問題**:行動 1i18n 閃電清零)需要全域替換 `src/` 底下數千行代碼,`main` 分支會進入持續 3-5 天的「混沌狀態」。若此期間生產爆發 P0 Bug如簽核按鈕失效**無法切出乾淨的 Hotfix 分支**,導致修復與重構互相衝突。
> 🔴 **行動 1 開始前,必須先建立 `release/v1.x` 穩定分支!**
#### 必須在行動 1 之前執行
```bash
# Step 0行動 1 前置):建立穩定基準分支
git checkout main
git pull origin main
git checkout -b release/v1.x
git push origin release/v1.x
# 在 GitHub 設定 release/v1.x 為 Protected Branch
# Settings → Branches → Add branch protection rule → release/v1.x
# ✅ Require pull request reviews (1 approver)
# ✅ Do not allow bypassing the above settings
```
#### 正確的行動順序
```
Step 0建立 release/v1.x🆕 前置步驟,必須先做)
Step 1宣佈 Frontend Feature Freeze禁止非 i18n PR 合併前端代碼)
Step 2i18n 閃電清零(在 fix/i18n-zero-violation 分支進行)
Step 3PR 合併到 main → 解除 Frontend Freeze
Step 4ESLint i18n Plugin 切換為 error 模式
Step 5繼續行動 2-9Storybook、Terminal 等)
```
#### Freeze 期間 P0 Hotfix 緊急流程
```bash
# 情境簽核按鈕在生產失效i18n 清零正在進行中
# 1. 從穩定基準切出 hotfix不影響 i18n 重構)
git checkout release/v1.x
git checkout -b hotfix/fix-approval-button
# 2. 最小化修復(只改 Bug不動其他代碼
# ... 修復代碼 ...
git add . && git commit -m "fix: approval button not responding on mobile"
# 3. PR 到 release/v1.x → CD 直接部署 release 分支
git push origin hotfix/fix-approval-button
# → PR 合併到 release/v1.x
# → 觸發 CD 部署CD 配置需支援 release/* 分支)
# 4. Cherry-pick 到 main不中斷 i18n 重構)
git checkout main
git cherry-pick <hotfix-commit-hash>
# 若有衝突:手動解決後繼續
```
#### Hotfix 觸發條件(須加入 HARD_RULES.md
```
P0 Hotfix 判定標準(任一條件符合即觸發):
□ 統帥無法使用核心功能簽核按鈕、登入、Telegram 通知)
□ Sentry P0 Error 每分鐘 > 10 次
□ 服務 availability < 99%(監控頁面顯示)
□ OpenClaw 決策鏈完全中斷超過 5 分鐘
```

View File

@@ -0,0 +1,264 @@
# RunBook: Phase D — Sentry Comment 回寫啟動指南
> **類型**: 操作型 RunBook
> **優先級**: 🔴 P0功能框架已建只缺 Token 配置)
> **建立**: 2026-03-29 12:35 (台北)
> **建立者**: Antigravity
> **工時預估**: 1.52 小時
> **前置條件**: AWOOOI API 正常運行 (`/api/v1/health` 返回 200)
---
## 背景與現況
### 🔍 精確現況診斷
`sentry_webhook.py``post_sentry_comment()` 函式已實作完整邏輯:
```
sentry_webhook.py:251 → 呼叫 post_sentry_comment()
sentry_service.py:206 → post_issue_comment() 已實作 POST /api/0/issues/{id}/comments/
sentry_service.py:223 → 若 SENTRY_AUTH_TOKEN 為空,直接 return None 並 warning
```
**唯一阻塞點**`settings.SENTRY_AUTH_TOKEN` 環境變數未設定,導致 comment 靜默跳過。
### 資料流確認
```
Sentry Issue 觸發
/api/v1/webhooks/sentry/error (sentry_webhook.py)
analyze_and_comment() [Background Task]
call_openclaw_analyzer() → OpenClaw AI 分析
create_sentry_approval() → 建立 Approval ✅ 已運作
send_sentry_telegram_alert()→ Telegram 通知 ✅ 已運作
post_sentry_comment() → ❌ SENTRY_AUTH_TOKEN 缺失,靜默跳過
```
---
## Step 1: 取得 Sentry API Token
### 1.1 登入 Sentry 後台
```
瀏覽器開啟http://192.168.0.110:9000
帳號:參見 docs/security/SECRETS_REFERENCE.md
```
### 1.2 建立 API Token
```
路徑:設定 → API → Auth Tokens → Create New Token
權限設定:
☑ project:read
☑ project:write
☑ issues:write ← 必須勾選,否則無法回寫 comment
☑ event:read
Token 名稱建議awoooi-openclaw-comment-writer
```
### 1.3 記錄 Token請勿存入代碼庫
```bash
# 暫存到環境變數(本機測試用)
export SENTRY_AUTH_TOKEN="sentry_xxx..."
# 驗證 Token 有效性
curl -s http://192.168.0.110:9000/api/0/organizations/ \
-H "Authorization: Bearer $SENTRY_AUTH_TOKEN" | python3 -m json.tool | head -20
# 預期看到 organization 列表,無 401
```
---
## Step 2: 注入 GitHub SecretsCD 自動化)
### 2.1 加入 GitHub Repository Secrets
```
路徑GitHub → owenhytsai/awoooi → Settings → Secrets → Actions
新增以下 Secrets
名稱SENTRY_AUTH_TOKEN
值:步驟 1.2 取得的 Token
```
### 2.2 更新 K8s Secret手動注入生產環境
```bash
# 在 192.168.0.120K3s Master執行
kubectl patch secret awoooi-secrets -n awoooi-prod \
--patch="{\"data\":{\"SENTRY_AUTH_TOKEN\":\"$(echo -n 'YOUR_TOKEN' | base64)\"}}"
# 驗證
kubectl get secret awoooi-secrets -n awoooi-prod -o jsonpath='{.data.SENTRY_AUTH_TOKEN}' | base64 -d
```
### 2.3 更新 k8s/awoooi-prod/03-secrets.yaml模板
```yaml
# k8s/awoooi-prod/03-secrets.yaml
# 新增以下欄位(使用 CD 自動注入,非硬編碼)
stringData:
# ... 現有欄位 ...
SENTRY_AUTH_TOKEN: "${SENTRY_AUTH_TOKEN}" # CD 自動注入
```
---
## Step 3: 更新 CD Workflow 自動注入
```yaml
# .github/workflows/cd.yaml
# 在 "Inject K8s Secrets" 步驟中新增 SENTRY_AUTH_TOKEN
- name: Inject K8s Secrets
run: |
kubectl patch secret awoooi-secrets -n awoooi-prod \
--patch="{\"data\":{
\"OPENCLAW_TG_BOT_TOKEN\":\"$(echo -n '${{ secrets.OPENCLAW_TG_BOT_TOKEN }}' | base64)\",
\"OPENCLAW_TG_CHAT_ID\":\"$(echo -n '${{ secrets.OPENCLAW_TG_CHAT_ID }}' | base64)\",
\"SENTRY_AUTH_TOKEN\":\"$(echo -n '${{ secrets.SENTRY_AUTH_TOKEN }}' | base64)\"
}}"
```
---
## Step 4: 驗證 Sentry Comment 功能
### 4.1 本地單元測試(快速驗證)
```bash
cd /Users/ogt/awoooi
source .env
# 設定測試 Token
export SENTRY_AUTH_TOKEN="你的真實 Token"
export SENTRY_SELF_HOSTED_URL="http://192.168.0.110:9000"
# 用 Python 直接測試 SentryService
python3 -c "
import asyncio
import sys
sys.path.insert(0, 'apps/api/src')
from services.sentry_service import SentryService
async def test():
svc = SentryService(
base_url='http://192.168.0.110:9000',
auth_token='$SENTRY_AUTH_TOKEN'
)
# 先列出 Issues 找一個真實 ID
issues = await svc.list_issues(project='awoooi-api', limit=3)
if issues:
issue_id = issues[0]['id']
print(f'找到 Issue: {issue_id}')
result = await svc.post_issue_comment(
issue_id=issue_id,
text='🤖 **AWOOOI 測試** - Sentry Comment 回寫功能正常運作。'
)
print(f'Comment 結果: {result}')
else:
print('無 Issue 可測試')
asyncio.run(test())
"
```
### 4.2 E2E 端對端驗證(生產環境)
```bash
# 1. 在 Sentry 手動觸發一個測試 Issue
curl -X POST http://localhost:8000/api/v1/webhooks/sentry/error \
-H "Content-Type: application/json" \
-d '{
"action": "triggered",
"data": {
"issue": {
"id": "TEST-001",
"title": "AWOOOI Comment 功能測試",
"level": "error",
"culprit": "test.py:1",
"firstSeen": "2026-03-29T12:00:00Z",
"count": 1,
"project": {"slug": "awoooi-api"}
},
"event": {
"message": "這是一個測試錯誤",
"platform": "python"
}
}
}'
# 預期回應
# {"status": "accepted", "issue_id": "TEST-001", "message": "Analysis scheduled"}
# 2. 等待 60 秒後OpenClaw 分析需時)
sleep 60
# 3. 到 Sentry UI 的 Issue 中確認是否有 AI 分析 Comment
# http://192.168.0.110:9000/organizations/sentry/issues/TEST-001/
```
### 4.3 確認日誌
```bash
# 查看 API Pod 日誌
kubectl logs -n awoooi-prod \
$(kubectl get pod -n awoooi-prod -l app=awoooi-api -o name | head -1) \
--tail=50 | grep -i sentry_comment
# 預期看到
# sentry_comment_posted issue_id=xxx comment_id=12345
```
---
## Step 5: 部署與驗收
```bash
# 觸發 CD
git add k8s/awoooi-prod/03-secrets.yaml .github/workflows/cd.yaml
git commit -m "feat(sentry): enable comment write-back via SENTRY_AUTH_TOKEN injection"
git push origin main
# 確認 CD 成功
gh run list --workflow=cd.yaml --limit 1
# 確認 Pod 有新 Token
kubectl exec -n awoooi-prod \
$(kubectl get pod -n awoooi-prod -l app=awoooi-api -o name | head -1) \
-- env | grep SENTRY_AUTH_TOKEN
```
---
## 驗收標準
| 項目 | 通過條件 |
|------|---------|
| K8s Secret 已注入 | `kubectl get secret` 確認 `SENTRY_AUTH_TOKEN` 不為空 |
| Token 有效 | Sentry API `/api/0/organizations/` 返回 200 |
| Comment 回寫 | Sentry Issue 中有「AI 錯誤分析」Comment |
| 日誌正常 | `sentry_comment_posted` 日誌出現,無 `sentry_comment_failed` |
| 頻率統計 | Comment 含「頻率統計」表格24h 次數 > 1 時顯示)|
---
## 常見問題排除
| 症狀 | 診斷指令 | 解法 |
|------|---------|------|
| `sentry_comment_skipped` 日誌 | `env \| grep SENTRY_AUTH_TOKEN` | Secret 未注入,重跑 Step 3 |
| `sentry_api_unauthorized` | 手動 curl Sentry API | Token 權限不足,重新建立 |
| `sentry_api_timeout` | `curl -v http://192.168.0.110:9000/` | Sentry 服務本身異常 |
| OpenClaw 分析失敗 | `curl http://192.168.0.188:8089/health` | OpenClaw 服務需重啟 |

View File

@@ -0,0 +1,233 @@
# RunBook: Phase E — SignOz Webhook Handler 生產部署
> **類型**: 操作型 RunBook
> **優先級**: 🔴 P0
> **建立**: 2026-03-29 12:35 (台北)
> **建立者**: Antigravity
> **工時預估**: 1.52 小時
> **前置條件**: SignOz UI 可在 http://192.168.0.188:3301 正常訪問
---
## 背景與現況
### 🔍 精確現況診斷
```
signoz_webhook.py → 完整實作 (363 行,含 4 步驟完整流程)
main.py:419 → 已正確路由 include_router(signoz_webhook_v1.router)
端點POST /api/v1/webhooks/signoz/alert ✅ 已可接收
問題SignOz 告警規則未指向此 Webhook
```
**唯一阻塞點**SignOz 告警規則 (`ops/signoz/alerting/rules.yaml`) 的 `webhook` 欄位尚未設定或未部署到 SignOz 主機。
### 完整資料流
```
SignOz 偵測到異常 (Error Rate / Latency / No Traces)
SignOz Alert Manager 觸發告警
POST http://192.168.0.120:32334/api/v1/webhooks/signoz/alert ← 需要配置
process_signoz_alert() [Background Task]
├── AnomalyCounter 記錄頻率 (ADR-037) ✅
├── IncidentService 建立事件 ✅
├── ApprovalService 建立簽核 ✅
└── TelegramGateway 發送通知 ✅
```
---
## Step 1: 確認 API 端點可達
```bash
# 從 188 主機測試 SignOz Webhook 端點
curl -s http://192.168.0.120:32334/api/v1/webhooks/signoz/health
# 預期:{"status": "ok", "service": "signoz-webhook", "timestamp": "..."}
# 如端點不通,確認 Pod 狀態
kubectl get pod -n awoooi-prod -l app=awoooi-api
```
---
## Step 2: 設定 SignOz 告警規則
### 2.1 確認 ops/signoz/alerting/rules.yaml 已建立
```bash
# 確認檔案存在
ls /Users/ogt/awoooi/ops/signoz/alerting/
# 如不存在,從 IMPLEMENTATION_STEPS_REMAINING_PHASES.md 的 Phase E 代碼複製
```
### 2.2 部署告警規則到 SignOz 主機
```bash
# 登入 SignOz 主機
ssh root@192.168.0.188
# 確認 SignOz 告警配置目錄
docker inspect signoz-query-service | grep -A5 "Mounts"
# 常見路徑:/opt/signoz/config/ 或 /data/signoz/
# 複製告警規則(從本機)
# 先在本機執行:
scp /Users/ogt/awoooi/ops/signoz/alerting/rules.yaml \
root@192.168.0.188:/opt/signoz/config/alerting/
# 在 188 主機重啟 SignOz Alert Manager不重啟整個 SignOz
docker restart signoz-alert-manager 2>/dev/null || \
docker restart signoz # 若是單容器部署
```
### 2.3 透過 SignOz API 驗證規則載入
```bash
# 在 188 主機執行
curl -s http://localhost:3301/api/v3/alerts/rules | python3 -m json.tool | head -40
# 預期看到 APIHighErrorRate, APIHighLatencyP99 等規則名稱
```
---
## Step 3: 設定 SignOz Webhook Channel
SignOz 告警通知支援 Webhook Channel需要透過 SignOz Web UI 或 API 設定。
### 3.1 透過 SignOz UI 設定(推薦)
```
瀏覽器開啟http://192.168.0.188:3301
路徑Settings → Alert Channels → New Channel
類型Webhook
名稱AWOOOI-API
URLhttp://192.168.0.120:32334/api/v1/webhooks/signoz/alert
Send resolved notifications☑ (可選)
```
### 3.2 透過 API 設定(腳本化)
```bash
# 在 188 主機執行
curl -s -X POST http://localhost:3301/api/v1/channels \
-H "Content-Type: application/json" \
-d '{
"name": "AWOOOI-API",
"type": "webhook",
"data": {
"webhook_url": "http://192.168.0.120:32334/api/v1/webhooks/signoz/alert"
}
}'
# 預期返回 channel ID
```
---
## Step 4: 建立測試告警規則
為了驗證整個鏈路,建立一個低閾值測試規則:
```bash
# 在 188 主機的 SignOz 目錄建立測試規則
cat > /tmp/test-alert.yaml << 'EOF'
groups:
- name: e2e_test
rules:
- alert: AWOOOI_E2E_SMOKE_TEST
expr: up{job="awoooi-api"} == 1 # 永遠觸發API 存活時)
for: 1m
labels:
severity: info
source: signoz
test: "true"
annotations:
summary: "E2E Smoke Test - 請忽略"
description: "這是 AWOOOI 告警鏈路的自動測試"
webhook: "http://192.168.0.120:32334/api/v1/webhooks/signoz/alert"
EOF
```
---
## Step 5: 端到端驗證
### 5.1 手動觸發測試
```bash
# 直接向 AWOOOI API 發送模擬 SignOz 告警
curl -s -X POST http://192.168.0.120:32334/api/v1/webhooks/signoz/alert \
-H "Content-Type: application/json" \
-d '{
"alertname": "APIHighErrorRate",
"status": "firing",
"labels": {
"alertname": "APIHighErrorRate",
"severity": "critical",
"service_name": "awoooi-api",
"source": "signoz"
},
"annotations": {
"summary": "API 錯誤率 > 5%",
"description": "服務 awoooi-api 錯誤率超標,這是一個測試告警"
},
"startsAt": "2026-03-29T12:00:00Z"
}'
# 預期回應
# {"status": "ok", "processed": 1, "results": [{"status": "accepted", "alert_name": "APIHighErrorRate"}]}
```
### 5.2 確認 Telegram 收到告警
```
預期在 Telegram Bot 中收到:
═══════════════════════════
📊 SignOz: APIHighErrorRate
═══════════════════════════
服務awoooi-api
摘要API 錯誤率 > 5%
[ Y 確認 ] [ N 忽略 ]
```
### 5.3 確認 API 日誌
```bash
kubectl logs -n awoooi-prod \
$(kubectl get pod -n awoooi-prod -l app=awoooi-api -o name | head -1) \
--tail=30 | grep -i signoz
# 預期看到:
# signoz_alert_received payload=...
# signoz_anomaly_recorded alert_name=APIHighErrorRate
# signoz_alert_processed alert_name=APIHighErrorRate incident_id=xxx
# signoz_telegram_sent approval_id=xxx
```
---
## 驗收標準
| 項目 | 通過條件 |
|------|---------|
| Webhook 端點可達 | `curl .../signoz/health` 返回 200 |
| SignOz 規則載入 | `/api/v3/alerts/rules` 包含 `APIHighErrorRate` |
| 手動測試正常 | 回應 `{"status": "ok"}` |
| Telegram 通知 | 成功收到告警卡片 |
| Incident 建立 | DB 中可查到 `source=signoz` 的 Incident |
| Approval 建立 | `GET /api/v1/approvals` 顯示新 Approval |
---
## 常見問題排除
| 症狀 | 診斷 | 解法 |
|------|------|------|
| Webhook 404 | `curl .../signoz/health` | 確認主機是 32334 而非 8089 |
| SignOz 規則不觸發 | SignOz UI → Alerts 頁 | 確認 Prometheus 端點可抓到 awoooi-api metrics |
| Telegram 未收到 | 查 API 日誌 | 確認 `OPENCLAW_TG_BOT_TOKEN` Secret 已注入 |
| Incident 建立失敗 | 查 API 日誌 `incident_creation_failed` | 確認 PostgreSQL 連線正常 |

View File

@@ -0,0 +1,313 @@
# RunBook: Worker HPA — 水平自動擴展設定
> **類型**: 操作型 RunBook
> **優先級**: 🔴 P0Worker 目前單點故障風險)
> **建立**: 2026-03-29 12:35 (台北)
> **建立者**: Antigravity
> **工時預估**: 3060 分鐘
> **前置條件**: K3s 叢集健康120/121 皆 Ready
---
## 背景與現況
### 🔍 精確現況診斷
**現有 HPA 配置 (`12-hpa.yaml`)**
| Deployment | Min | Max | CPU 閾值 | Memory 閾值 |
|-----------|-----|-----|---------|------------|
| awoooi-api | 2 | 6 | 70% | 80% |
| awoooi-web | 2 | 6 | 70% | 80% |
| **awoooi-worker** | ❌ 無 | ❌ 無 | — | — |
**Worker 的特殊性**
- Worker 消費 Redis Streams (Event Bus)
- 不像 API/Web 依賴 CPU/Memory 觸發,應依賴 **Queue 長度觸發**
- 但 K3s 預設沒有安裝 KEDAKubernetes Event-driven Autoscaling
- **最保守方案**:設定 min:1 max:3以 CPU 為指標
---
## 方案比較
| 方案 | 優點 | 缺點 | 適合性 |
|------|------|------|-------|
| **A: CPU HPA立即可行** | 零依賴,立即部署 | 不直接反應 Queue 長度 | ✅ 推薦(短期) |
| B: KEDA Redis Stream HPA | 最精確,按 Queue 長度擴縮 | 需安裝 KEDA operator | 🟡 中期規劃 |
| C: 固定 2 副本(無 HPA | 簡單穩定 | 浪費資源 | ❌ 不推薦 |
**決策**:採用方案 ACPU HPA並記錄方案 B 的未來路徑。
---
## Step 1: 確認 Worker 資源設定
```bash
# 查看現有 Worker Deployment 資源限制
kubectl get deployment awoooi-worker -n awoooi-prod -o yaml | grep -A 20 resources
# 預期看到:
# resources:
# requests:
# cpu: "100m"
# memory: "256Mi"
# limits:
# cpu: "500m"
# memory: "512Mi"
```
**如果沒有設定 resourcesHPA 無法正常運作!** 必須先在 `08-deployment-worker.yaml` 加入資源限制。
---
## Step 2: 更新 k8s/awoooi-prod/12-hpa.yaml
在現有檔案末尾追加 Worker HPA
```yaml
# =============================================================================
# Worker HPA追加到 12-hpa.yaml 末尾)
# =============================================================================
# K-Worker 2026-03-29: Worker HPACPU 指標min:1 max:3
# 注意Worker 消費 Redis Streams未來可升級為 KEDA Redis Stream 指標
# 建立者Antigravity
# =============================================================================
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: awoooi-worker-hpa
namespace: awoooi-prod
labels:
app.kubernetes.io/name: awoooi
app.kubernetes.io/component: worker
annotations:
description: "Worker 水平自動擴展 (1-3 replicas, 70% CPU)"
note: "未來可升級為 KEDA Redis Stream 指標,按 Queue 長度動態擴縮"
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: awoooi-worker
minReplicas: 1 # 保持最少 1 個處理事件
maxReplicas: 3 # 2 節點叢集的合理上限
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
behavior:
scaleUp:
stabilizationWindowSeconds: 120 # Worker 擴展比 API 保守120s vs 60s
policies:
- type: Pods
value: 1
periodSeconds: 120
scaleDown:
stabilizationWindowSeconds: 600 # Worker 縮容非常保守,避免事件處理中斷
policies:
- type: Pods
value: 1
periodSeconds: 300
```
---
## Step 3: 確認 Worker Deployment 有資源設定
```bash
# 查看現有設定
kubectl get deployment awoooi-worker -n awoooi-prod -o jsonpath='{.spec.template.spec.containers[0].resources}'
```
若無資源設定,在 `08-deployment-worker.yaml` 加入:
```yaml
# apps/api/src/workers 對應的 K8s Deployment
# 在 container spec 加入:
resources:
requests:
cpu: "100m" # Worker 正常負載估算
memory: "256Mi"
limits:
cpu: "500m" # 防止單 Worker 吃掉所有 CPU
memory: "512Mi"
```
---
## Step 4: 部署
```bash
# 方法 A直接 apply推薦只更新 HPA
kubectl apply -f k8s/awoooi-prod/12-hpa.yaml
# 確認 HPA 建立成功
kubectl get hpa -n awoooi-prod
# 預期輸出:
# NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS
# awoooi-api-hpa Deployment/api 5%/70% 2 6 2
# awoooi-web-hpa Deployment/web 3%/70% 2 6 2
# awoooi-worker-hpa Deployment/worker 8%/70% 1 3 1 ← 新增
# 方法 B透過 CD 觸發(標準流程)
git add k8s/awoooi-prod/12-hpa.yaml
git commit -m "feat(k8s): add Worker HPA (min:1 max:3 CPU 70%)"
git push origin main
```
---
## Step 5: 壓力測試驗證 HPA 觸發
```bash
# 模擬大量事件涌入(謹慎,在非尖峰時段執行)
for i in {1..100}; do
curl -s -X POST http://192.168.0.120:32334/api/v1/webhooks/alertmanager \
-H "Content-Type: application/json" \
-d '{
"version": "4",
"status": "firing",
"alerts": [{"status": "firing", "labels": {"alertname": "LoadTest", "severity": "info"}, "annotations": {}}]
}' &
done
# 觀察 HPA 反應(每 15 秒看一次)
watch -n 15 'kubectl get hpa awoooi-worker-hpa -n awoooi-prod'
```
---
## 中期路線圖:升級 KEDA Redis Stream HPA
```yaml
# 未來安裝 KEDA 後,可替換為更精確的 HPA
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: awoooi-worker-scaledobject
namespace: awoooi-prod
spec:
scaleTargetRef:
name: awoooi-worker
minReplicaCount: 1
maxReplicaCount: 5
triggers:
- type: redis
metadata:
address: "192.168.0.188:6380"
listName: "awoooi:events" # Redis Stream Key
listLength: "20" # 每個 Pod 處理最多 20 個待處理事件
```
KEDA 安裝指令(未來執行):
```bash
kubectl apply -f https://github.com/kedacore/keda/releases/download/v2.13.1/keda-2.13.1.yaml
```
---
## 驗收標準
| 項目 | 通過條件 |
|------|---------|
| HPA 建立 | `kubectl get hpa -n awoooi-prod` 顯示 `awoooi-worker-hpa` |
| 指標正常 | TARGETS 顯示實際 CPU%,非 `<unknown>` |
| Worker 正常運行 | `kubectl get pod -n awoooi-prod -l app=awoooi-worker` 顯示 Running |
| 最小副本 | Worker 期望副本數 = 1 |
---
## ⚠️ 架構安全補丁2026-03-29 更新,部署前必讀)
> 來源:`ARCHITECTURAL_RISK_WAR_GAME.md` 深度沙盤推演,代碼確認級別
### 補丁 1XCLAIM + Active Sweeper部署 HPA 的前置條件)
**❌ 現況**`signal_worker.py` 完全沒有 Redis PEL 孤兒任務回收機制。
**影響**Worker Pod 被 HPA 縮容(或非優雅崩潰)時,正在處理的任務卡在 Redis PELPending Entries List中永久無人處理。
> 🔴 **HPA 必須在 XCLAIM 機制合併 main 之後才能部署!**
需要在 `signal_worker.py` 加入的兩個機制:
```python
# 1. 啟動時接管孤兒_claim_orphaned_tasks在 start() 中調用)
# 2. 運行中持續掃描_reclaim_loop與 _consume_loop 並行)
async def _reclaim_loop(self, interval_s: int = 300) -> None:
"""每 5 分鐘主動掃描 PEL接管閒置超過 5 分鐘的孤兒任務"""
while self._running:
await asyncio.sleep(interval_s)
claimed = await self._claim_orphaned_tasks(idle_ms=300_000)
if claimed > 0:
logger.info("active_sweeper_claimed", count=claimed)
```
---
### 補丁 2terminationGracePeriodSeconds 三層對齊
**❌ 現況**`signal_worker.py``stop()` timeout = **5 秒**AI 分析任務最長 60 秒。K8s 的 `terminationGracePeriodSeconds` 未設定(預設 30 秒)。兩個值都不夠,且彼此不對齊。
**需要同時修改兩個地方**
```yaml
# k8s/awoooi-prod/08-deployment-worker.yaml
spec:
template:
spec:
terminationGracePeriodSeconds: 90 # 🆕 必須設定(比 Python timeout 多 15 秒緩衝)
containers:
- name: awoooi-worker
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 5"] # 讓 K8s 先更新 Endpoint 再發 SIGTERM
```
```python
# apps/api/src/workers/signal_worker.py
async def stop(self) -> None:
self._running = False
if self._task:
try:
await asyncio.wait_for(self._task, timeout=75.0) # 🆕 從 5 秒改為 75 秒
except (TimeoutError, asyncio.CancelledError):
self._task.cancel()
logger.info("signal_worker_stopped")
```
**三層數值關係**
```
preStop sleep: 5s
Python timeout: 75s ← 比 K8s grace period 少 15s 緩衝
K8s grace period: 90s ← terminationGracePeriodSeconds
```
---
### 合規確認指令(部署後必須執行)
```bash
# 確認 terminationGracePeriodSeconds 已生效
kubectl get deployment awoooi-worker -n awoooi-prod \
-o jsonpath='{.spec.template.spec.terminationGracePeriodSeconds}'
# 預期90
# 模擬縮容,確認優雅關機
kubectl scale deployment awoooi-worker -n awoooi-prod --replicas=0
kubectl logs -n awoooi-prod -l app=awoooi-worker --tail=20
# 預期看到shutdown_signal_received → signal_worker_shutting_down → signal_worker_stopped
# 整個流程在 90 秒內完成
```