Some checks failed
CD Pipeline / build-and-deploy (push) Has been cancelled
Wave 8 P3.1-T2 三項感知強化(多 engineer 補完):
Sentry Webhook 簽章驗證:
- sentry_webhook.py: 接入 SentryWebhookService.verify_sentry_signature()
- 拒絕無效 sentry-hook-signature → 401 → 防偽造攻擊
DiagnosisAggregator Pod 深診斷整合:
- pre_decision_investigator.py: 新增 _collect_diagnosis_aggregator()
- ENABLE_DIAGNOSIS_AGGREGATOR feature flag 守衛(default=False)
- evidence_snapshot.py: extra_diagnosis 欄位 + build_summary 顯示
- timeout=3.0s + try/except 隔離(fail-soft)
- Conservative 策略:待重疊分析確認 vs PreDecisionInvestigator 不重複
config.py:
- 新增 ENABLE_DIAGNOSIS_AGGREGATOR Field(default=False,K8s ConfigMap 動態啟用)
Solver B1 補測(commit 7c726ebc 對應):
- test_solver_recommended_actions.py — 20 tests + 3 skipped
- 驗證結構化 recommended_actions(北極星 §1.1 修復多樣性 ≥ 40%)
- LLM 失敗 graceful degraded(candidates=[], degraded=True)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Multiple Engineers (Wave 8 P3.1-T2) <noreply@anthropic.com>
574 lines
19 KiB
Python
574 lines
19 KiB
Python
"""
|
||
AWOOOI API - Sentry Webhook Handler
|
||
====================================
|
||
接收 Sentry Issue Alert,轉發給 OpenClaw 進行 AI 分析
|
||
|
||
整合流程:
|
||
1. Sentry Alert → AWOOOI API Webhook
|
||
2. 組裝錯誤上下文
|
||
3. 呼叫 OpenClaw Error Analyzer Agent
|
||
4. 結果回寫 Sentry Issue Comment
|
||
5. 發送 Telegram 告警 (含截圖)
|
||
6. 建立 Approval 供人工審核
|
||
|
||
🔴 HARD RULE: 時間顯示使用 Asia/Taipei (UTC+8)
|
||
"""
|
||
|
||
import uuid
|
||
|
||
import structlog
|
||
from fastapi import APIRouter, BackgroundTasks, HTTPException, Request
|
||
from pydantic import BaseModel
|
||
|
||
from src.core.circuit_breaker import get_openclaw_guard
|
||
from src.core.metrics import (
|
||
record_alert_chain_failure,
|
||
record_alert_chain_success,
|
||
record_alert_processed,
|
||
record_anomaly,
|
||
)
|
||
from src.models.approval import (
|
||
ApprovalRequestCreate,
|
||
BlastRadius,
|
||
DataImpact,
|
||
RiskLevel,
|
||
)
|
||
from src.services.anomaly_counter import get_anomaly_counter
|
||
from src.services.approval_db import get_approval_service
|
||
from src.services.openclaw_http_service import get_openclaw_http_service
|
||
from src.services.sentry_service import get_sentry_service
|
||
# 2026-04-27 P3.1-T2 by Claude — Tier-2 三服務感知強化:補 SentryWebhookService 簽章驗證
|
||
from src.services.sentry_webhook_service import (
|
||
SentrySignatureError,
|
||
verify_sentry_signature,
|
||
)
|
||
from src.services.telegram_gateway import get_telegram_gateway
|
||
from src.utils.timezone import now_taipei_iso
|
||
|
||
logger = structlog.get_logger(__name__)
|
||
|
||
router = APIRouter(prefix="/webhooks/sentry", tags=["Sentry Webhook"])
|
||
|
||
# OpenClaw 配置 - 從 settings 讀取 (P1-1 修復, 2026-03-29)
|
||
# 2026-03-29: SENTRY_API_URL 已移至 settings.SENTRY_SELF_HOSTED_URL (Wave A.1)
|
||
|
||
# Sentry Level → Risk Level 映射
|
||
SENTRY_LEVEL_TO_RISK = {
|
||
"fatal": RiskLevel.CRITICAL,
|
||
"error": RiskLevel.HIGH,
|
||
"warning": RiskLevel.MEDIUM,
|
||
"info": RiskLevel.LOW,
|
||
}
|
||
|
||
# 去重配置 (Phase 10.2.1 - 2026-03-27)
|
||
SENTRY_DEDUP_TTL = 600 # 10 分鐘內不重複發送同一 issue
|
||
|
||
|
||
class SentryIssuePayload(BaseModel):
|
||
"""Sentry Issue Alert Payload (簡化版)"""
|
||
action: str # created, resolved, etc.
|
||
data: dict
|
||
actor: dict | None = None
|
||
|
||
|
||
class ErrorAnalysisResult(BaseModel):
|
||
"""錯誤分析結果"""
|
||
root_cause: str
|
||
impact: str
|
||
fix_suggestion: str
|
||
prevention: str
|
||
confidence: float
|
||
analyzed_by: str # ollama, claude
|
||
|
||
|
||
@router.get("/health")
|
||
async def sentry_webhook_health() -> dict:
|
||
"""Wave A.6 Smoke Test: Sentry Webhook 可達性探測"""
|
||
return {"status": "ok", "webhook": "sentry"}
|
||
|
||
|
||
@router.post("/error")
|
||
async def handle_sentry_error(
|
||
request: Request,
|
||
background_tasks: BackgroundTasks
|
||
):
|
||
"""
|
||
Sentry Issue Webhook Handler
|
||
|
||
觸發條件:
|
||
- Issue 新建 (action=created)
|
||
- Level: error 或 fatal
|
||
|
||
處理流程:
|
||
1. 解析 Sentry payload
|
||
2. 組裝錯誤上下文
|
||
3. 背景執行 OpenClaw 分析
|
||
4. 回寫 Sentry Comment
|
||
"""
|
||
try:
|
||
# 2026-04-27 P3.1-T2 by Claude — Tier-2 三服務感知強化:接入 SentryWebhookService 簽章驗證
|
||
body = await request.body()
|
||
sig_header = request.headers.get("sentry-hook-signature", "")
|
||
try:
|
||
verify_sentry_signature(body, sig_header)
|
||
except SentrySignatureError as sig_err:
|
||
logger.warning("sentry_signature_rejected", error=str(sig_err))
|
||
raise HTTPException(status_code=401, detail=str(sig_err)) from sig_err
|
||
|
||
payload = await request.json()
|
||
logger.info(f"Received Sentry webhook: action={payload.get('action')}")
|
||
|
||
# 只處理新建的 issue
|
||
if payload.get("action") != "triggered":
|
||
return {"status": "ignored", "reason": "action is not triggered"}
|
||
|
||
# 提取錯誤資訊
|
||
issue_data = payload.get("data", {}).get("issue", {})
|
||
|
||
# Phase 10.2.1: 去重檢查 (10 分鐘內不重複發送)
|
||
issue_id = issue_data.get("id")
|
||
sentry_service = get_sentry_service()
|
||
if not await sentry_service.check_dedup(issue_id, ttl=SENTRY_DEDUP_TTL):
|
||
return {"status": "deduplicated", "issue_id": issue_id, "ttl": SENTRY_DEDUP_TTL}
|
||
event_data = payload.get("data", {}).get("event", {})
|
||
|
||
error_context = {
|
||
"issue_id": issue_data.get("id"),
|
||
"title": issue_data.get("title"),
|
||
"culprit": issue_data.get("culprit"),
|
||
"level": issue_data.get("level"),
|
||
"first_seen": issue_data.get("firstSeen"),
|
||
"count": issue_data.get("count"),
|
||
"project": issue_data.get("project", {}).get("slug"),
|
||
|
||
# 事件詳情
|
||
"message": event_data.get("message"),
|
||
"platform": event_data.get("platform"),
|
||
"tags": event_data.get("tags", []),
|
||
|
||
# Stack trace (最後5個 frame)
|
||
"stacktrace": _extract_stacktrace(event_data),
|
||
}
|
||
|
||
# 判斷是否需要 AI 分析
|
||
level = issue_data.get("level", "error")
|
||
if level not in ["error", "fatal"]:
|
||
return {"status": "ignored", "reason": f"level {level} does not require analysis"}
|
||
|
||
# 背景執行分析
|
||
background_tasks.add_task(
|
||
analyze_and_comment,
|
||
error_context=error_context,
|
||
issue_id=issue_data.get("id"),
|
||
project_slug=issue_data.get("project", {}).get("slug"),
|
||
)
|
||
|
||
return {
|
||
"status": "accepted",
|
||
"issue_id": error_context["issue_id"],
|
||
"message": "Analysis scheduled"
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.exception("Sentry webhook processing failed")
|
||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||
|
||
|
||
def _extract_stacktrace(event_data: dict) -> list[dict]:
|
||
"""提取 Stack Trace (最後5個 frame)"""
|
||
try:
|
||
exception = event_data.get("exception", {})
|
||
values = exception.get("values", [])
|
||
if not values:
|
||
return []
|
||
|
||
stacktrace = values[0].get("stacktrace", {})
|
||
frames = stacktrace.get("frames", [])
|
||
|
||
# 取最後5個 frame,只保留關鍵資訊
|
||
return [
|
||
{
|
||
"filename": f.get("filename"),
|
||
"function": f.get("function"),
|
||
"lineno": f.get("lineno"),
|
||
"context_line": f.get("context_line"),
|
||
}
|
||
for f in frames[-5:]
|
||
]
|
||
except Exception:
|
||
return []
|
||
|
||
|
||
async def analyze_and_comment(
|
||
error_context: dict,
|
||
issue_id: str,
|
||
project_slug: str
|
||
):
|
||
"""
|
||
背景任務:分析錯誤 + Telegram 告警 + 建立 Approval
|
||
|
||
Phase 10: Sentry + OpenClaw AI 整合
|
||
Phase 21 (ADR-037): 異常頻率統計
|
||
|
||
執行順序 (避免邏輯衝突):
|
||
1. 記錄異常頻率 (AnomalyCounter)
|
||
2. 呼叫 OpenClaw 分析
|
||
3. 建立 Approval (含頻率資訊)
|
||
4. 發送 Telegram 告警 (含頻率資訊)
|
||
5. 回寫 Sentry Comment (含頻率資訊)
|
||
"""
|
||
try:
|
||
logger.info("sentry_analysis_started", issue_id=issue_id)
|
||
|
||
# 1. 記錄異常頻率 (ADR-037)
|
||
anomaly_counter = get_anomaly_counter()
|
||
anomaly_signature = {
|
||
"alert_name": "sentry_error",
|
||
"service": error_context.get("project", "unknown"),
|
||
"error_type": error_context.get("title", "unknown"),
|
||
"namespace": "sentry", # Sentry 來源統一標記
|
||
}
|
||
frequency = await anomaly_counter.record_anomaly(anomaly_signature)
|
||
frequency_dict = frequency.to_dict()
|
||
|
||
logger.info(
|
||
"anomaly_frequency_recorded",
|
||
issue_id=issue_id,
|
||
anomaly_key=frequency.anomaly_key,
|
||
count_24h=frequency.count_24h,
|
||
escalation_level=frequency.escalation_level,
|
||
)
|
||
|
||
# Wave A.5: 記錄異常指標 (ADR-037)
|
||
record_anomaly(
|
||
alert_name="sentry_error",
|
||
service=error_context.get("project", "unknown"),
|
||
frequency_24h=frequency.count_24h,
|
||
escalation_level=frequency.escalation_level,
|
||
)
|
||
|
||
# 2. 呼叫 OpenClaw 分析
|
||
analysis = await call_openclaw_analyzer(error_context)
|
||
|
||
# 3. 建立 Approval (含頻率資訊)
|
||
approval_id = await create_sentry_approval(
|
||
error_context=error_context,
|
||
analysis=analysis,
|
||
anomaly_frequency=frequency_dict,
|
||
)
|
||
|
||
# 4. 發送 Telegram 告警 (含頻率資訊)
|
||
await send_sentry_telegram_alert(
|
||
error_context=error_context,
|
||
analysis=analysis,
|
||
approval_id=approval_id,
|
||
anomaly_frequency=frequency_dict,
|
||
)
|
||
|
||
# 5. 回寫 Sentry Comment (如果分析成功,含頻率資訊)
|
||
if analysis:
|
||
await post_sentry_comment(
|
||
project_slug=project_slug,
|
||
issue_id=issue_id,
|
||
analysis=analysis,
|
||
anomaly_frequency=frequency_dict,
|
||
)
|
||
|
||
logger.info(
|
||
"sentry_analysis_completed",
|
||
issue_id=issue_id,
|
||
approval_id=approval_id,
|
||
has_analysis=analysis is not None,
|
||
escalation_level=frequency.escalation_level,
|
||
)
|
||
|
||
# Wave A.5: 記錄告警鏈路成功 (ADR-037)
|
||
record_alert_chain_success("sentry")
|
||
record_alert_processed(
|
||
source="sentry",
|
||
severity=error_context.get("level", "error"),
|
||
outcome="incident_created",
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.exception("sentry_analysis_failed", issue_id=issue_id, error=str(e))
|
||
# Wave A.5: 記錄告警鏈路失敗 (ADR-037)
|
||
record_alert_chain_failure("sentry")
|
||
|
||
|
||
async def call_openclaw_analyzer(error_context: dict) -> ErrorAnalysisResult | None:
|
||
"""
|
||
呼叫 OpenClaw Error Analyzer Agent
|
||
|
||
ADR-038: 雙層保護
|
||
- Layer 1: Circuit Breaker(5 連續失敗 → 斷路 60 秒)
|
||
- Layer 2: Concurrency Semaphore(最多 3 並發)
|
||
|
||
優先使用 Ollama (本地,零成本)
|
||
Fallback: Claude (高嚴重性)
|
||
|
||
Phase 22 P0 修復: 使用 OpenClawHttpService (2026-03-31)
|
||
"""
|
||
guard = get_openclaw_guard()
|
||
|
||
# ADR-038 Layer 1: Circuit Breaker 快速失敗
|
||
if guard.is_circuit_open():
|
||
logger.warning(
|
||
"openclaw_circuit_open",
|
||
metrics=guard.get_metrics(),
|
||
)
|
||
return None
|
||
|
||
# ADR-038 Layer 2: Concurrency Semaphore 排隊
|
||
async with guard.semaphore:
|
||
try:
|
||
# Phase 22 P0: 使用 Service 層而非直接 httpx
|
||
service = get_openclaw_http_service()
|
||
data = await service.analyze_error(
|
||
error_context=error_context,
|
||
prefer_local=True,
|
||
timeout=60.0,
|
||
)
|
||
|
||
if data:
|
||
guard.record_success()
|
||
return ErrorAnalysisResult(**data)
|
||
else:
|
||
guard.record_failure()
|
||
return None
|
||
|
||
except Exception as e:
|
||
logger.exception(f"OpenClaw call failed: {e}")
|
||
guard.record_failure()
|
||
return None
|
||
|
||
|
||
async def post_sentry_comment(
|
||
project_slug: str,
|
||
issue_id: str,
|
||
analysis: ErrorAnalysisResult,
|
||
anomaly_frequency: dict | None = None,
|
||
):
|
||
"""
|
||
回寫分析結果到 Sentry Issue Comment
|
||
|
||
API: POST /api/0/issues/{issue_id}/comments/
|
||
Phase 21 (ADR-037): 含異常頻率統計
|
||
"""
|
||
# 頻率統計區塊 (ADR-037)
|
||
frequency_section = ""
|
||
if anomaly_frequency and anomaly_frequency.get("count_24h", 0) > 1:
|
||
freq = anomaly_frequency
|
||
escalation_emoji = {
|
||
None: "",
|
||
"REPEAT": ":warning:",
|
||
"ESCALATE": ":red_circle:",
|
||
"PERMANENT_FIX": ":rotating_light:",
|
||
}.get(freq.get("escalation_level"), "")
|
||
|
||
frequency_section = f"""
|
||
## 頻率統計 {escalation_emoji}
|
||
|
||
| 時間窗口 | 次數 |
|
||
|---------|------|
|
||
| 1 小時 | {freq.get('count_1h', 0)} |
|
||
| 24 小時 | {freq.get('count_24h', 0)} |
|
||
| 7 天 | {freq.get('count_7d', 0)} |
|
||
| 30 天 | {freq.get('count_30d', 0)} |
|
||
|
||
**修復嘗試**: {freq.get('auto_repair_count', 0)} 次
|
||
"""
|
||
if freq.get("escalation_level"):
|
||
frequency_section += f"**升級建議**: {freq['escalation_level']}\n"
|
||
|
||
comment_text = f"""## AI 錯誤分析 (by {analysis.analyzed_by})
|
||
|
||
**根本原因 (Root Cause)**
|
||
{analysis.root_cause}
|
||
|
||
**影響範圍 (Impact)**
|
||
{analysis.impact}
|
||
|
||
**建議修復 (Fix Suggestion)**
|
||
{analysis.fix_suggestion}
|
||
|
||
**預防措施 (Prevention)**
|
||
{analysis.prevention}
|
||
{frequency_section}
|
||
---
|
||
*分析信心度: {analysis.confidence:.0%} | 分析時間: {now_taipei_iso()}*
|
||
"""
|
||
|
||
try:
|
||
# Wave A.4: 使用 SentryService 回寫 Comment (ADR-037, 2026-03-29)
|
||
# 符合 leWOOOgo 模組化原則: Router 層透過 Service 層存取外部 API
|
||
sentry_service = get_sentry_service()
|
||
result = await sentry_service.post_issue_comment(issue_id, comment_text)
|
||
|
||
if result:
|
||
logger.info(
|
||
"sentry_comment_success",
|
||
issue_id=issue_id,
|
||
comment_id=result.get("id"),
|
||
)
|
||
else:
|
||
# Token 未配置或 API 失敗時記錄 (不中斷流程)
|
||
logger.warning("sentry_comment_skipped_or_failed", issue_id=issue_id)
|
||
|
||
except Exception as e:
|
||
logger.exception("sentry_comment_failed", issue_id=issue_id, error=str(e))
|
||
|
||
|
||
# =============================================================================
|
||
# Telegram 告警 (Phase 10: Sentry + OpenClaw)
|
||
# =============================================================================
|
||
|
||
async def send_sentry_telegram_alert(
|
||
error_context: dict,
|
||
analysis: ErrorAnalysisResult | None,
|
||
approval_id: str,
|
||
anomaly_frequency: dict | None = None,
|
||
):
|
||
"""
|
||
發送 Sentry 錯誤告警到 Telegram
|
||
|
||
Phase 21 (ADR-037): 含異常頻率統計
|
||
|
||
格式 (project_sentry_openclaw_v2.md):
|
||
═══════════════════════════
|
||
🐛 SENTRY 錯誤告警
|
||
═══════════════════════════
|
||
📍 components/dashboard.tsx:142
|
||
❌ TypeError: Cannot read property 'x' of null
|
||
📊 頻率: 1h:3 / 24h:12 / 7d:42
|
||
───────────────────────────
|
||
🧠 OpenClaw 分析:
|
||
「這是 null check 問題...」
|
||
───────────────────────────
|
||
[ Y 建立修復任務 ] [ n 忽略 ]
|
||
"""
|
||
try:
|
||
telegram = get_telegram_gateway()
|
||
await telegram.initialize()
|
||
|
||
# 提取錯誤資訊
|
||
title = error_context.get("title", "Unknown Error")
|
||
culprit = error_context.get("culprit", "unknown")
|
||
level = error_context.get("level", "error")
|
||
|
||
# 發送 Sentry 告警卡片 (含 Y/n 按鈕)
|
||
# TODO(2026-04-05): Sentry 路徑無 incident_id,待 Sentry→Incident 關聯後補傳
|
||
await telegram.send_approval_card(
|
||
approval_id=approval_id,
|
||
risk_level="high" if level in ["fatal", "error"] else "medium",
|
||
resource_name=culprit,
|
||
root_cause=analysis.root_cause if analysis else title,
|
||
suggested_action=analysis.fix_suggestion if analysis else "待分析",
|
||
primary_responsibility="FE" if "tsx" in culprit or "jsx" in culprit else "BE",
|
||
confidence=analysis.confidence if analysis else 0.0,
|
||
namespace="sentry",
|
||
anomaly_frequency=anomaly_frequency,
|
||
# 2026-04-02 ogt: 修復 ai_provider 未傳遞 → Telegram 顯示「AI 仲裁判定」而非具體模型名稱
|
||
ai_provider=analysis.analyzed_by if analysis else "",
|
||
)
|
||
|
||
logger.info(
|
||
"sentry_telegram_sent",
|
||
approval_id=approval_id,
|
||
escalation_level=anomaly_frequency.get("escalation_level") if anomaly_frequency else None,
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.exception("sentry_telegram_failed", error=str(e))
|
||
|
||
|
||
# =============================================================================
|
||
# Approval 創建 (Phase 10: Sentry + OpenClaw)
|
||
# =============================================================================
|
||
|
||
async def create_sentry_approval(
|
||
error_context: dict,
|
||
analysis: ErrorAnalysisResult | None,
|
||
anomaly_frequency: dict | None = None,
|
||
) -> str:
|
||
"""
|
||
為 Sentry 錯誤建立 Approval 記錄
|
||
|
||
Phase 21 (ADR-037): 含異常頻率統計
|
||
|
||
Returns:
|
||
str: Approval ID
|
||
"""
|
||
try:
|
||
approval_service = get_approval_service()
|
||
|
||
# 決定風險等級 (考慮頻率升級)
|
||
level = error_context.get("level", "error")
|
||
risk_level = SENTRY_LEVEL_TO_RISK.get(level, RiskLevel.MEDIUM)
|
||
|
||
# ADR-037: 根據頻率升級風險等級
|
||
if anomaly_frequency:
|
||
escalation = anomaly_frequency.get("escalation_level")
|
||
if escalation == "PERMANENT_FIX":
|
||
risk_level = RiskLevel.CRITICAL
|
||
elif escalation == "ESCALATE" and risk_level != RiskLevel.CRITICAL:
|
||
risk_level = RiskLevel.HIGH
|
||
|
||
# 組裝 Approval 請求
|
||
title = error_context.get("title", "Unknown Error")
|
||
culprit = error_context.get("culprit", "unknown")
|
||
project = error_context.get("project", "unknown")
|
||
issue_id = error_context.get("issue_id", "unknown")
|
||
|
||
# 組裝 metadata (含頻率資訊)
|
||
metadata = {
|
||
"source": "sentry",
|
||
"alert_type": f"sentry_{level}",
|
||
"sentry_issue_id": issue_id,
|
||
"sentry_project": project,
|
||
"culprit": culprit,
|
||
"error_count": error_context.get("count", 1),
|
||
"first_seen": error_context.get("first_seen"),
|
||
"stacktrace": error_context.get("stacktrace", []),
|
||
"llm_provider": analysis.analyzed_by if analysis else "pending",
|
||
"llm_confidence": analysis.confidence if analysis else 0.0,
|
||
}
|
||
|
||
# ADR-037: 添加頻率資訊到 metadata
|
||
if anomaly_frequency:
|
||
metadata["anomaly_frequency"] = anomaly_frequency
|
||
|
||
# 組裝 Approval 請求 (符合 ApprovalRequestBase schema)
|
||
approval_request = ApprovalRequestCreate(
|
||
action=f"Sentry {level.upper()} Alert: {culprit}",
|
||
description=f"{title}\n\nRoot Cause: {analysis.root_cause if analysis else '待分析'}\nSuggestion: {analysis.fix_suggestion if analysis else '待 AI 分析'}",
|
||
risk_level=risk_level,
|
||
blast_radius=BlastRadius(
|
||
affected_pods=1,
|
||
estimated_downtime="0",
|
||
related_services=[project],
|
||
data_impact=DataImpact.READ_ONLY,
|
||
),
|
||
dry_run_checks=[], # Sentry 告警無 dry-run
|
||
requested_by="sentry-webhook",
|
||
metadata=metadata,
|
||
)
|
||
|
||
# 創建 Approval (ID 由 Service 自動生成)
|
||
approval = await approval_service.create_approval(request=approval_request)
|
||
approval_id = str(approval.id)
|
||
|
||
logger.info(
|
||
"sentry_approval_created",
|
||
approval_id=approval_id,
|
||
issue_id=issue_id,
|
||
risk_level=risk_level.value,
|
||
)
|
||
|
||
return approval_id
|
||
|
||
except Exception as e:
|
||
logger.exception("sentry_approval_creation_failed", error=str(e))
|
||
# 即使創建失敗,也返回一個臨時 ID
|
||
return f"temp-{uuid.uuid4().hex[:8]}"
|