Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ class AccountRecord(BaseModel):
preview_concurrency_time: str = ""
ticket_pool_size: int = 0 # 0 = disabled; N > 0 = pool mode: collect N tickets first
ticket_pool_drain_interval_ms: int = 0 # 0 = parallel drain; N > 0 = serial drain interval
preview_warmup_lead_seconds: int = 0 # scheduler fires this many seconds before scheduled_start_time
stock_monitor_enabled: bool = False
stock_monitor_last_checked_at: str | None = None
stock_monitor_last_message: str = ""
Expand Down Expand Up @@ -130,6 +131,7 @@ class PublicAccountRecord(BaseModel):
preview_concurrency_time: str = ""
ticket_pool_size: int = 0
ticket_pool_drain_interval_ms: int = 0
preview_warmup_lead_seconds: int = 0
invitation_code: str = DEFAULT_INVITATION_CODE
stock_monitor_enabled: bool = False
stock_monitor_last_checked_at: str | None = None
Expand Down Expand Up @@ -289,6 +291,7 @@ class AccountPreferencesRequest(BaseModel):
preview_concurrency_time: str | None = None
ticket_pool_size: int | None = None
ticket_pool_drain_interval_ms: int | None = None
preview_warmup_lead_seconds: int | None = None
schedule_enabled: bool | None = None
scheduled_start_time: str | None = None

Expand Down Expand Up @@ -324,6 +327,18 @@ def validate_ticket_pool_drain_interval_ms(cls, value: int | None) -> int | None
raise ValueError("ticket 发射间隔不能超过 10000ms")
return v

@field_validator("preview_warmup_lead_seconds")
@classmethod
def validate_preview_warmup_lead_seconds(cls, value: int | None) -> int | None:
if value is None:
return None
v = int(value)
if v < 0:
raise ValueError("warmup 提前秒数不能为负数")
if v > 120:
raise ValueError("warmup 提前秒数不能超过 120 秒")
return v


class NetworkModeRequest(BaseModel):
"""Runtime network egress mode switch payload."""
Expand Down
2 changes: 2 additions & 0 deletions app/runtime_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@
"randstr",
"sign",
"ticket",
"ticket_value",
"token",
"randstr_value",
}


Expand Down
42 changes: 30 additions & 12 deletions app/services/account_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import secrets
import shutil
import logging
import threading
from datetime import datetime, timezone
from functools import lru_cache
from http.cookies import SimpleCookie
Expand Down Expand Up @@ -49,6 +50,22 @@ def __init__(self) -> None:
self.settings = settings
self.accounts_store = JsonFileStore(settings.accounts_path, default_factory=list)
self.tasks_store = JsonFileStore(settings.tasks_path, default_factory=list)
self._session_stores: dict[str, JsonFileStore] = {}
self._session_stores_lock = threading.Lock()

def _get_session_store(self, account_id: str) -> JsonFileStore:
with self._session_stores_lock:
store = self._session_stores.get(account_id)
if store is None:
store = JsonFileStore(
self._session_path(account_id),
default_factory=lambda aid=account_id: AccountSessionState(
account_id=aid,
updated_at=utc_now_iso(),
).model_dump(),
)
self._session_stores[account_id] = store
return store

def list_accounts(self) -> list[PublicAccountRecord]:
accounts = [AccountRecord.model_validate(item) for item in self.accounts_store.read()]
Expand Down Expand Up @@ -132,6 +149,12 @@ def updater(records: list[dict[str, Any]]) -> list[dict[str, Any]]:
)
if existing
else 0,
preview_warmup_lead_seconds=max(
0,
min(120, int(existing.get("preview_warmup_lead_seconds") or 0)),
)
if existing
else 0,
stock_monitor_enabled=bool(existing.get("stock_monitor_enabled")) if existing else False,
stock_monitor_last_checked_at=existing.get("stock_monitor_last_checked_at") if existing else None,
stock_monitor_last_message=str(existing.get("stock_monitor_last_message") or "") if existing else "",
Expand Down Expand Up @@ -284,6 +307,8 @@ def update_preferences(self, account_id: str, request: AccountPreferencesRequest
account.ticket_pool_size = max(0, min(50, int(request.ticket_pool_size)))
if request.ticket_pool_drain_interval_ms is not None:
account.ticket_pool_drain_interval_ms = max(0, min(10_000, int(request.ticket_pool_drain_interval_ms)))
if request.preview_warmup_lead_seconds is not None:
account.preview_warmup_lead_seconds = max(0, min(120, int(request.preview_warmup_lead_seconds)))
if self._should_skip_today_after_schedule_update(
account=account,
previous_schedule_enabled=previous_schedule_enabled,
Expand Down Expand Up @@ -328,22 +353,12 @@ def _scheduled_run_key(self, current_date: str, scheduled_start_time: str) -> st
return f"{current_date}|{(scheduled_start_time or '').strip()}"

def load_session(self, account_id: str) -> AccountSessionState:
path = self._session_path(account_id)
store = JsonFileStore(
path,
default_factory=lambda: AccountSessionState(
account_id=account_id,
updated_at=utc_now_iso(),
).model_dump(),
)
store = self._get_session_store(account_id)
return AccountSessionState.model_validate(store.read())

def save_session(self, session: AccountSessionState) -> AccountSessionState:
session.updated_at = utc_now_iso()
store = JsonFileStore(
self._session_path(session.account_id),
default_factory=dict,
)
store = self._get_session_store(session.account_id)
store.write(session.model_dump())
return session

Expand Down Expand Up @@ -391,6 +406,8 @@ def update_tasks(records: list[dict[str, Any]]) -> list[dict[str, Any]]:
session_path = self._session_path(account_id)
if session_path.exists():
session_path.unlink()
with self._session_stores_lock:
self._session_stores.pop(account_id, None)
self._remove_account_artifacts(account_id)
get_runtime_log_service().log_account_event(
account_id=account_id,
Expand Down Expand Up @@ -431,6 +448,7 @@ def to_public_account(self, account: AccountRecord) -> PublicAccountRecord:
preview_concurrency_time=account.preview_concurrency_time,
ticket_pool_size=account.ticket_pool_size,
ticket_pool_drain_interval_ms=account.ticket_pool_drain_interval_ms,
preview_warmup_lead_seconds=account.preview_warmup_lead_seconds,
invitation_code=account.invitation_code,
stock_monitor_enabled=account.stock_monitor_enabled,
stock_monitor_last_checked_at=account.stock_monitor_last_checked_at,
Expand Down
Loading