# Phase 1 Final Critic Sign-off — Operation Ollama-First v5.0
> **日期**:2026-05-03 / critic-A11(第二輪 / 收尾)
> **審查範圍**:A3 / A4 / A5 / 第一輪 A11 修補的全部產出
> **基準文件**:`docs/phase1_critic_review_20260503.md`
---
## Verdict
- [ ] APPROVED — Phase 1 deploy ready
- [x] **APPROVED WITH NOTES** — 接受 deploy;4 項 NOTE 統帥部署前/後處理即可
- [ ] CONDITIONAL — 修以下後 deploy
- [ ] REJECTED
> **理由**:本輪沒有發現新 BLOCKER。前一輪 BLOCKER 中 B2(pgcrypto)migration 端已修補;B1(ai_usage_tracking ORM 落後 schema)屬於既有技術債、不阻擋 v5.0 觀測層上線。Logger 與 token report 行為正確、52/52 unit test 通過、失敗安全與 PII 紀律執行到位。NOTE 主要是文件對齊與已知盲區(Bot main path token=0 / chat_id 進 meta),不影響 P1 觀測層收尾。
---
## 前一輪 BLOCKER / HIGH 處理確認
| 編號 | 等級 | 描述 | 本輪驗證 |
|---|---|---|---|
| **B1** | 🔴 | ai_usage_tracking ORM 與實際欄位脫鉤(雙寫並存) | ⚠️ **未動**。ORM `database/ai_models.py:72-109` 仍是過時版;屬既有技術債,不影響 v5.0 觀測層(A4/A5 一律走 raw SQL → ai_calls,未碰 ai_usage_tracking)。建議列入 Phase 12 deprecate roadmap。 |
| **B2** | 🔴 | migration 026 DIGEST() 需 pgcrypto | ✅ **已驗證**。`migrations/026_add_embedding_signature.sql` 頂部已加 `CREATE EXTENSION IF NOT EXISTS pgcrypto;`。 |
| **H1** | 🟠 | provider 白名單 CHECK | ✅ **已驗證**。`migrations/024:88-91` `chk_ai_calls_provider` 列出 7 個 provider 與 `_PROVIDER_DISPLAY` 完全一致。 |
| **H2** | 🟠 | meta/error 大小護欄 | ✅ **已驗證**。`024:104-109` meta ≤ 8192 / error ≤ 4096 octet。Python 端 `set_error` 也截 2000 字(`ai_call_logger.py:168`),雙保險。 |
| **H3** | 🟠 | budgets 漏 nim/nim_via_elephant | ✅ **已驗證**。`025:170-199` 7 個 provider × monthly + 3 條全供應商總額(daily/weekly/monthly)共 10 筆種子。 |
| **H4** | 🟠 | idx 非 covering(latency 樂觀) | ⚠️ 文件層 — 不影響部署。 |
| **M1-M6/L3** | 🟡🔵 | 細節修補 | ✅ 全數在 024/025 内驗證通過。 |
| **M4** | 🟡 | manager.py 未 import 新 model | ⚠️ A4/A5 全走 raw SQL,未建立 AICall ORM class,所以 import 缺口不會引發 `Base.metadata.create_all` 漏表(migrations 直接建表)。**不阻擋**。 |
| **L1** | 🔵 | ewoooc migration 編號衝突 | 🟡 未驗證 — 統帥 deploy 前手動 `git fetch ewoooc && git log ewoooc/main --oneline -- migrations/` 即可。 |
**小結**:第一輪 BLOCKER 中可由 critic 自動修補的 1/2 已修;B1 為**設計文資料漂移**,本輪確認 v5.0 觀測層**完全不依賴** ai_usage_tracking,所以解耦處理(不阻擋 deploy)。
---
## 本輪新發現 Findings
### BLOCKER
**無**。
### HIGH
#### H5. caller 欄位**沒有** CHECK 白名單 — 本輪實際比對下發現先前 H1 修補只覆蓋 `provider` 不含 `caller`
- **位置**:`migrations/024_create_ai_calls_table.sql:55`(`caller VARCHAR(64) NOT NULL`)+ `:86-110` 所有 CHECK,無 `chk_ai_calls_caller`
- **證據**:grep `chk_` 在 024 共 7 條 constraint,僅針對 provider/status/fallback/duration/meta/error,**caller 完全沒護欄**
- **影響**:A4 接入的 13 個 caller 名(`hermes_intent`/`hermes_analyst`/`hermes_rule_engine`/`code_review_hermes`/`code_review_openclaw`/`code_review_elephant`/`openclaw_qa`/`openclaw_qa_nim`/`openclaw_weekly`/`openclaw_daily`/`openclaw_monthly`/`openclaw_meta`/`nemotron_dispatch`/`openclaw_bot_main`/`openclaw_bot_gemini`/`openclaw_bot_nim`)若未來打字打錯(例如 `openclae_qa`)DB 不會擋;token 報表 GROUP BY caller 會出現假名稱,污染統計
- **緩解**:戰役 v5.0 收尾才標到的問題,本輪先 NOTE 不阻 deploy,但建議 Phase 5 跑保留任務時加:
```sql
ALTER TABLE ai_calls ADD CONSTRAINT chk_ai_calls_caller_known
CHECK (caller ~ '^[a-z][a-z0-9_]{2,63}$') NOT VALID;
```
(格式約束而非完整白名單,避免每次擴 caller 都改 schema)
- **嚴重度判定**:本來 BLOCKER,但因 v5.0 上線前 caller 名是**集中於 ai_call_logger 的固定字串**(13 個全部 grep 過),typo 風險可控 → 降為 HIGH。
#### H6. `chat_id` 寫入 ai_calls.meta — 屬 PII(Telegram 用戶識別)
- **位置**:`routes/openclaw_bot_routes.py:6832, 6892, 6959, 7034`
- **證據**:4 個 Bot Q&A 入口的 `meta={'chat_id': chat_id, ...}` 全部把 Telegram chat_id 直接落地進 ai_calls.meta JSONB
- **規格牴觸**:
- `services/token_report_service.py:18`「PII 保護: 報表訊息不含 prompt 原文;ai_insights metadata 只存統計 meta(不存 username)」
- `feedback_user_input_html_injection`:Telegram 用戶識別屬 user-controllable 欄位,不該以明文落地 90 天
- **影響**:90 天保留期 + 萬一報表程式換成 raw query 可被反查;雖然 ai_calls.caller 維度不會直接顯示 chat_id,但稽核時違反「Telegram username/chat_id 進 DB 必雜湊」原則
- **建議修法**(不阻 deploy,可在 Phase 2 同步):
```python
meta={'chat_id_hash': hashlib.sha256(str(chat_id).encode()).hexdigest()[:12], ...}
```
或乾脆改成 `meta={'has_chat_id': bool(chat_id), ...}`(只記是否屬聊天會話)
- **嚴重度判定**:HIGH。短期內統帥(即唯一 Telegram operator)即所有 chat_id 來源,外洩面向小;但 PII 紀律一致性必須維持,所以列入 deploy 後 Phase 2 第一波 patch 清單。
### MEDIUM
#### M7. `openclaw_bot_main` token 永遠記為 0 — Section 5「Ollama Tokens」會嚴重低估
- **位置**:`routes/openclaw_bot_routes.py:6834` `ollama_service.generate(...)` 回傳 `OllamaResponse`,但 `OllamaResponse` 沒有 `prompt_eval_count`/`eval_count` 欄位(`services/ollama_service.py:88-95` dataclass 只有 success/content/model/error/total_duration/host)
- **影響**:
- Bot 主鏈走 GCP Ollama 的所有 Q&A token 記為 0
- Section 5「今日 Ollama Tokens vs 7 日均」失真
- Section 1 `ollama_pct` 計算分子下偏 → 報表會顯示「Ollama 失守」假警報
- **建議修法**(Phase 2 A6 修 ollama_service.py 時順便):
```python
@dataclass
class OllamaResponse:
success: bool
content: str
model: str
error: Optional[str] = None
total_duration: Optional[float] = None
host: Optional[str] = None
prompt_tokens: int = 0 # ← 新增
completion_tokens: int = 0 # ← 新增
```
並在 `generate()` 内 `data.get('prompt_eval_count')/eval_count` 帶入。
- **暫行處置**:A4 應在 `routes/openclaw_bot_routes.py:6834` 加 TODO 標記「待 ollama_service 補 token 欄位後接回」。本輪不阻 deploy。
#### M8. `caller='openclaw_qa_nim'` 由 `_call_nvidia_nim` 動態組成,與 logger 端命名習慣脫鉤
- **位置**:`services/openclaw_strategist_service.py:737` `nim_caller = f"{caller}_nim"`
- **影響**:Section 3 TOP caller 報表會出現 5 個 NIM 變體(`openclaw_qa_nim`/`openclaw_weekly_nim`/`openclaw_daily_nim`/`openclaw_monthly_nim`/`openclaw_meta_nim`)
- **判定**:這其實是好設計(清楚標示 NIM 路徑由哪個原 caller fallback 來的),但與 H5 中提到「未來加 caller 白名單」邏輯衝突 → 若加 CHECK constraint 必須允許 `_nim` 後綴
- **建議**:不修;但 ADR-028 撰寫時要明文聲明此命名慣例。
#### M9. ai_insights INSERT 缺 `confidence` 欄位 — 走 default 0.5,但 token report 是規則引擎產出,理論該標 1.0
- **位置**:`services/token_report_service.py:790-799`
- **影響**:未來 RAG 檢索時,token report 的洞察會被當「中信度」混入;其實這是規則引擎死硬產出,應該標高信度
- **建議修法**:INSERT 加 `confidence` 欄位設 1.0;或將 `avg_quality` 從 0.9 改為 1.0
- **嚴重度**:MEDIUM,不阻 deploy。
#### M10. `daily_token_report` 截斷邏輯雙重保險,但**截斷點落在 HTML tag 中間**會壞 parse_mode='HTML'
- **位置**:
- `services/token_report_service.py:130` `report_html[: _TELEGRAM_MAX_CHARS - 80]`
- `services/telegram_templates.py:566` `body[: _DAILY_TOKEN_REPORT_MAX_CHARS - 80]`
- **風險**:若截斷剛好落在 `...` 之間(例如卡在 `]*$', '', truncated)` 把不完整的開 tag 砍掉
- **嚴重度**:MEDIUM,建議 Phase 2 修;不阻 deploy(萬一觸發只是該日報表掉而已,scheduler 不爆)。
### LOW
#### L5. `qwen3:14b` 在戰役 v5.0 Frontier 升級表中提到,但 COST_TABLE 未列
- **位置**:`services/ai_call_logger.py:43-62`
- **驗證**:`grep -n "qwen3" services/ai_call_logger.py services/token_report_service.py` → 0 hit
- **影響**:未來 P2 把 NemoTron 改用 qwen3:14b 時,第一波寫入會走 `_calc_cost` 的 `unknown model` 路徑,log warning 但成本回 0(因為 qwen3 是本地 Ollama 也應為 0)→ 行為正確但 noisy log
- **建議**:在 COST_TABLE 加:
```python
'qwen3:14b': {'in': 0.0, 'out': 0.0},
'qwen3:14b-q4_K_M': {'in': 0.0, 'out': 0.0}, # 視 v5.0 量化版
```
- **嚴重度**:LOW(Phase 2 會修),不阻 deploy。
#### L6. `total_cost_usd` SUM 無 NUMERIC 上限保險
- **位置**:`services/token_report_service.py:195` `COALESCE(SUM(cost_usd), 0)`
- **思考**:單筆 cost_usd 是 NUMERIC(10,6)(上限 9999.999999)。若一日內呼叫 100,000 筆且每筆 ~$0.01,SUM 仍 < 10K 安全
- **判定**:v5.0 規模下不會炸;但 Phase 9 預算守門需重新評估 — 統帥可忽略。
#### L7. `total_duration` (Decimal) 隱式轉 float 可能損失精度
- **位置**:`services/token_report_service.py:30` `from decimal import Decimal` 但未使用
- **影響**:dead import,無功能影響
- **建議**:刪 line 30 import。
#### L8. `Section 4` 預算列在無預算時用 `_pad('未設定預算', 10)` 寬度可能斷行
- **位置**:`services/token_report_service.py:843-844`
- **驗證**:「未設定預算」5 個中文 = 10 寬度,剛好;無 padding 餘量。寫死 OK。
- **嚴重度**:LOW,FYI。
---
## Unit test 實測
```
$ /opt/anaconda3/bin/python3 -m pytest tests/test_ai_call_logger.py tests/test_token_report_service.py -v 2>&1 | tail -30
tests/test_token_report_service.py::TestQueriesViaMock::test_query_top_callers_orders_by_tokens PASSED [ 76%]
tests/test_token_report_service.py::TestQueriesViaMock::test_query_cost_breakdown_filters_zero_cost PASSED [ 78%]
tests/test_token_report_service.py::TestSendDailyReport::test_send_happy_path PASSED [ 80%]
tests/test_token_report_service.py::TestSendDailyReport::test_send_truncates_oversized_message PASSED [ 82%]
tests/test_token_report_service.py::TestSendDailyReport::test_send_resilient_to_telegram_failure PASSED [ 84%]
tests/test_token_report_service.py::TestSendDailyReport::test_generate_returns_failure_msg_when_db_dies PASSED [ 86%]
tests/test_token_report_service.py::TestTelegramTemplate::test_daily_token_report_appends_footer PASSED [ 88%]
tests/test_token_report_service.py::TestTelegramTemplate::test_daily_token_report_truncates_to_4096 PASSED [ 90%]
tests/test_token_report_service.py::TestTelegramTemplate::test_daily_token_report_escapes_footer_url PASSED [ 92%]
tests/test_token_report_service.py::TestFormatHelpers::test_fmt_kb PASSED [ 94%]
tests/test_token_report_service.py::TestFormatHelpers::test_esc_handles_none PASSED [ 96%]
tests/test_token_report_service.py::TestFormatHelpers::test_budget_line_zero_budget PASSED [ 98%]
tests/test_token_report_service.py::TestFormatHelpers::test_trend_line_handles_zero_baseline PASSED [100%]
============================== 52 passed in 0.21s ==============================
```
**結論**:52/52 全綠(22 ai_call_logger + 30 token_report_service),覆蓋:
- happy/exception/explicit-fallback/set-error 三種 context manager 路徑
- decorator + model_extractor + 例外 reraise
- DB 失敗 swallow / async dispatch 失敗 swallow
- COST_TABLE 各 provider 計算 + 未知 model + NIM 前綴自動 0 + 負數安全
- AI_CALL_LOGGING_ENABLED 開關 + kill-switch 連續失敗
- 6 條告警規則(spike/gemini share/error rate/budget/gcp hit/cache)+ insights 規則
- 報表 6 段落齊全 + 4096 截斷 + HTML escape + DB fail 路徑
- format helpers(fmt_kb/esc/budget_line/trend_line)邊界
**通過 ai_call_logger 「DB 失敗永不影響主流程」鐵律**:`test_db_failure_does_not_break_main_flow` + `test_async_dispatch_failure_swallowed` 兩條測試直接證明。
**通過「meta 不洩露 prompt 原文」鐵律**:`test_meta_does_not_leak_raw_prompt_into_call_state` + `test_set_prompt_hash_truncates_to_12` 兩條測試。
---
## 安全/PII 審計(六大類深審)
| 類別 | 結論 | 證據 |
|---|---|---|
| **A. logger 安全** | ✅ 失敗安全到位 | `_write_to_db` 全 try/except / kill-switch 在連續 10 次失敗觸發 (`_record_failure:80-89`) / `_async_write` 走 daemon thread 不阻塞 |
| **B. PII 保護** | ⚠️ **新發現 chat_id 進 meta(H6)** | logger 本身只用 `set_prompt_hash` 雜湊;但 4 個 Bot 入口直接灌 chat_id 進 meta — 違反規格 |
| **C. SQL Injection** | ✅ 全參數化 | `_exec_query` 強制走 SQLAlchemy `text(sql), params`;7 條報表 SQL 全用 named param |
| **D. HTML escape** | ✅ 對齊既有風格 | `_esc()` 對 `&<>` 三字元與 `telegram_templates._html_escape` 一致;所有 user-controlled (caller/model/error/insight text) 進 HTML 前都 escape |
| **E. 路由 / cron 衝突** | ✅ 23:55 唯一 | 17 條既有 cron 中無 23:5x 區段;資源競爭風險低(DB query 預估 < 30s) |
| **F. 預算 0 / 除 0** | ✅ 全數防護 | 7 處潛在除 0 全部有 `if X else default` 守衛 |
---
## 與既有系統整合風險
### 1. A4 修改 vs Phase 2 A6 即將修改 — **conflict 風險評估**
| 檔案 | A4(Phase 1)做了什麼 | A6(Phase 2)將做什麼 | Conflict 風險 |
|---|---|---|---|
| `services/ollama_service.py` | + `get_host_label()` / `OllamaResponse.host` 欄位 / `host=self.host` 4 處 | 預期擴 GCP/111 切換邏輯(B2)、補 token 欄位(M7) | 🟡 中 — 都動同一個 `OllamaResponse` dataclass + `generate()` 主體;建議 A6 先 rebase 再開工 |
| `services/code_review_pipeline_service.py` | 3 處包 log_ai_call(hermes/openclaw/elephant) | 修補 B3 / 接入 ad-hoc retry | 🟡 中 — 都動 `_hermes_scan` / `_openclaw_assess` 主流程;rebase 前先讀 A4 區塊 |
| `services/aider_heal_executor.py` | A4 **未動** | A6 將動 | ✅ 低 — 完全分離 |
| `routes/openclaw_bot_routes.py` | 4 處包 log_ai_call(main/gemini×2/nim) | 預期不會碰 | ✅ 低 |
| `services/hermes_analyst_service.py` | 2 處包 log_ai_call + 修 commit 00591c5 殘留 bug | 預期不碰 | ✅ 低 |
| `services/nemoton_dispatcher_service.py` | 1 處包 log_ai_call | 預期不碰 | ✅ 低 |
| `services/openclaw_strategist_service.py` | _call_gemini/_call_nvidia_nim 加 `caller=` 參數 | 預期不碰 | ✅ 低 |
**建議**:
- A6 開工前先 `git pull origin main` 拉到 A4 的 commit
- 動 `ollama_service.py` 時優先**新增**而非改既有 dataclass 欄位(minimize merge conflict)
- 動 `code_review_pipeline_service.py` 時保留現有 `with log_ai_call(...)` 包裝層,僅在内層修補
### 2. `commit 00591c5` 殘留 bug 修復確認
`services/hermes_analyst_service.py:194-200`:原本 commit 00591c5 動到 `except Exception as e:` 區塊時,誤把 `logger.warning` 抹除留下孤立 f-string。本輪 A4 順手修補:
```python
except Exception as e:
# NOTE: 修補 commit 00591c5 殘留的孤立 f-string(原 logger.warning 被誤刪)
logger.warning(
f"[Hermes.intent] Ollama 連線失敗,降級規則引擎"
f"(model={HERMES_MODEL} error={type(e).__name__}: {e})"
)
_ctx.set_error(f"{type(e).__name__}: {e}")
_ctx.fallback_to_caller('hermes_rule_engine')
return None
```
✅ **已驗證**:logger.warning 完整呼叫 + ctx.set_error + fallback_to_caller 三件齊全。原 silent failure 反模式已破。
---
## Phase 2 銜接建議
統帥批准 Phase 2 A6 開工前,建議先 commit Phase 1 的所有變動到 main(A6 才有乾淨 baseline)。順序:
```bash
git add migrations/024 migrations/025 migrations/026 # A3
git add services/ai_call_logger.py services/token_report_service.py # A4/A5
git add tests/test_ai_call_logger.py tests/test_token_report_service.py # A4/A5 tests
git add services/hermes_analyst_service.py services/nemoton_dispatcher_service.py
git add services/openclaw_strategist_service.py services/code_review_pipeline_service.py
git add services/ollama_service.py routes/openclaw_bot_routes.py
git add run_scheduler.py services/telegram_templates.py
git add docs/phase0_audit_report_20260503.md docs/phase1_db_design_20260503.md
git add docs/phase1_critic_review_20260503.md docs/phase1_final_critic_signoff_20260503.md
git commit -m "[Phase 1] Operation Ollama-First v5.0 觀測層落地 (A3 migration / A4 logger 13 callers / A5 token report)"
```
部署後第一波驗證(Phase 2 啟動前):
```sql
-- 1. CHECK constraint 全在
SELECT conname FROM pg_constraint WHERE conrelid='ai_calls'::regclass ORDER BY conname;
-- 2. 種子預算 10 筆
SELECT period, provider, budget_usd, alert_pct FROM ai_call_budgets ORDER BY period, provider NULLS FIRST;
-- 3. logger 寫入煙測(A4 接入後第一次 LLM 呼叫應出現)
SELECT caller, provider, model, status, input_tokens, output_tokens, duration_ms
FROM ai_calls ORDER BY called_at DESC LIMIT 20;
-- 4. caller 分布(驗證 13 個白名單值)
SELECT caller, COUNT(*) FROM ai_calls GROUP BY caller ORDER BY 2 DESC;
-- 5. 失敗率(觀察 kill-switch 是否誤觸發)
SELECT status, COUNT(*) FROM ai_calls
WHERE called_at >= NOW() - INTERVAL '24h' GROUP BY status;
```
Phase 2 進度第 1 天觀察點:
- `SELECT count(*) FROM ai_calls;` 應 ≥ 100(戰役前審計 34 個呼叫點 / 一日 ~200 calls)
- `SELECT count(DISTINCT caller) FROM ai_calls;` 應 ≥ 13
- 23:55 cron 第一次跑完應有 1 筆 `ai_insights WHERE insight_type='daily_token_report'`
---
## NOTE 清單(Sign-off 條件)
deploy 後 7 天内由統帥決策處理:
- [ ] **NOTE-1(H5)**:考慮加 caller 格式 CHECK constraint(NOT VALID 不阻既存資料)
- [ ] **NOTE-2(H6)**:4 個 Bot 入口的 `chat_id` 改成 hash 後存(Phase 2 第一波 patch)
- [ ] **NOTE-3(M7)**:`OllamaResponse` 補 `prompt_tokens/completion_tokens` 欄位 → 修復 `openclaw_bot_main` token=0 黑洞(與 Phase 2 A6 ollama_service 改動合併)
- [ ] **NOTE-4(L1)**:deploy 前手動驗 `git fetch ewoooc && git log ewoooc/main --oneline -- migrations/`
deploy 後 30 天可選優化:
- [ ] **M9** ai_insights confidence 標 1.0 / **M10** HTML tag 截斷修補 / **L5** qwen3:14b 進 COST_TABLE / **L7** 刪 `Decimal` dead import
- [ ] **B1** ai_usage_tracking ORM 對齊真實 schema(雙寫 deprecate roadmap,與 ADR-028 合併)
---
## Final Sign-off
```
critic-A11 / 2026-05-03 / Phase 1 final closure
Verdict: APPROVED WITH NOTES
Tests: 52/52 PASSED (22 ai_call_logger + 30 token_report)
Findings (本輪新發現): 0 BLOCKER / 2 HIGH (H5/H6) / 4 MEDIUM (M7-M10) / 4 LOW (L5-L8)
Findings (前輪殘留): 1 HIGH (H4 文件) / 1 MEDIUM (M4 解耦) / 1 LOW (L1 統帥手驗)
簽署:critic-A11 (Operation Ollama-First v5.0 / Phase 1 sign-off)
```