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:
Yaojia Wang
2026-06-18 11:38:28 +02:00
commit d3dc620a71
74 changed files with 12960 additions and 0 deletions

236
packages/db/ww_db/models.py Normal file
View 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)