Files
writer-work-flow/packages/agents/ww_agents/schemas.py
Yaojia Wang bf39f50b2f fix(qa): 实施 4 组设计型 QA 项——规则删除/codex 角色/大纲卷过滤/文风回炉锚点
#5 规则 DELETE + id:暴露 RuleView.id(PK);新增 DELETE /projects/{id}/rules/{rule_id}
  (项目/规则不存在→404,成功→204,按 (id,project_id) 限定);rule_repo 加
  list_for_project/delete;RulesPage 每条加删除(乐观删+回滚+toast)。assemble 侧
  RuleView(缓存前缀)不动,列表另立 RuleListItemView。
#7 codex 角色 relations:写侧本已持久化、读端点 _existing_characters 硬编码 []。
  加 _relations_from_jsonb 解析 {name,kind,note},CodexPage 渲染关系 chip。
#8 角色入库幂等:SqlCharacterWriteRepo.create 改 (project_id,name) app 层 upsert——
  重复入库改更新而非插入;不加 UNIQUE/迁移(线上已有重复行会让约束迁移失败)。
#1 大纲卷过滤:GET /outline 支持可选 ?volume(无参=全部,向后兼容);OutlineEditor
  加「查看:全部/卷N」筛选,与生成目标卷解耦。
H3/#9 文风回炉锚点:StyleDriftSegment 加 text(逐字命中段),style.md 指示审稿输出;
  前端按内容锚点定位回炉目标(idx 仅排序),命中失败 → 提示「无法定位该段」而非
  静默 no-op。style golden fixture 已重生成。

契约变更已 pnpm gen:api(RuleView.id / DELETE rules / outline ?volume)。无迁移
(alembic 无漂移)。门禁绿:ruff/mypy(210)/alembic/pytest 760 · 前端 tsc/lint/vitest 329。
2026-06-25 12:53:03 +02:00

597 lines
24 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.

"""续审 Agent 的结构化输出 schemaARCH §5.4 continuity 行 / §6.1 冲突分类)。
snake_casePydantic v2。网关经 instructor 保证产出符合这些 schemaC1/§4.4)。
不变量 #3四审只读——审稿期只产**冲突清单**,不产 digestdigest 在验收时
从终稿另提,见不变量 #4 / ARCH §5.5)。
"""
from __future__ import annotations
from typing import Literal
from pydantic import BaseModel, Field
# ARCH §6.1 五类冲突;受限枚举,便于前端就地裁决分类与统计。
ConflictType = Literal[
"性格漂移",
"能力不符",
"设定违例",
"地理矛盾",
"时间线倒错",
]
class Conflict(BaseModel):
"""单条一致性冲突ARCH §6.1)。"""
type: ConflictType = Field(description="冲突分类(五类之一)")
where: str = Field(description="本章定位:哪一段/哪句触发冲突")
refs: list[str] = Field(
default_factory=list,
description="冲突来源引用:相关章节号、设定项或人物卡条目",
)
suggestion: str = Field(description="改法建议")
# 一键采纳补丁对(可缺):可由局部改写修复时给出,供前端把 original→replacement
# find-replace 进终稿(作者点「采纳改法」触发,仍只读——审稿只提议、不改稿)。
# 不可定位到单一连续片段(跨段/缺设定等)则两者留空,回退到作者手改。
original: str | None = Field(
default=None,
description="可一键采纳时:从本章草稿**逐字摘录**的精确原文最小句段(含标点,不得改写);不可定位则留空",
)
replacement: str | None = Field(
default=None,
description="可一键采纳时:对应 original 的改后文本(同粒度最小句段);不可定位则留空",
)
class ContinuityReview(BaseModel):
"""续审结构化产出:仅冲突清单(无 digest
digest 不在审稿期产——那是验收时从**终稿**另提(不变量 #4
"""
conflicts: list[Conflict] = Field(
default_factory=list,
description="检出的一致性冲突;无冲突时为空列表",
)
# ---- 大纲 Agent 结构化输出ARCH §5.4 outliner 行 / §6.2 伏笔窗口)----
class ForeshadowWindow(BaseModel):
"""伏笔回收窗口关联本章与某条伏笔ARCH §6.2)。
排大纲时把伏笔的「埋设章 → 期望回收区间」绑到具体章节;接近回收窗口的章节
提示作者推进/收束。下界/上界可缺省(仅埋设、尚未排定回收时间)。
"""
code: str = Field(description="伏笔编码(关联 foreshadow.code")
plant_chapter: int | None = Field(
default=None, description="埋设章号(伏笔在本章/此章首次出现)"
)
expected_close_from: int | None = Field(default=None, description="期望回收区间下界(章号)")
expected_close_to: int | None = Field(default=None, description="期望回收区间上界(章号)")
class OutlineChapter(BaseModel):
"""单章大纲:章号 + 节拍清单 + 关联伏笔窗口。"""
no: int = Field(description="章号")
beats: list[str] = Field(default_factory=list, description="本章节拍/情节要点(有序)")
foreshadow_windows: list[ForeshadowWindow] = Field(
default_factory=list,
description="本章关联的伏笔回收窗口;无则空列表",
)
class OutlineResult(BaseModel):
"""大纲结构化产出分卷分章的章节大纲清单ARCH §5.4 writes=outline
声明式 writes不变量 #3本结构是排大纲的纯产物落 `outline` 表经验收/
端点T3.5),不在 agent 节点直接写库。
"""
chapters: list[OutlineChapter] = Field(
default_factory=list,
description="分章大纲;无则空列表",
)
# ---- 伏笔续审 Agent 结构化输出ARCH §5.4 foreshadow-analyst 行 / §6.2----
class ForeshadowSuggestion(BaseModel):
"""单条伏笔建议:本章新埋线 或 疑似回收ARCH §6.2)。
只读建议——作者在验收裁决里确认后才登记/改状态(不变量 #3本审不写库。
`code` 可缺(新埋线尚无编码时由作者命名);`where` 给本章定位。
"""
code: str | None = Field(
default=None, description="伏笔编码(关联已登记 foreshadow.code新埋可缺"
)
title: str = Field(description="伏笔标题/一句话描述")
where: str | None = Field(default=None, description="本章定位:哪一段埋下/疑似回收")
note: str | None = Field(default=None, description="建议说明(埋设要点 / 回收理由)")
class ForeshadowReview(BaseModel):
"""伏笔续审结构化产出:本章新埋 + 疑似回收两组建议ARCH §5.4 / §6.2)。
只读建议清单(不变量 #3登记/状态变更经验收裁决(作者确认)。
"""
planted: list[ForeshadowSuggestion] = Field(
default_factory=list,
description="本章疑似新埋的伏笔;无则空列表",
)
resolved: list[ForeshadowSuggestion] = Field(
default_factory=list,
description="本章疑似回收/收束的伏笔;无则空列表",
)
# ---- 节奏续审 Agent 结构化输出ARCH §5.4 pace-checker 行 / §6.4----
class PaceIssue(BaseModel):
"""单条节奏问题:注水段定位 + 原因ARCH §6.4)。"""
where: str = Field(description="本章定位:哪一段疑似注水/拖沓")
reason: str = Field(description="判定理由(信息密度低 / 重复 / 偏题等)")
class PaceReview(BaseModel):
"""节奏续审结构化产出:注水段 + 章末钩子 + 爽点节拍图ARCH §5.4 / §6.4)。
只读(不变量 #3。`beat_map` 是逐段节拍强度序列(整数),供前端 ▁▃▅ 可视化。
"""
water: list[PaceIssue] = Field(
default_factory=list,
description="疑似注水/拖沓段;无则空列表",
)
hook: bool = Field(default=False, description="章末是否有有效钩子(悬念/反转/期待)")
beat_map: list[int] = Field(
default_factory=list,
description="逐段爽点节拍强度序列(整数,供前端节拍图可视化)",
)
# ---- 文风提取轨结构化输出ARCH §5.4 style-auditor「提取」轨 / §6.9----
class StyleDimension(BaseModel):
"""单维文风指纹:维度名 + 取值 + 原文证据引用ARCH §6.9)。
每维必须带原文证据quotes便于作者核验「这条文风结论来自哪句」。
"""
name: str = Field(description="文风维度名(如句长节奏 / 叙事人称 / 用词风格)")
value: str = Field(description="该维度在样本中的取值/概括")
evidence: list[str] = Field(
default_factory=list,
description="原文证据:支撑该维度判定的样本原句摘录",
)
class StyleFingerprintResult(BaseModel):
"""文风指纹提取产出16 维文风指纹清单ARCH §5.4 提取轨)。
声明式 writes不变量 #3本结构是从样本提取的纯产物落 `style_fingerprint` 表
经端点T4.3),不在 agent 节点直接写库。16 维 = 9 通用 + 7 中文网文专有。
"""
dimensions: list[StyleDimension] = Field(
default_factory=list,
description="文风维度清单(每维带原文证据);无则空列表",
)
# ---- 文风漂移轨结构化输出ARCH §5.4 style-auditor「打分」轨 = 第四审)----
class StyleDriftSegment(BaseModel):
"""单个漂移段:原文锚 + 段索引 + 相似度分 + 可选标签ARCH §5.4 打分轨)。
`text` 是从本章草稿**逐字摘录**的该漂移段原文(含标点,不得改写)——前端据此用
内容匹配在终稿里定位回炉目标(**内容锚**,不靠位置 idx避免分段方式不一致导致定位
到错段 / 越界静默失败)。`idx` 仅供前端展示排序0 起);`score` 是该段相对文风指纹
的相似度0100越低越偏`label` 可缺(如「机翻腔」「叙述拖沓」等漂移类型说明)。
"""
idx: int = Field(description="本章段索引0 起,仅供展示排序)")
text: str = Field(
default="",
description="该漂移段从本章草稿**逐字摘录**的原文(含标点,不得改写);供前端内容锚定位回炉目标",
)
score: int = Field(description="该段相对文风指纹的相似度0100越低越偏离")
label: str | None = Field(default=None, description="漂移类型标签(如「机翻腔」);可缺")
class StyleDriftReview(BaseModel):
"""文风漂移续审结构化产出:整体相似度 + 低相似段ARCH §5.4 / 第四审)。
只读(不变量 #3。`score` 是整章相对文风指纹的整体相似度0100
**无指纹优雅降级**:材料中无文风指纹时返回 `score=100, segments=[]`(默认值),
不报错、不阻塞其余审§5.2 失败隔离之外的内容性兜底)。
"""
score: int = Field(default=100, description="整章相对文风指纹的整体相似度0100")
segments: list[StyleDriftSegment] = Field(
default_factory=list,
description="低相似度(疑似漂移)段清单;无则空列表(含无指纹降级)",
)
# ---- worldbuilder 结构化输出ARCH §5.4 worldbuilder 行 / §6.5 / §4.5----
class WorldEntityCard(BaseModel):
"""单个世界观实体ARCH §5.4`{type,name,rules}` 硬规则显式)。
`type`(势力/地理/力量体系/物品…)+ `name` + `rules`**显式硬规则**清单:
不可违背的设定,供后续 continuity 续审引用比对)。映射 `world_entities` 表
type/name 列 + rules 入 JSONB落库经入库端点T5.2,不变量 #3
"""
type: str = Field(description="实体类型(势力 / 地理 / 力量体系 / 物品 / 概念 等)")
name: str = Field(description="实体名")
rules: list[str] = Field(
default_factory=list,
description="该实体的硬规则清单(显式不可违背设定,供 continuity 校验引用)",
)
class WorldGenResult(BaseModel):
"""worldbuilder 结构化产出世界观实体清单ARCH §5.4 writes=world_entities
声明式 writes不变量 #3本结构是 worldbuilder 的纯产物,落 `world_entities` 表
经入库端点T5.2),不在 agent 节点直接写库。
"""
entities: list[WorldEntityCard] = Field(
default_factory=list,
description="生成的世界观实体清单;无则空列表",
)
# ---- character-gen 结构化输出ARCH §5.4 character-gen 行 / §6.5 / §4.5----
class CharacterRelation(BaseModel):
"""单条人物关系(写入 `characters.relations` JSONB 冗余)。
`name`(关系对象角色名)+ `kind`(宿敌 / 师徒 / CP / 同盟 …)+ 可选 `note`。
双向边由编排器在入库时统一维护§4.5 三关键能力之三),本 schema 仅声明本侧。
"""
name: str = Field(description="关系对象的角色名")
kind: str = Field(description="关系类型(宿敌 / 师徒 / CP / 同盟 / 工具人 等)")
note: str | None = Field(default=None, description="关系说明(可缺)")
class CharacterCard(BaseModel):
"""单张结构化角色卡ARCH §5.4 character-gen 行name/role/traits/backstory/arc/...)。
映射 `characters` 表列name/role/backstory + traits/arc/speech_tics/tags/relations
入 JSONB落库经入库端点T5.2,不变量 #3。`role` = 角色定位(主角/CP/对手/导师/
工具人§4.5 网文专属)。
"""
name: str = Field(description="角色名(取名风格契合世界观)")
role: str = Field(description="角色定位(主角 / CP / 对手 / 导师 / 工具人 等)")
traits: list[str] = Field(
default_factory=list,
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[CharacterRelation] = Field(
default_factory=list,
description="关系网:与已有/同批角色建边;无则空列表",
)
class CharacterGenResult(BaseModel):
"""character-gen 结构化产出一组角色卡ARCH §5.4 writes=characters / §4.5 群像)。
声明式 writes不变量 #3本结构是 character-gen 的纯产物,落 `characters` 表
经入库端点T5.2),且**入库前过编排器 continuity 校验**§6.5)。单生成 → 1 张卡;
批量群像 → 多张(防雷同:生成时注入「已生成卡 + 已有角色」要求差异化)。
"""
cards: list[CharacterCard] = Field(
default_factory=list,
description="生成的角色卡清单;无则空列表",
)
# ---- 创作工具箱 · 脑洞生成器结构化输出T6 通用生成器框架 · brainstorm----
class Idea(BaseModel):
"""单条脑洞:前提 + 钩子 + 适配题材(创作工具箱最简生成器产物)。
纯预览产物——不映射任何业务表、不入库(`brainstorm_spec.writes=[]`,不变量 #3
`genre_fit` 可缺(作者未限定赛道时由模型留空)。
"""
premise: str = Field(description="一句话脑洞/设定前提")
hook: str = Field(description="抓人钩子:为何让读者想追读")
genre_fit: str | None = Field(default=None, description="适配题材/赛道(可缺)")
class IdeaListResult(BaseModel):
"""脑洞生成器结构化产出一组脑洞T6 · 通用生成器框架首个最简生成器)。
声明式只读(不变量 #3纯预览端点同步返回不写任何业务表仅记 ledger
"""
ideas: list[Idea] = Field(
default_factory=list,
description="生成的脑洞清单;无则空列表",
)
# ---- 创作工具箱 · 书名生成器book-titlelight纯预览 writes=[]----
class Title(BaseModel):
"""单个书名候选:书名 + 取名理由(创作工具箱 book-title 产物)。
纯预览产物——不映射任何业务表、不入库(`book_title_spec.writes=[]`,不变量 #3
"""
title: str = Field(description="书名候选")
rationale: str = Field(description="取名理由:为何抓人/契合题材")
class TitleListResult(BaseModel):
"""书名生成器结构化产出一组书名候选T6 创作工具箱)。
声明式只读(不变量 #3纯预览端点同步返回不写任何业务表。
"""
titles: list[Title] = Field(
default_factory=list,
description="生成的书名候选清单;无则空列表",
)
# ---- 创作工具箱 · 简介生成器blurbanalyst纯预览 writes=[]----
class Blurb(BaseModel):
"""单条作品简介:正文 + 切入角度(创作工具箱 blurb 产物)。
纯预览产物——不映射任何业务表、不入库(`blurb_spec.writes=[]`,不变量 #3
"""
text: str = Field(description="简介正文(一段抓人文案)")
angle: str = Field(description="切入角度:从哪个卖点/钩子切入")
class BlurbResult(BaseModel):
"""简介生成器结构化产出多版差异化简介T6 创作工具箱)。
声明式只读(不变量 #3纯预览端点同步返回不写任何业务表。
"""
variants: list[Blurb] = Field(
default_factory=list,
description="生成的简介变体清单;无则空列表",
)
# ---- 创作工具箱 · 取名生成器namelight纯预览 writes=[]----
class NameSuggestion(BaseModel):
"""单条命名建议:名字 + 类别 + 可选说明(创作工具箱 name 产物)。
纯预览产物——不映射任何业务表、不入库(`name_spec.writes=[]`,不变量 #3
`kind` 标注命名对象类别(人物/势力/地点/功法/物品…)。
"""
name: str = Field(description="建议的名字")
kind: str = Field(description="命名对象类别(人物 / 势力 / 地点 / 功法 / 物品 等)")
note: str | None = Field(default=None, description="命名说明(取意/出处,可缺)")
class NameListResult(BaseModel):
"""取名生成器结构化产出一组命名建议T6 创作工具箱)。
声明式只读(不变量 #3纯预览端点同步返回不写任何业务表。
"""
names: list[NameSuggestion] = Field(
default_factory=list,
description="生成的命名建议清单;无则空列表",
)
# ---- 创作工具箱 · 金手指生成器golden-fingerwriter声明式 writes=world_entities----
class GoldenFinger(BaseModel):
"""单个金手指系统:名称 + 机制 + 成长 + 限制(创作工具箱 golden-finger 产物)。
映射 `world_entities`(力量体系类);落库经入库端点(不变量 #3。`limits` = 不可
违背的能力边界/代价,供 continuity 续审引用比对(防能力不符)。
"""
name: str = Field(description="金手指名称")
mechanism: str = Field(description="核心机制:如何运作、触发条件")
growth: str = Field(description="成长路径:随剧情如何升级/进阶")
limits: str = Field(description="限制与代价:能力边界、副作用(防越界,供一致性校验)")
class GoldenFingerResult(BaseModel):
"""金手指生成器结构化产出一组金手指系统T6 创作工具箱)。
声明式 writes不变量 #3本结构是纯产物落 `world_entities` 表经入库端点,
不在 agent 节点直接写库。
"""
systems: list[GoldenFinger] = Field(
default_factory=list,
description="生成的金手指系统清单;无则空列表",
)
# ---- 创作工具箱 · 术语表生成器glossaryanalyst声明式 writes=world_entities----
class GlossaryTerm(BaseModel):
"""单条术语:名称 + 类型 + 定义 + 硬规则(创作工具箱 glossary 产物)。
映射 `world_entities`concept/术语类);`rules` = 该术语的硬规则清单,
对齐 `world_entities.rules`,供 continuity 续审引用比对(不变量 #3
"""
name: str = Field(description="术语名")
type: str = Field(description="术语类型(境界 / 功法 / 货币 / 度量 / 称谓 等)")
definition: str = Field(description="术语定义(一句话释义)")
rules: list[str] = Field(
default_factory=list,
description="该术语的硬规则清单(不可违背设定,供 continuity 校验引用)",
)
class GlossaryResult(BaseModel):
"""术语表生成器结构化产出一组术语T6 创作工具箱)。
声明式 writes不变量 #3本结构是纯产物落 `world_entities` 表经入库端点,
不在 agent 节点直接写库。
"""
terms: list[GlossaryTerm] = Field(
default_factory=list,
description="生成的术语清单;无则空列表",
)
# ---- 创作工具箱 · 开篇生成器openingwriter纯预览 writes=[]----
class Opening(BaseModel):
"""单版开篇正文(创作工具箱 opening 产物)。
纯预览产物——不映射任何业务表、不入库(`opening_spec.writes=[]`,不变量 #3
"""
text: str = Field(description="开篇正文(一段成稿文本)")
class OpeningResult(BaseModel):
"""开篇生成器结构化产出多版开篇T6 创作工具箱)。
声明式只读(不变量 #3纯预览端点同步返回不写任何业务表。
"""
variants: list[Opening] = Field(
default_factory=list,
description="生成的开篇变体清单;无则空列表",
)
# ---- 创作工具箱 · 细纲生成器fine-outlineanalyst声明式 writes=outline----
class Scene(BaseModel):
"""单个场景节拍:序号 + 节拍 + 目的 + 冲突 + 钩子(创作工具箱 fine-outline 产物)。
把某章 outline 的粗节拍展开为细粒度场景;落库经入库端点(不变量 #3
"""
idx: int = Field(description="场景序号章内有序0/1 起均可)")
beat: str = Field(description="场景节拍:本场发生什么")
purpose: str = Field(description="叙事目的:推动主线/塑造人物/铺垫伏笔")
conflict: str = Field(description="本场冲突/张力来源")
hook: str = Field(description="场景钩子:驱动读者读下一场的悬念")
class DetailedOutlineResult(BaseModel):
"""细纲生成器结构化产出某章展开的场景清单T6 创作工具箱)。
声明式 writes不变量 #3本结构是纯产物落 `outline` 表经入库端点,
不在 agent 节点直接写库。
"""
scenes: list[Scene] = Field(
default_factory=list,
description="展开的场景清单;无则空列表",
)
# ---- 竞品快赢 · 续写生成器continuewriter纯预览 writes=[]----
class ContinuationResult(BaseModel):
"""续写生成器结构化产出承接前文的续写正文Scope B 竞品快赢)。
纯预览产物——不映射任何业务表、不入库(`continue_spec.writes=[]`,不变量 #3
"""
text: str = Field(description="续写正文(承接前文,可直接作为下文草稿)")
# ---- 竞品快赢 · 扩写生成器expandwriter纯预览 writes=[]----
class PolishResult(BaseModel):
"""扩写生成器结构化产出:在原文基础上扩写/丰富后的正文Scope B 竞品快赢)。
纯预览产物——不映射任何业务表、不入库(`expand_spec.writes=[]`,不变量 #3
"""
text: str = Field(description="扩写后的正文(在原文基础上丰富细节/铺陈)")
# ---- 竞品快赢 · 降 AI 率生成器de-aianalyst纯预览 writes=[]----
class DeAiResult(BaseModel):
"""降 AI 率生成器结构化产出:去 AI 腔/口语化改写后的正文Scope B 竞品快赢)。
纯预览产物——不映射任何业务表、不入库(`de_ai_spec.writes=[]`,不变量 #3
"""
text: str = Field(description="降 AI 率改写后的正文(去机翻腔、更自然的人写质感)")
# ---- 竞品快赢 · 拆书生成器teardownanalyst纯预览 writes=[]----
class BookTeardownResult(BaseModel):
"""拆书生成器结构化产出样本作品的结构化拆解Scope B 竞品快赢)。
F1可经 ingest 端点把结论拍平为项目 `rules` 条目落库(`teardown_spec.writes=["rules"]`
入库仍经白名单 gate不变量 #3预览generate不写库。
"""
themes: list[str] = Field(
default_factory=list,
description="核心主题/立意清单;无则空列表",
)
archetypes: list[str] = Field(
default_factory=list,
description="人物原型/角色模板清单;无则空列表",
)
structure: str = Field(description="叙事结构概述(开篇/铺垫/高潮/收束的脉络)")
hooks: list[str] = Field(
default_factory=list,
description="抓人钩子/爽点套路清单;无则空列表",
)