feat: Phase 0 — monorepo 骨架 + 全表迁移 + FastAPI/Next 骨架 + CI
- uv(workspace) + pnpm monorepo;docker-compose(pg+api+web) - SQLAlchemy 16 MVP 表 + Alembic 初版迁移(无漂移,users stub) - FastAPI 骨架:统一错误信封(带 request_id) + structlog + /jobs/:id + OpenAPI - Next.js 骨架:纸感主题 token + OpenAPI→TS 客户端代码生成(gen:api) - CI(ruff/mypy/pytest + pg service + alembic 漂移校验) - 四份设计规格(PRODUCT/UX/ARCHITECTURE/DEV_PLAN) + CLAUDE.md
This commit is contained in:
236
packages/db/ww_db/models.py
Normal file
236
packages/db/ww_db/models.py
Normal file
@@ -0,0 +1,236 @@
|
||||
"""ORM 模型——全部 MVP 表(ARCH §3.1)。
|
||||
|
||||
snake_case;无向量列;P2 表 timeline/decisions 不建(见 DEV_PLAN T0.2)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import (
|
||||
BigInteger,
|
||||
ForeignKey,
|
||||
Integer,
|
||||
LargeBinary,
|
||||
Text,
|
||||
UniqueConstraint,
|
||||
text,
|
||||
)
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from ww_db.base import Base, CreatedAt, TimestampedMixin, UuidPk
|
||||
|
||||
# ---- 运营/系统表 ----
|
||||
|
||||
|
||||
class User(UuidPk, CreatedAt, Base):
|
||||
__tablename__ = "users"
|
||||
email: Mapped[str] = mapped_column(Text, unique=True, nullable=False)
|
||||
display_name: Mapped[str | None] = mapped_column(Text)
|
||||
|
||||
|
||||
# ---- 创作数据表 ----
|
||||
|
||||
|
||||
class Project(UuidPk, TimestampedMixin, Base):
|
||||
__tablename__ = "projects"
|
||||
owner_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("users.id"), nullable=False)
|
||||
title: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
genre: Mapped[str | None] = mapped_column(Text)
|
||||
logline: Mapped[str | None] = mapped_column(Text)
|
||||
premise: Mapped[str | None] = mapped_column(Text)
|
||||
theme: Mapped[str | None] = mapped_column(Text)
|
||||
selling_points: Mapped[list[Any]] = mapped_column(JSONB, server_default=text("'[]'"))
|
||||
structure: Mapped[str | None] = mapped_column(Text)
|
||||
|
||||
|
||||
class Character(UuidPk, CreatedAt, Base):
|
||||
__tablename__ = "characters"
|
||||
project_id: Mapped[uuid.UUID] = mapped_column(
|
||||
ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
name: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
role: Mapped[str | None] = mapped_column(Text)
|
||||
traits: Mapped[dict[str, Any] | None] = mapped_column(JSONB)
|
||||
appearance: Mapped[str | None] = mapped_column(Text)
|
||||
motive: Mapped[str | None] = mapped_column(Text)
|
||||
backstory: Mapped[str | None] = mapped_column(Text)
|
||||
arc: Mapped[dict[str, Any] | None] = mapped_column(JSONB)
|
||||
speech_tics: Mapped[dict[str, Any] | None] = mapped_column(JSONB)
|
||||
tags: Mapped[list[Any]] = mapped_column(JSONB, server_default=text("'[]'"))
|
||||
relations: Mapped[list[Any]] = mapped_column(JSONB, server_default=text("'[]'"))
|
||||
first_chapter: Mapped[int | None] = mapped_column(Integer)
|
||||
latest_state: Mapped[str | None] = mapped_column(Text)
|
||||
|
||||
|
||||
class WorldEntity(UuidPk, CreatedAt, Base):
|
||||
__tablename__ = "world_entities"
|
||||
project_id: Mapped[uuid.UUID] = mapped_column(
|
||||
ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
type: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
name: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
rules: Mapped[dict[str, Any] | None] = mapped_column(JSONB)
|
||||
first_chapter: Mapped[int | None] = mapped_column(Integer)
|
||||
latest_state: Mapped[str | None] = mapped_column(Text)
|
||||
|
||||
|
||||
class Outline(UuidPk, Base):
|
||||
__tablename__ = "outline"
|
||||
__table_args__ = (UniqueConstraint("project_id", "chapter_no"),)
|
||||
project_id: Mapped[uuid.UUID] = mapped_column(
|
||||
ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
volume: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
chapter_no: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
beats: Mapped[dict[str, Any] | None] = mapped_column(JSONB)
|
||||
foreshadow_windows: Mapped[list[Any]] = mapped_column(JSONB, server_default=text("'[]'"))
|
||||
|
||||
|
||||
class ChapterDigest(UuidPk, CreatedAt, Base):
|
||||
__tablename__ = "chapter_digests"
|
||||
project_id: Mapped[uuid.UUID] = mapped_column(
|
||||
ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
chapter_no: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
facts: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False)
|
||||
|
||||
|
||||
class Foreshadow(UuidPk, Base):
|
||||
__tablename__ = "foreshadow"
|
||||
__table_args__ = (UniqueConstraint("project_id", "code"),)
|
||||
project_id: Mapped[uuid.UUID] = mapped_column(
|
||||
ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
code: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
title: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
status: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("'OPEN'"))
|
||||
planted_at: Mapped[int | None] = mapped_column(Integer)
|
||||
content: Mapped[str | None] = mapped_column(Text)
|
||||
expected_close_from: Mapped[int | None] = mapped_column(Integer)
|
||||
expected_close_to: Mapped[int | None] = mapped_column(Integer)
|
||||
importance: Mapped[str | None] = mapped_column(Text)
|
||||
links: Mapped[list[Any]] = mapped_column(JSONB, server_default=text("'[]'"))
|
||||
progress: Mapped[list[Any]] = mapped_column(JSONB, server_default=text("'[]'"))
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
nullable=False, server_default=text("now()"), onupdate=text("now()")
|
||||
)
|
||||
|
||||
|
||||
class StyleFingerprint(UuidPk, CreatedAt, Base):
|
||||
__tablename__ = "style_fingerprint"
|
||||
project_id: Mapped[uuid.UUID] = mapped_column(
|
||||
ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
dimensions_json: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False)
|
||||
evidence_json: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False)
|
||||
version: Mapped[int] = mapped_column(Integer, nullable=False, server_default=text("1"))
|
||||
|
||||
|
||||
class Rule(UuidPk, CreatedAt, Base):
|
||||
__tablename__ = "rules"
|
||||
project_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||
ForeignKey("projects.id", ondelete="CASCADE")
|
||||
)
|
||||
level: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
content: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
|
||||
|
||||
class Chapter(UuidPk, CreatedAt, Base):
|
||||
__tablename__ = "chapters"
|
||||
__table_args__ = (UniqueConstraint("project_id", "chapter_no", "version"),)
|
||||
project_id: Mapped[uuid.UUID] = mapped_column(
|
||||
ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
volume: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
chapter_no: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
content: Mapped[str | None] = mapped_column(Text)
|
||||
status: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("'draft'"))
|
||||
version: Mapped[int] = mapped_column(Integer, nullable=False, server_default=text("1"))
|
||||
|
||||
|
||||
class ChapterReview(UuidPk, CreatedAt, Base):
|
||||
__tablename__ = "chapter_reviews"
|
||||
project_id: Mapped[uuid.UUID] = mapped_column(
|
||||
ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
chapter_no: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
chapter_version: Mapped[int | None] = mapped_column(Integer)
|
||||
conflicts: Mapped[list[Any]] = mapped_column(JSONB, server_default=text("'[]'"))
|
||||
foreshadow_sug: Mapped[list[Any]] = mapped_column(JSONB, server_default=text("'[]'"))
|
||||
style: Mapped[dict[str, Any] | None] = mapped_column(JSONB)
|
||||
pace: Mapped[dict[str, Any] | None] = mapped_column(JSONB)
|
||||
health_score: Mapped[int | None] = mapped_column(Integer)
|
||||
decisions: Mapped[dict[str, Any] | None] = mapped_column(JSONB)
|
||||
|
||||
|
||||
# ---- 运营/系统表(续)----
|
||||
|
||||
|
||||
class ProviderCredential(UuidPk, CreatedAt, Base):
|
||||
__tablename__ = "provider_credentials"
|
||||
__table_args__ = (UniqueConstraint("owner_id", "project_id", "provider"),)
|
||||
owner_id: Mapped[uuid.UUID] = mapped_column(
|
||||
ForeignKey("users.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
project_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||
ForeignKey("projects.id", ondelete="CASCADE")
|
||||
)
|
||||
provider: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
api_key_enc: Mapped[bytes] = mapped_column(LargeBinary, nullable=False)
|
||||
|
||||
|
||||
class TierRouting(UuidPk, Base):
|
||||
__tablename__ = "tier_routing"
|
||||
__table_args__ = (UniqueConstraint("project_id", "tier"),)
|
||||
project_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||
ForeignKey("projects.id", ondelete="CASCADE")
|
||||
)
|
||||
tier: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
provider: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
model: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
fallback: Mapped[list[Any]] = mapped_column(JSONB, server_default=text("'[]'"))
|
||||
|
||||
|
||||
class UsageLedger(UuidPk, CreatedAt, Base):
|
||||
__tablename__ = "usage_ledger"
|
||||
owner_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("users.id"), nullable=False)
|
||||
project_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("projects.id"))
|
||||
provider: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
model: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
input_tokens: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
output_tokens: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
cache_read: Mapped[int] = mapped_column(Integer, server_default=text("0"))
|
||||
cost_minor: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
||||
currency: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
|
||||
|
||||
class Skill(UuidPk, CreatedAt, Base):
|
||||
__tablename__ = "skills"
|
||||
scope: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
owner_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("users.id"))
|
||||
name: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
description: Mapped[str | None] = mapped_column(Text)
|
||||
tier: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
system_prompt: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
input_schema: Mapped[dict[str, Any] | None] = mapped_column(JSONB)
|
||||
output_schema: Mapped[dict[str, Any] | None] = mapped_column(JSONB)
|
||||
reads: Mapped[list[Any]] = mapped_column(JSONB, server_default=text("'[]'"))
|
||||
writes: Mapped[list[Any]] = mapped_column(JSONB, server_default=text("'[]'"))
|
||||
genre: Mapped[str | None] = mapped_column(Text)
|
||||
examples: Mapped[list[Any]] = mapped_column(JSONB, server_default=text("'[]'"))
|
||||
|
||||
|
||||
class Job(UuidPk, TimestampedMixin, Base):
|
||||
__tablename__ = "jobs"
|
||||
project_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||
ForeignKey("projects.id", ondelete="CASCADE")
|
||||
)
|
||||
kind: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
status: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("'queued'"))
|
||||
progress: Mapped[int] = mapped_column(Integer, server_default=text("0"))
|
||||
result: Mapped[dict[str, Any] | None] = mapped_column(JSONB)
|
||||
error: Mapped[str | None] = mapped_column(Text)
|
||||
Reference in New Issue
Block a user