"""供 T-304 使用的稳定本地配置与恢复快照。""" from __future__ import annotations from dataclasses import dataclass, field import re from cmbuyer_client.core.models import ClaimRequest, ClaimedTask, DeviceCredentials, RenewRequest from cmbuyer_client.core.validation import require_string, require_uuid4 LOOPBACK_SERVICE_URL = "http://127.0.0.1:8080" PROFILE_ID_RE = re.compile(r"[a-z0-9][a-z0-9_-]{0,63}") @dataclass(frozen=True) class ProfileSettings: profile_id: str service_url: str device_id: str adb_path: str adb_serial: str transport: str poll_interval_seconds: int = 15 failure_threshold: int = 3 http_timeout_seconds: int = 10 step_timeout_seconds: int = 45 def __post_init__(self) -> None: if not isinstance(self.profile_id, str) or PROFILE_ID_RE.fullmatch(self.profile_id) is None: raise ValueError("invalid_profile_id") if self.service_url != LOOPBACK_SERVICE_URL: raise ValueError("service_url_not_allowed") require_uuid4(self.device_id, "invalid_device_id") require_string(self.adb_path, "invalid_adb_path", maximum=1024) require_string(self.adb_serial, "invalid_adb_serial", maximum=200) if self.transport not in ("usb", "wifi"): raise ValueError("invalid_transport") _range(self.poll_interval_seconds, 5, 300, "invalid_poll_interval") _range(self.failure_threshold, 1, 10, "invalid_failure_threshold") _range(self.http_timeout_seconds, 1, 120, "invalid_http_timeout") _range(self.step_timeout_seconds, 5, 300, "invalid_step_timeout") @dataclass(frozen=True, repr=False) class LoadedProfile: settings: ProfileSettings credentials: DeviceCredentials = field(repr=False) def __repr__(self) -> str: return f"LoadedProfile(settings={self.settings!r}, credentials=[已隐藏])" @dataclass(frozen=True) class PollingSession: profile_id: str session_id: str accept_new: bool def __post_init__(self) -> None: require_uuid4(self.session_id, "invalid_session_id") if type(self.accept_new) is not bool: raise ValueError("invalid_accept_new") @dataclass(frozen=True) class PendingEvidence: task_id: str attempt_id: str kind: str upload_key: str status: str @dataclass(frozen=True) class RecoverySnapshot: session: PollingSession | None pending_claim: ClaimRequest | None active_claim: ClaimedTask | None = field(repr=False) pending_renew: RenewRequest | None = field(repr=False) pending_evidence: tuple[PendingEvidence, ...] def _range(value: object, minimum: int, maximum: int, reason: str) -> None: if type(value) is not int or not minimum <= value <= maximum: raise ValueError(reason)