feat(wordcloud): 收口在途开发(布局/存储/前端)+ R4 WCD 生产任务(jobs wcd_file)与生产订单列表

This commit is contained in:
2026-08-13 14:22:48 +08:00
parent 1d17b5e20d
commit e518540235
32 changed files with 3525 additions and 592 deletions
+54 -3
View File
@@ -6,7 +6,9 @@ import uuid
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional
from .metadata_store import MetadataStore
from .schemas import JobDetail, JobEvent, JobStatus
@@ -18,9 +20,20 @@ class JobState:
class JobManager:
def __init__(self) -> None:
def __init__(self, store: Optional[MetadataStore] = None) -> None:
self._lock = threading.Lock()
self.store = store
self._jobs: dict[str, JobState] = {}
self._restore_from_store()
def _restore_from_store(self) -> None:
if not self.store:
return
for status in self.store.load_jobs():
self._jobs[status.job_id] = JobState(
status=status,
events=self.store.load_events(status.job_id, limit=100),
)
def create_job(self) -> str:
job_id = uuid.uuid4().hex
@@ -38,6 +51,8 @@ class JobManager:
)
with self._lock:
self._jobs[job_id] = JobState(status=status)
if self.store:
self.store.upsert_job(status)
return job_id
def exists(self, job_id: str) -> bool:
@@ -53,11 +68,19 @@ class JobManager:
state = self._jobs[job_id]
return JobDetail(status=state.status, recent_events=state.events[-100:])
def delete_job(self, job_id: str) -> None:
with self._lock:
self._jobs.pop(job_id, None)
if self.store:
self.store.delete_job(job_id)
def set_artifacts(self, job_id: str, artifacts: dict[str, str]) -> None:
with self._lock:
status = self._jobs[job_id].status
status.artifacts = artifacts
status.updated_at = datetime.now(timezone.utc)
if self.store:
self.store.upsert_job(status)
def set_status(
self,
@@ -68,6 +91,7 @@ class JobManager:
progress_percent: int,
message: str,
error: str | None = None,
elapsed_seconds: float | None = None,
) -> None:
with self._lock:
s = self._jobs[job_id].status
@@ -78,14 +102,28 @@ class JobManager:
s.updated_at = datetime.now(timezone.utc)
if error is not None:
s.error = error
if elapsed_seconds is not None:
s.elapsed_seconds = elapsed_seconds
if self.store:
self.store.upsert_job(s)
def add_event(self, job_id: str, *, kind: str, stage: str, progress_percent: int, message: str) -> None:
def add_event(
self,
job_id: str,
*,
kind: str,
stage: str,
progress_percent: int,
message: str,
elapsed_seconds: float | None = None,
) -> None:
event = JobEvent(
type=kind,
stage=stage,
progress_percent=progress_percent,
message=message,
timestamp=datetime.now(timezone.utc),
elapsed_seconds=elapsed_seconds,
)
with self._lock:
state = self._jobs[job_id]
@@ -94,13 +132,26 @@ class JobManager:
state.status.progress_percent = progress_percent
state.status.message = message
state.status.updated_at = event.timestamp
if elapsed_seconds is not None:
state.status.elapsed_seconds = elapsed_seconds
for sub in state.subscribers:
sub.put(event)
if self.store:
self.store.add_event(job_id, event)
self.store.upsert_job(state.status)
def subscribe(self, job_id: str) -> queue.Queue:
q: queue.Queue = queue.Queue()
with self._lock:
self._jobs[job_id].subscribers.append(q)
state = self._jobs[job_id]
# A fast job can emit preview_ready/completed before the browser
# finishes opening the SSE connection. Replay the existing event
# history into this subscriber so timing and preview updates are
# never lost; the lock also makes the snapshot/registration
# atomic with respect to new events.
for event in state.events:
q.put(event)
state.subscribers.append(q)
return q
def unsubscribe(self, job_id: str, q: queue.Queue) -> None: