"""SQLAlchemy 2.x ORM models,对应 DESIGN.md §7.4 schema。 5 张表:users / tasks / messages / runs / usage_events。 - users 本地形态固定 INSERT sentinel(`00000000-...`) - messages.payload 用 jsonb,GIN 索引在 migration 里建 - runs / usage_events 在 B 阶段先建表,真正写入要等 D 阶段(HTTP /v1 + run 生命周期) """ from __future__ import annotations from datetime import datetime from decimal import Decimal from typing import Any, Optional from uuid import UUID, uuid4 from sqlalchemy import ( BigInteger, DateTime, ForeignKey, Integer, Numeric, Text, UniqueConstraint, func, ) from sqlalchemy.dialects.postgresql import JSONB, UUID as PG_UUID from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column class Base(DeclarativeBase): pass # 本地单用户 sentinel —— 所有本地 task 都 FK 到这一行 SENTINEL_USER_ID: UUID = UUID("00000000-0000-0000-0000-000000000000") class User(Base): __tablename__ = "users" user_id: Mapped[UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid4) email: Mapped[Optional[str]] = mapped_column(Text, nullable=True) oidc_subject: Mapped[Optional[str]] = mapped_column(Text, nullable=True) password_hash: Mapped[Optional[str]] = mapped_column(Text, nullable=True) plan: Mapped[Optional[str]] = mapped_column(Text, nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), nullable=False ) class Task(Base): __tablename__ = "tasks" task_id: Mapped[UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid4) user_id: Mapped[UUID] = mapped_column( PG_UUID(as_uuid=True), ForeignKey("users.user_id"), nullable=False ) name: Mapped[str] = mapped_column(Text, nullable=False) working_dir: Mapped[str] = mapped_column(Text, nullable=False) skill: Mapped[str] = mapped_column(Text, nullable=False, default="") description: Mapped[str] = mapped_column(Text, nullable=False, default="") status: Mapped[str] = mapped_column(Text, nullable=False, default="active") model: Mapped[str] = mapped_column(Text, nullable=False, default="") model_profile: Mapped[str] = mapped_column(Text, nullable=False, default="") reasoning_effort: Mapped[str] = mapped_column(Text, nullable=False, default="") tokens_prompt: Mapped[int] = mapped_column(Integer, nullable=False, default=0) tokens_completion: Mapped[int] = mapped_column(Integer, nullable=False, default=0) cost_usd: Mapped[Decimal] = mapped_column(Numeric(12, 6), nullable=False, default=0) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), nullable=False ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False ) class Message(Base): __tablename__ = "messages" __table_args__ = (UniqueConstraint("task_id", "idx", name="uq_messages_task_idx"),) message_id: Mapped[UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid4) task_id: Mapped[UUID] = mapped_column( PG_UUID(as_uuid=True), ForeignKey("tasks.task_id", ondelete="CASCADE"), nullable=False, ) idx: Mapped[int] = mapped_column(Integer, nullable=False) payload: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False) tokens_in: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) tokens_out: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), nullable=False ) class Run(Base): __tablename__ = "runs" run_id: Mapped[UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid4) task_id: Mapped[UUID] = mapped_column( PG_UUID(as_uuid=True), ForeignKey("tasks.task_id", ondelete="CASCADE"), nullable=False, ) status: Mapped[str] = mapped_column(Text, nullable=False, default="pending") started_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True) finished_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True) error: Mapped[Optional[str]] = mapped_column(Text, nullable=True) tokens_p: Mapped[int] = mapped_column(Integer, nullable=False, default=0) tokens_c: Mapped[int] = mapped_column(Integer, nullable=False, default=0) class UsageEvent(Base): """append-only 审计。task_id / run_id 不 FK,task 硬删后审计仍存活(§7.4)。""" __tablename__ = "usage_events" id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) user_id: Mapped[UUID] = mapped_column(PG_UUID(as_uuid=True), nullable=False) task_id: Mapped[Optional[UUID]] = mapped_column(PG_UUID(as_uuid=True), nullable=True) run_id: Mapped[Optional[UUID]] = mapped_column(PG_UUID(as_uuid=True), nullable=True) kind: Mapped[str] = mapped_column(Text, nullable=False) value: Mapped[Decimal] = mapped_column(Numeric(20, 8), nullable=False) ts: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), nullable=False )