Files
writer-work-flow/packages/db/ww_db/models.py
Yaojia Wang 821ace5989 feat(web): 立项新增 基调/结局/视角 三字段(书级常量入 stable_core 缓存前缀)
灵感计划 §3④ + §4 D 枚举决策:
- projects 表加 tone/ending_type/narrative_pov(Text nullable、无 CHECK,同 genre/structure;
  迁移 e5f6a7b8c9d0 nullable 免 backfill)。
- ProjectCreateRequest/ProjectResponse + domain ProjectCreate/ProjectView 全链路透传(snake_case)。
- 完整 stable_core 链路:ProjectSpecView + SqlProjectSpecRepo.spec + _build_spec_section 渲染进
  「作品蓝本」块——书级常量随书稳定,字节稳定守缓存前缀不变量 #9,绝不入 volatile。
- 前端 wizard.ts 三字段 + 预设数组 TONES/ENDING_TYPES/NARRATIVE_POVS + ProjectWizard 控件与确认页回显。
- 契约变更已 pnpm gen:api + memory/contracts.md 登记。
2026-07-06 15:59:49 +02:00

322 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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,
CheckConstraint,
DateTime,
ForeignKey,
Index,
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, index=True)
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)
# 立项书级常量(灵感④)——同 genre/structureText nullable、无 CHECK枚举约束留前端预设
tone: Mapped[str | None] = mapped_column(Text)
ending_type: Mapped[str | None] = mapped_column(Text)
narrative_pov: 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"
__table_args__ = (
Index(
"ix_chapter_digests_project_chapter",
"project_id",
text("chapter_no DESC"),
),
)
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"),
CheckConstraint(
"status IN ('OPEN', 'PARTIAL', 'CLOSED', 'OVERDUE')",
name="ck_foreshadow_status",
),
)
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(
DateTime(timezone=True),
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"
__table_args__ = (
Index(
"ix_rules_global",
"id",
postgresql_where=text("project_id IS NULL"),
),
)
project_id: Mapped[uuid.UUID | None] = mapped_column(
ForeignKey("projects.id", ondelete="CASCADE"), index=True
)
level: Mapped[str] = mapped_column(Text, nullable=False)
content: Mapped[str] = mapped_column(Text, nullable=False)
class Chapter(UuidPk, TimestampedMixin, Base):
__tablename__ = "chapters"
__table_args__ = (
UniqueConstraint("project_id", "chapter_no", "version"),
CheckConstraint(
"status IN ('draft', 'accepted')",
name="ck_chapters_status",
),
)
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 ChapterInjection(UuidPk, TimestampedMixin, Base):
"""本章注入覆盖B0 可控版):作者对确定性自动选择的微调,每章一行。
与 `outline.beats` 分表存放(复用 outline 行会污染 beats/prompt已否决
pinned/excluded 为 JSONB list[{kind,name}]recent_n 覆盖近况回看章数NULL=用默认)。
"""
__tablename__ = "chapter_injection"
__table_args__ = (UniqueConstraint("project_id", "chapter_no"),)
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)
pinned: Mapped[list[Any]] = mapped_column(JSONB, server_default=text("'[]'"))
excluded: Mapped[list[Any]] = mapped_column(JSONB, server_default=text("'[]'"))
recent_n: Mapped[int | None] = mapped_column(Integer)
class ChapterReview(UuidPk, CreatedAt, Base):
__tablename__ = "chapter_reviews"
__table_args__ = (
Index(
"ix_chapter_reviews_project_chapter",
"project_id",
"chapter_no",
text("created_at DESC"),
),
)
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)
auth_type: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("'api_key'"))
api_key_enc: Mapped[bytes | None] = mapped_column(LargeBinary, nullable=True)
oauth_enc: Mapped[bytes | None] = mapped_column(LargeBinary, nullable=True)
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, index=True)
project_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("projects.id"), index=True)
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 PromptTemplate(UuidPk, CreatedAt, Base):
"""作者保存/复用的提示词模板(单用户本地版;无分享/市场)。
owner_id 为单用户原型 stubtool_key 可选关联某生成器NULL=通用)。
"""
__tablename__ = "prompt_templates"
owner_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("users.id"), nullable=False, index=True)
title: Mapped[str] = mapped_column(Text, nullable=False)
body: Mapped[str] = mapped_column(Text, nullable=False)
category: Mapped[str | None] = mapped_column(Text)
tool_key: Mapped[str | None] = mapped_column(Text)
class Job(UuidPk, TimestampedMixin, Base):
__tablename__ = "jobs"
__table_args__ = (
Index(
"ix_jobs_status_running",
"status",
postgresql_where=text("status = 'running'"),
),
)
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)