- selection.select_relevant_entities 加 pinned/excluded frozenset 入参;新理由 author_pin(末位) - assemble 加 override 关键字参(recent_n 覆盖回看章数 + pin/排除);GET/PUT/draft 同读同一覆盖(不变量 #6 作者兜底) - 新 domain/injection_repo.py(InjectionOverride/EntityRef/SqlInjectionOverrideRepo,upsert 只 flush) - 新表 chapter_injection(迁移 ad2c4c663daf,唯一 project_id+chapter_no;复用 outline 行已否决) - PUT/GET injection 端点 + InjectionOverrideRequest/回显 pinned/excluded;recent_n 1..20 校验 422 - 单测:selection pin/排除/exclude>pin + assemble override + 端点 PUT/404/422;regen TS 客户端 - 门禁绿:ruff/format · mypy 163 · alembic 无漂移 · pytest 476 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
257 lines
11 KiB
Python
257 lines
11 KiB
Python
"""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 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"
|
||
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)
|
||
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)
|