## Phase 1-3: Control Plane + Contract System - awooop_phase1_control_plane_2026-05-04.sql: 12 張核心表 + RLS - awooop_phase1_batch1_rls_2026-05-04.sql: 全部 FORCE RLS + GRANT - packages/awooop-contracts/: 六合約 JSON Schema + golden fixtures - src/models/awooop_contracts.py: Pydantic v2 contract models(extra=forbid) - src/repositories/contract_repository.py: contract lifecycle(draft→published→active) - src/services/contract_service.py: HMAC publish sig + Redis multi-sig activate - src/services/schema_validator.py: LLM output validator(retry×3, E-SCHEMA-001) ## Phase 2: Tenant Isolation - awooop_phase2_budget_ledger_2026-05-04.sql: budget_ledger + RLS - src/services/budget_service.py: Token Budget Hard Kill 三層防線 - src/core/context.py: PROJECT_ID ContextVar(31 background loop 自動繼承) - src/db/base.py + models.py: project_id 欄位 + RLS set_config 注入 - src/hermes/nl_gateway.py: project_id Redis key 前綴(Phase A 雙寫) - src/services/anomaly_counter.py: per-project 改造(Phase A fallback) ## Phase 4: Platform Shell in Shadow Mode - awooop_phase4_run_state_2026-05-04.sql: run_state + step_journal + idempotency - src/services/run_state_machine.py: 8-state FSM + SKIP LOCKED + stale reaper - src/services/platform_runtime.py: UUID v7 + W3C trace_id + shadow_execute - src/services/audit_sink.py: PII/secret redaction 9 patterns - src/api/v1/platform/runs.py: POST/GET /v1/platform/runs(Router→Service 架構) - src/workers/platform_worker.py: SKIP LOCKED worker + heartbeat + reaper loop - src/main.py: platform router + lifespan worker start/stop ## Phase 5: MCP Gateway 五閘門 - awooop_phase5_mcp_gateway_2026-05-04.sql: 4 表 + RLS - src/plugins/mcp/gateway.py: McpGateway(Gate 1~5, E-MCP-GATE-001~009) - src/plugins/mcp/redaction_middleware.py: 雙層 redaction + 16K 截斷 - src/plugins/mcp/registry.py: __provider name mangling(ADR-116) - src/plugins/mcp/credential_resolver.py: k8s secret ref 解析 - tests/test_mcp_credential_isolation.py: 10 個迴歸測試(secret leak 防再現) ## Phase 6-8: EwoooC + Channel Hub + Approval Token - awooop_phase6_ewoooc_onboarding_2026-05-04.sql: ewoooc tenant + 4 read-only MCP tools - awooop_phase7_channel_hub_2026-05-04.sql: conversation_event + outbound_message - src/services/provider_proxy.py: ProviderProxy + PlatformEnvelope(ADR-115) - src/services/channel_hub.py: Telegram inbound mirror + Progressive Feedback(30s) - src/services/awooop_approval_token.py: HS256 + jti NX replay 防護 + suggest mode Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
111 lines
4.5 KiB
Bash
Executable File
111 lines
4.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# Telegram dedup 修復驗證 — commit b3a0f0d7 (fingerprint dedup + 24h TTL)
|
||
# 部署時間: 2026-05-02 16:25 Asia/Taipei
|
||
# 用法: ssh wooo@192.168.0.121 'bash -s' < verify_telegram_dedup_b3a0f0d7.sh
|
||
# 或 scp 上去後 sudo bash verify_telegram_dedup_b3a0f0d7.sh
|
||
# 純讀,不寫任何 prod 資料
|
||
|
||
set -e
|
||
|
||
POD=$(sudo kubectl get pods -n awoooi-prod -l app=awoooi-api -o jsonpath='{.items[0].metadata.name}')
|
||
echo "=== Pod: $POD ==="
|
||
echo "=== Image SHA (應含 b3a0f0d7) ==="
|
||
sudo kubectl get pod -n awoooi-prod "$POD" -o jsonpath='{.spec.containers[0].image}'
|
||
echo
|
||
echo
|
||
|
||
echo "=== A. 過去 1h Telegram 發送 top(部署後)==="
|
||
sudo kubectl exec -n awoooi-prod "$POD" -- python -c "
|
||
import asyncio, os, asyncpg
|
||
async def q():
|
||
conn = await asyncpg.connect(os.environ['DATABASE_URL'])
|
||
rows = await conn.fetch(\"\"\"
|
||
SELECT
|
||
COALESCE(i.title, 'unknown') AS alertname,
|
||
COALESCE(i.affected_services[1], 'unknown') AS target,
|
||
COUNT(t.id) AS msg_count,
|
||
MIN(t.created_at) AS first_sent,
|
||
MAX(t.created_at) AS last_sent
|
||
FROM notification_outcomes t
|
||
JOIN approval_records a ON t.approval_id = a.id
|
||
JOIN incidents i ON a.incident_id = i.id
|
||
WHERE t.channel='telegram' AND t.created_at > now() - interval '1 hour'
|
||
GROUP BY 1,2 ORDER BY 3 DESC LIMIT 10
|
||
\"\"\")
|
||
for r in rows:
|
||
print(f\" {r['msg_count']:>3} | {r['alertname'][:40]:<40} | {r['target'][:30]:<30} | first={r['first_sent']:%H:%M} last={r['last_sent']:%H:%M}\")
|
||
await conn.close()
|
||
asyncio.run(q())
|
||
"
|
||
|
||
echo
|
||
echo "=== B. 過去 24h(含部署前對照)==="
|
||
sudo kubectl exec -n awoooi-prod "$POD" -- python -c "
|
||
import asyncio, os, asyncpg
|
||
async def q():
|
||
conn = await asyncpg.connect(os.environ['DATABASE_URL'])
|
||
rows = await conn.fetch(\"\"\"
|
||
SELECT
|
||
COALESCE(i.title, 'unknown') AS alertname,
|
||
COALESCE(i.affected_services[1], 'unknown') AS target,
|
||
COUNT(t.id) AS msg_count
|
||
FROM notification_outcomes t
|
||
JOIN approval_records a ON t.approval_id = a.id
|
||
JOIN incidents i ON a.incident_id = i.id
|
||
WHERE t.channel='telegram' AND t.created_at > now() - interval '24 hours'
|
||
GROUP BY 1,2 ORDER BY 3 DESC LIMIT 10
|
||
\"\"\")
|
||
for r in rows:
|
||
print(f\" {r['msg_count']:>3} | {r['alertname'][:40]:<40} | {r['target'][:30]:<30}\")
|
||
await conn.close()
|
||
asyncio.run(q())
|
||
"
|
||
|
||
echo
|
||
echo "=== C. 截圖兩 INC 最後發送時刻 ==="
|
||
sudo kubectl exec -n awoooi-prod "$POD" -- python -c "
|
||
import asyncio, os, asyncpg
|
||
async def q():
|
||
conn = await asyncpg.connect(os.environ['DATABASE_URL'])
|
||
rows = await conn.fetch(\"\"\"
|
||
SELECT i.id, i.title, COUNT(t.id) AS total_24h,
|
||
MAX(t.created_at) AS last_sent,
|
||
COUNT(t.id) FILTER (WHERE t.created_at > '2026-05-02 16:25 Asia/Taipei'::timestamptz) AS post_deploy
|
||
FROM notification_outcomes t
|
||
JOIN approval_records a ON t.approval_id = a.id
|
||
JOIN incidents i ON a.incident_id = i.id
|
||
WHERE i.id IN ('INC-20260501-6FE3BD','INC-20260502-FD6E21')
|
||
AND t.channel='telegram' AND t.created_at > now() - interval '24 hours'
|
||
GROUP BY 1,2 ORDER BY 1
|
||
\"\"\")
|
||
for r in rows:
|
||
print(f\" {r['id']} | {r['title'][:40]:<40} | 24h={r['total_24h']} 部署後={r['post_deploy']} last={r['last_sent']:%H:%M}\")
|
||
await conn.close()
|
||
asyncio.run(q())
|
||
"
|
||
|
||
echo
|
||
echo "=== D. Redis dedup key 結構(fingerprint 應已建立)==="
|
||
sudo kubectl exec -n awoooi-prod "$POD" -- python -c "
|
||
import asyncio, os
|
||
from redis.asyncio import Redis
|
||
async def q():
|
||
r = Redis.from_url(os.environ['REDIS_URL'])
|
||
fp_keys = await r.keys('telegram_sent:fp:*')
|
||
inc_keys = await r.keys('telegram_sent:INC-*')
|
||
print(f' telegram_sent:fp:* (新格式) = {len(fp_keys)} (應 > 0)')
|
||
print(f' telegram_sent:INC-* (舊格式) = {len(inc_keys)} (應 = 0 或減少中)')
|
||
if fp_keys:
|
||
print(f' 範例 fp key: {fp_keys[0].decode() if isinstance(fp_keys[0], bytes) else fp_keys[0]}')
|
||
sweeper_keys = await r.keys('sweeper_done:*')
|
||
print(f' sweeper_done:* = {len(sweeper_keys)} (24h TTL,整個 INVESTIGATING 集合)')
|
||
asyncio.run(q())
|
||
"
|
||
|
||
echo
|
||
echo "=== 驗收標準 ==="
|
||
echo "✅ A 段任何 fingerprint msg_count ≤ 2 → 修復生效"
|
||
echo "✅ C 段兩 INC 部署後 ≤ 1 → 鐵證生效"
|
||
echo "✅ D 段 telegram_sent:fp:* 已建立 → 新 dedup 邏輯有跑"
|
||
echo "❌ 任何 fingerprint 部署後仍 ≥ 5 → 未生效,回報 Claude"
|