M4(文风): style-auditor 双轨(提取指纹/漂移第四审)+ jobs 长任务框架(zombie reaper) + 回炉 refine + GET /style read-back。 M5(生成+扩展): worldbuilder/character-gen(入库 continuity 409 gate + partition_writes 白名单 + schema→JSONB 形变); 网关多 provider 回退链/熔断/能力降级(Anthropic/Gemini 适配器);Skill registry + 表权限沙箱 + 规则; 前端 角色生成器/世界观/Codex/规则页/技能库/⌘K 命令面板。 K1(Kimi Code 订阅接入): OAuth device-flow(kimi-code)+ 静态 Console key(kimi-code-key)两路径; coding 端点 KimiCLI 伪造头(实测 UA allow-list 门禁,缺则 403)+ JSON 模式结构化(thinking ⊥ tool_choice)。 本地联调修复: CORS 中间件;assemble 注入 premise+「写第N章」指令(修空 prompt 400); GET /outline·/draft read-back + 大纲/工作台/审稿页重载;写页 client/server 常量边界 + notFound 健壮化; 字数 toLocaleString locale 水合;审稿页终稿从已存草稿 seed(修 accept 422)。 门禁: backend ruff/mypy(157)/alembic 无漂移/pytest 451 · frontend lint/tsc/vitest/build。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
239 lines
10 KiB
Python
239 lines
10 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 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)
|