"""群发消息相关数据模型。""" from __future__ import annotations from typing import Optional, Literal from pydantic import BaseModel, Field class BatchSendRequest(BaseModel): """群发请求。 三层幂等: - 批级:client_request_id(None 时自动生成 batch_id) - 单条:内部按 f"{batch_id}:{to_wxid}" 作为 client_request_id 传给 orchestrator - orchestrator:复用 IdemCache(TTL=300s) """ targets: list[str] = Field( ..., min_length=1, max_length=200, description="目标 wxid 列表(去重后 ≤ 200)", ) content: str = Field( ..., min_length=1, max_length=2000, description="消息内容(支持 {nickname} 占位符,发送前按目标联系人替换)", ) client_request_id: Optional[str] = Field( None, description="可选:批级幂等 ID,None 时自动生成 batch_id", ) abort_on_consecutive_fail: int = Field( 5, ge=1, le=20, description="连续失败 N 次中止整批(剩余标记 skipped)", ) class BatchItemResult(BaseModel): """单条群发结果。""" to_wxid: str # unknown: 发送被 cancel 中断,实际发送状态未知(send_queue worker 可能已发出) status: Literal["success", "failed", "skipped", "pending", "unknown"] error_code: Optional[str] = None error_message: Optional[str] = None verified: Optional[bool] = None class BatchSendStatus(BaseModel): """群发任务状态。""" batch_id: str status: Literal["pending", "running", "completed", "failed", "cancelled"] = "pending" total: int = 0 processed: int = 0 success: int = 0 failed: int = 0 skipped: int = 0 progress: float = Field(0.0, ge=0.0, le=1.0) estimated_remaining_sec: Optional[int] = None abort_reason: Optional[str] = None items: Optional[list[BatchItemResult]] = None # 详细结果(可选,列表大时省略)