Files
writer-work-flow/apps/api/ww_api/schemas/generation.py
Yaojia Wang 7babb4854b feat(web): 角色群像按定位配比生成(role_mix 缩减版)
- 契约: CharacterGenerateRequest 加 role_mix{定位:数量},model_validator 归一
  count=各值之和(每项≥1、总和≤12),缺省退回单一 role+count
- @llm: build_character_gen_context/run_character_gen 加 role_mix 参并注入配比块
  (确定性、无时间戳,守 assemble 纯函数);router 穿参
- character-gen.md 加"按配比严格分配 role"纪律 + regen prompt_hashes 金标准
- 前端: sanitizeRoleMix/roleMixTotal + buildCharacterGenerateRequest 第4参 +
  useCharacterGen 穿参 + RoleMixEditor + CharacterGenerator 单一/配比双模
2026-07-06 17:24:16 +02:00

195 lines
7.6 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.

"""生成/入库端点的请求/响应 schemaC3 扩 / 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, model_validator
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据需求生成角色卡预览群像防雷同
`role_mix`(⑦ 群像按定位配比)与 `count` 的关系:`role_mix` 在场时 `count` 取各值之和
(忽略客户端显式 count每项数量须 ≥ 1、总和须 ≤ `MAX_GENERATE_COUNT`;缺省则退回
单一 `role` + `count` 语义。
"""
brief: str = Field(min_length=1, description="角色生成需求")
count: int = Field(
default=1,
ge=1,
le=MAX_GENERATE_COUNT,
description="生成数量role_mix 在场时取各值之和)",
)
role: str | None = Field(default=None, description="角色定位(主角/CP/对手/导师/工具人 等)")
role_mix: dict[str, int] | None = Field(
default=None,
description="按定位配比生成 {定位: 数量};在场时 count=各值之和每项≥1、总和≤12",
)
@model_validator(mode="after")
def _reconcile_role_mix(self) -> CharacterGenerateRequest:
"""配比在场时校验并把 count 定为各值之和(构造期归一化,非外部对象变更)。"""
if self.role_mix is None:
return self
if not self.role_mix:
raise ValueError("role_mix 不能为空对象")
if any(n < 1 for n in self.role_mix.values()):
raise ValueError("role_mix 每个定位的数量须 ≥ 1")
total = sum(self.role_mix.values())
if total > MAX_GENERATE_COUNT:
raise ValueError(f"role_mix 数量总和须 ≤ {MAX_GENERATE_COUNT}")
self.count = total
return self
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":...}`→strtags/relations 直落 list。
"""
characters: list[CharacterCardView] = Field(default_factory=list)
class WorldEntityListResponse(BaseModel):
"""GET /projects/:id/world_entities已入库世界观实体全量列表设定库 Codex 真源)。
DB JSONB dict 列 `{"rules":[...]}`→裸 listworldbuilder 形变的逆向)。
"""
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)