"""任务元数据: Session 上层,落 PG `tasks` 表(§7 B Step 3)。 Session 只管对话消息;Task 管 mode/description/status/model/tokens/cost/时间戳 —— 跨轮次共享的元数据,DESIGN.md §7.1 / §7.4 规约。 state.json 已废除;字段从 PG 读出,save() 走 INSERT ... ON CONFLICT DO UPDATE。 created_at / updated_at 由 PG server_default / onupdate 管,Python 侧只读。 """ from __future__ import annotations from dataclasses import dataclass from datetime import datetime from typing import Optional from uuid import UUID from .storage import upsert_task from .storage.models import Task as TaskRow from .storage.utils import get_task def _iso(dt: Optional[datetime]) -> str: return dt.isoformat(timespec="seconds") if dt else "" @dataclass class TaskState: task_id: str # UUID 字符串形式(对外展示用,DB 仍是 UUID) user_id: UUID # 归属 user,UPSERT INSERT 路径必填(列 NOT NULL) name: str = "" # 任务显示名(列 NOT NULL,新建必填;resume 时从 DB 读) working_dir: str = "" # 工作目录(db 形态:ROOT 内相对 / ROOT 外绝对;空=未绑) skill: str = "" # 智能体类型(coding / ppt / proposal / 自由形式,后续可对齐 skills/ 注册表) description: str = "" # 一句话描述,便于列表识别 status: str = "active" # active / completed / abandoned model: str = "" # caps.model_id model_profile: str = "" # 档案名,如 deepseek_v4.flash reasoning_effort: str = "" tokens_prompt: int = 0 tokens_completion: int = 0 cost_cny: float = 0.0 created_at: str = "" # PG server_default 填,Python 侧只读 updated_at: str = "" @property def tokens_total(self) -> int: return self.tokens_prompt + self.tokens_completion def save(self) -> None: """UPSERT 到 PG。created_at / updated_at 不参与写入(PG 自动管)。""" upsert_task( UUID(self.task_id), user_id=self.user_id, name=self.name, working_dir=self.working_dir, skill=self.skill, description=self.description, status=self.status, model=self.model, model_profile=self.model_profile, reasoning_effort=self.reasoning_effort, tokens_prompt=self.tokens_prompt, tokens_completion=self.tokens_completion, ) @classmethod def from_row(cls, row: TaskRow) -> "TaskState": return cls( task_id=str(row.task_id), user_id=row.user_id, name=row.name, working_dir=row.working_dir, skill=row.skill, description=row.description, status=row.status, model=row.model, model_profile=row.model_profile, reasoning_effort=row.reasoning_effort, tokens_prompt=row.tokens_prompt, tokens_completion=row.tokens_completion, cost_cny=float(row.cost_cny or 0), created_at=_iso(row.created_at), updated_at=_iso(row.updated_at), ) @classmethod def load(cls, task_id: UUID) -> Optional["TaskState"]: """从 PG 读;不存在返回 None。""" row = get_task(task_id) return cls.from_row(row) if row is not None else None