83 lines
3.0 KiB
Python
83 lines
3.0 KiB
Python
"""任务元数据: 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)
|
|
task_dir: str = "" # 绝对路径或留空(留空= ChatGPT thread 默认派生,§7.1)
|
|
mode: str = "" # coding / ppt / proposal / general / 自由形式
|
|
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_usd: 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),
|
|
task_dir=self.task_dir,
|
|
mode=self.mode,
|
|
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),
|
|
task_dir=row.task_dir,
|
|
mode=row.mode,
|
|
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_usd=float(row.cost_usd 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
|