打通 appearance/motive 从生成→schema→契约→落库→前端整条穿线 (DB 列早在初始迁移、此前一直 NULL,无新迁移),并原子重切人物 内驱口径。 - @llm: CharacterCard/character-gen.md 加 appearance/motive(给默认值守 解析韧性),重切——动机唯一落 motive、traits 只留核心/表层/阴影三层、 arc 引用 motive;regen prompt_hashes.json 金标准。 - @backend: CharacterCardView + CharacterWriteRepo/SqlCharacterWriteRepo (create + update backfill 分支)+ CharacterWriteFields + 路由双向形变 与 _existing_characters 带上两字段。 - @frontend: pnpm gen:api 重生成 TS 客户端;CharacterCardItem 展示/编辑 两字段。 - 测试: schema 默认值/解析、动机不双写重切回归、card_to_view 双向、 ingest 透传、list 暴露、SqlCharacterWriteRepo create/backfill 真 pg 集成。
166 lines
6.4 KiB
Python
166 lines
6.4 KiB
Python
"""生成/入库端点的请求/响应 schema(C3 扩 / ARCH §6.5 / §7.2)。
|
||
|
||
snake_case;前端经 OpenAPI 生成 TS 类型消费。改字段 → 前端必须 `pnpm gen:api`。
|
||
|
||
生成走**即时返回**(预览 → 作者确认 → 入库),非 jobs 长任务(M5-d):
|
||
- `POST /world/generate` / `POST /characters/generate`:返回**预览**(不持久化)。
|
||
- `POST /characters`(入库):作者确认的角色卡 → 过 continuity 预检 gate + 权限白名单 → 写库。
|
||
|
||
视图字段贴 `ww_agents.WorldEntityCard`/`CharacterCard`(保持生成产物形:traits/speech_tics
|
||
是 list、arc 是 str;入库时由写侧 repo 转 DB JSONB 形,见 character_repo)。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from pydantic import BaseModel, Field
|
||
|
||
from ww_api.schemas.rules import RuleView
|
||
|
||
# 批量生成数量上限(防一次性巨量调用;原型保守值)。
|
||
MAX_GENERATE_COUNT = 12
|
||
|
||
|
||
# ---- 世界观生成(预览)----
|
||
|
||
|
||
class WorldGenerateRequest(BaseModel):
|
||
"""POST /projects/:id/world/generate:据需求生成世界观实体(预览)。"""
|
||
|
||
brief: str = Field(min_length=1, description="世界观生成需求(题材/设定方向/约束)")
|
||
|
||
|
||
class WorldEntityCardView(BaseModel):
|
||
"""单个世界观实体卡(贴 ww_agents.WorldEntityCard;预览 + 入库共用形)。"""
|
||
|
||
type: str = Field(description="实体类型(势力 / 地理 / 力量体系 / 物品 / 概念 等)")
|
||
name: str = Field(description="实体名")
|
||
rules: list[str] = Field(default_factory=list, description="该实体的硬规则清单")
|
||
|
||
|
||
class WorldGenPreviewResponse(BaseModel):
|
||
"""世界观生成预览(不持久化;作者确认后另走入库,本期入库端点为角色)。"""
|
||
|
||
entities: list[WorldEntityCardView] = Field(default_factory=list)
|
||
|
||
|
||
# ---- 角色生成(预览)----
|
||
|
||
|
||
class CharacterGenerateRequest(BaseModel):
|
||
"""POST /projects/:id/characters/generate:据需求生成角色卡(预览,群像防雷同)。"""
|
||
|
||
brief: str = Field(min_length=1, description="角色生成需求")
|
||
count: int = Field(default=1, ge=1, le=MAX_GENERATE_COUNT, description="生成数量")
|
||
role: str | None = Field(default=None, description="角色定位(主角/CP/对手/导师/工具人 等)")
|
||
|
||
|
||
class CharacterRelationView(BaseModel):
|
||
"""单条人物关系(贴 ww_agents.CharacterRelation)。"""
|
||
|
||
name: str
|
||
kind: str
|
||
note: str | None = None
|
||
|
||
|
||
class CharacterCardView(BaseModel):
|
||
"""单张角色卡(贴 ww_agents.CharacterCard;预览 + 入库请求共用形)。"""
|
||
|
||
name: str = Field(description="角色名")
|
||
role: str = Field(description="角色定位")
|
||
traits: list[str] = Field(default_factory=list, description="性格特质清单(性格三层)")
|
||
appearance: str = Field(default="", description="外貌形象(缺则空串)")
|
||
motive: str = Field(default="", description="核心动机/欲望/恐惧(缺则空串)")
|
||
backstory: str = Field(description="背景故事")
|
||
arc: str = Field(description="人物弧光(一句话)")
|
||
speech_tics: list[str] = Field(default_factory=list, description="口癖/语言风格")
|
||
tags: list[str] = Field(default_factory=list, description="人设标签/萌点")
|
||
relations: list[CharacterRelationView] = Field(default_factory=list, description="关系网")
|
||
|
||
|
||
class CharacterGenPreviewResponse(BaseModel):
|
||
"""角色生成预览(不持久化;作者确认后经 POST /characters 入库)。"""
|
||
|
||
cards: list[CharacterCardView] = Field(default_factory=list)
|
||
|
||
|
||
# ---- 角色入库(作者确认后;过 continuity gate + 权限白名单)----
|
||
|
||
|
||
class CharacterIngestRequest(BaseModel):
|
||
"""POST /projects/:id/characters:把作者确认的角色卡入库。
|
||
|
||
`acknowledge_conflicts`:作者已查看并接受预检冲突时置 `true`,越过 continuity gate
|
||
(仿 accept 的冲突裁决:不静默入库,须作者确认;默认 false → 有冲突即 409,见端点)。
|
||
"""
|
||
|
||
cards: list[CharacterCardView] = Field(min_length=1, description="待入库的角色卡(至少 1 张)")
|
||
acknowledge_conflicts: bool = Field(
|
||
default=False, description="作者已知悉并接受 continuity 冲突 → 放行入库"
|
||
)
|
||
|
||
|
||
class IngestConflictView(BaseModel):
|
||
"""入库预检检出的单条 continuity 冲突(贴 ww_agents.Conflict 五类)。"""
|
||
|
||
type: str
|
||
where: str
|
||
refs: list[str] = Field(default_factory=list)
|
||
suggestion: str
|
||
|
||
|
||
class CharacterIngestResponse(BaseModel):
|
||
"""角色入库结果(201):写入的角色 + 被白名单丢弃的越权表(审计)。"""
|
||
|
||
created: list[str] = Field(default_factory=list, description="写入的角色名(按入参顺序)")
|
||
rejected_tables: list[str] = Field(
|
||
default_factory=list, description="被权限白名单丢弃的越权写表名(审计;正常为空)"
|
||
)
|
||
|
||
|
||
# ---- 读端点:设定库 Codex(角色 / 世界观全量列表)----
|
||
|
||
|
||
class CharacterListResponse(BaseModel):
|
||
"""GET /projects/:id/characters:已入库角色全量列表(设定库 Codex 真源)。
|
||
|
||
DB JSONB dict 列 → API list/str 反向解包(入库形变的逆向,见 character_repo):
|
||
`{"items":[...]}`→list、`{"text":...}`→str;tags/relations 直落 list。
|
||
"""
|
||
|
||
characters: list[CharacterCardView] = Field(default_factory=list)
|
||
|
||
|
||
class WorldEntityListResponse(BaseModel):
|
||
"""GET /projects/:id/world_entities:已入库世界观实体全量列表(设定库 Codex 真源)。
|
||
|
||
DB JSONB dict 列 `{"rules":[...]}`→裸 list(worldbuilder 形变的逆向)。
|
||
"""
|
||
|
||
world_entities: list[WorldEntityCardView] = Field(default_factory=list)
|
||
|
||
|
||
# ---- 读端点:规则列表 + 技能库(T5.6 前端)----
|
||
|
||
|
||
class RuleListResponse(BaseModel):
|
||
"""GET /projects/:id/rules:规则列表(规则页)。"""
|
||
|
||
rules: list[RuleView] = Field(default_factory=list)
|
||
|
||
|
||
class SkillView(BaseModel):
|
||
"""单个 skill 的声明式视图(技能库 UI)。"""
|
||
|
||
name: str
|
||
scope: str = Field(description="builtin / custom / community")
|
||
tier: str = Field(description="能力档位(writer / analyst / light)")
|
||
reads: list[str] = Field(default_factory=list)
|
||
writes: list[str] = Field(default_factory=list)
|
||
genre: str | None = None
|
||
|
||
|
||
class SkillListResponse(BaseModel):
|
||
"""GET /skills:技能库列表(按 name 升序)。"""
|
||
|
||
skills: list[SkillView] = Field(default_factory=list)
|