zcbot/core/task.py

89 lines
3.3 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)
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