写→审链新增第五审「人物塑造」(灵感③/D2):advisory 单维——仅建议、不阻断
验收(不进 conflicts、不碰 assert_conflicts_resolved、不入 REVIEW_RESERVED_NAMES);
只做人物塑造、不做氛围。
- SPEC characterization_spec(analyst,reads=("characters",),writes=())+
prompts/characterization.md(只读、显式忽略文本长度与辞藻华丽度、引用逐字片段+置信度)
- schema CharacterizationReview{issues[...]}(全字段默认值守解析韧性)+ 注册 catalog
- 计数三处 22→23 + regen prompt_hashes.json(仅加一条)
- DB chapter_reviews 加 characterization JSONB nullable 列(迁移 f6a7b8c9d0e1)
- 接线:graph REVIEW_SPECS 追加 · chain ReviewRecordRepo · collect(extract/record) ·
review_repo(Protocol/Sql/_to_view/ReviewView) · sse(event/factory/section) · normalize 自动纳入
- 契约 ReviewHistoryItem 加 characterization(gen:api 已重生成)
- 前端 sse/history/useReviewStream + CharacterizationPanel(只展示无裁决 UI、置信度降序+低置信折叠)+ ReviewReport 挂面板
- 测试 test_characterization_does_not_block_accept + 计数/golden + collect/normalize + 前端 reducer/normalize/parse
守不变量 #2/#3/#9;依赖 ⑧(motive/appearance 作客观锚点)。
238 lines
9.0 KiB
Python
238 lines
9.0 KiB
Python
"""项目(立项)与章节草稿的请求/响应 schema(C3 / ARCH §7.2)。
|
||
|
||
snake_case;前端经 OpenAPI 生成 TS 类型消费。改字段 → 前端必须 `pnpm gen:api`。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import uuid
|
||
from datetime import datetime
|
||
from typing import Any, Literal
|
||
|
||
from pydantic import BaseModel, Field
|
||
|
||
|
||
class ProjectCreateRequest(BaseModel):
|
||
"""POST /projects:立项向导字段(owner_id 由后端补 stub,不入参)。"""
|
||
|
||
title: str = Field(min_length=1)
|
||
genre: str | None = None
|
||
logline: str | None = None
|
||
premise: str | None = None
|
||
theme: str | None = None
|
||
selling_points: list[str] = Field(default_factory=list)
|
||
structure: str | None = None
|
||
# 立项书级常量(灵感④)——free text + 前端预设,无 Literal(Literal 只留 Verdict)。
|
||
tone: str | None = None
|
||
ending_type: str | None = None
|
||
narrative_pov: str | None = None
|
||
|
||
|
||
class ProjectResponse(BaseModel):
|
||
"""项目视图(创建/列表/详情共用)。"""
|
||
|
||
id: uuid.UUID
|
||
title: str
|
||
genre: str | None = None
|
||
logline: str | None = None
|
||
premise: str | None = None
|
||
theme: str | None = None
|
||
selling_points: list[str] = Field(default_factory=list)
|
||
structure: str | None = None
|
||
tone: str | None = None
|
||
ending_type: str | None = None
|
||
narrative_pov: str | None = None
|
||
updated_at: datetime | None = Field(default=None, description="项目最近更新时间")
|
||
pending_review_count: int = Field(default=0, ge=0, description="待审稿章节数")
|
||
|
||
|
||
class ProjectListResponse(BaseModel):
|
||
"""GET /projects:项目列表。"""
|
||
|
||
projects: list[ProjectResponse] = Field(default_factory=list)
|
||
|
||
|
||
# ---- AI 立项方案生成(灵感⑤;向导阶段 project 未建,纯预览不写库)----
|
||
|
||
# 请求字段长度上界(CR-H9:防超长向导草稿撑爆上下文/成本)。短字段=标签级,长字段=段落级。
|
||
_PLAN_SHORT_MAX = 200
|
||
_PLAN_LONG_MAX = 4000
|
||
_PLAN_LIST_MAX = 20
|
||
|
||
|
||
class ProjectPlanGenerateRequest(BaseModel):
|
||
"""POST /skills/project-plan/generate:向导草稿种子(不带 project 前缀,立项前 project 未建)。
|
||
|
||
**种子门控**:`genre` + `logline` 为必给种子(端点校验非空,否则 422)——空向导不出方案,
|
||
防 slop。其余字段有则贴合、无则不臆造(序列化进 project_context,仿 `_project_context`)。
|
||
全字段带 `max_length` 上界(CR-H9),`brief` = 作者一句话方向(可空)。
|
||
"""
|
||
|
||
genre: str | None = Field(default=None, max_length=_PLAN_SHORT_MAX, description="题材(种子)")
|
||
logline: str | None = Field(
|
||
default=None, max_length=_PLAN_LONG_MAX, description="一句话故事(种子)"
|
||
)
|
||
title: str | None = Field(
|
||
default=None, max_length=_PLAN_SHORT_MAX, description="暂定书名(可空)"
|
||
)
|
||
premise: str | None = Field(default=None, max_length=_PLAN_LONG_MAX, description="立意(可空)")
|
||
theme: str | None = Field(default=None, max_length=_PLAN_SHORT_MAX, description="主题(可空)")
|
||
structure: str | None = Field(
|
||
default=None, max_length=_PLAN_SHORT_MAX, description="故事结构(可空)"
|
||
)
|
||
tone: str | None = Field(default=None, max_length=_PLAN_SHORT_MAX, description="基调(可空)")
|
||
ending_type: str | None = Field(
|
||
default=None, max_length=_PLAN_SHORT_MAX, description="结局取向(可空)"
|
||
)
|
||
narrative_pov: str | None = Field(
|
||
default=None, max_length=_PLAN_SHORT_MAX, description="叙事视角(可空)"
|
||
)
|
||
selling_points: list[str] = Field(
|
||
default_factory=list, max_length=_PLAN_LIST_MAX, description="核心卖点(可空)"
|
||
)
|
||
# `str | None`(非 `str=""`):codegen 渲染为可选字段,前端种子请求可省略(brief 本就可空)。
|
||
brief: str | None = Field(
|
||
default=None, max_length=_PLAN_LONG_MAX, description="作者一句话方向/需求(可空)"
|
||
)
|
||
|
||
|
||
class ProjectPlanView(BaseModel):
|
||
"""立项方案生成结果(贴 ww_agents.ProjectPlanResult;结构化预览,不入库)。
|
||
|
||
全字段有默认值(守解析韧性)——前端按字段回填立项向导,只填非空交集、不覆盖作者已编辑值。
|
||
"""
|
||
|
||
title_candidates: list[str] = Field(default_factory=list, description="书名候选清单")
|
||
setting: str = Field(default="", description="时空背景")
|
||
narrative_structure: str = Field(default="", description="叙事结构")
|
||
story_core: str = Field(default="", description="故事核心")
|
||
ending_design: str = Field(default="", description="结局设计")
|
||
tone: str = Field(default="", description="基调")
|
||
|
||
|
||
class DraftStreamRequest(BaseModel):
|
||
"""POST /projects/:id/chapters/:no/draft:本章生成的可选输入(T4-b)。
|
||
|
||
`directive` = 作者本章指令(覆盖/补充大纲节拍)。临时输入,不持久化、无迁移;
|
||
后端只入 volatile(守缓存前缀不变量 #9)。无 body 时退化为纯按大纲生成(向后兼容)。
|
||
"""
|
||
|
||
directive: str | None = None
|
||
|
||
|
||
class DraftSaveRequest(BaseModel):
|
||
"""PUT /projects/:id/chapters/:no/draft:自动保存草稿正文。"""
|
||
|
||
text: str
|
||
|
||
|
||
class DraftResponse(BaseModel):
|
||
"""草稿保存结果(脱敏:只回元信息 + 长度,不回灌正文以外的衍生)。"""
|
||
|
||
project_id: uuid.UUID
|
||
chapter_no: int
|
||
volume: int
|
||
status: str
|
||
version: int
|
||
length: int
|
||
|
||
|
||
class DraftView(BaseModel):
|
||
"""GET .../draft:回灌已保存草稿(含正文,供工作台重载编辑器)。
|
||
|
||
与 PUT 的 `DraftResponse` 同元信息,**额外带 `content`**——读侧需要正文以重建编辑器,
|
||
保存侧不回灌正文故不带。`length` 仍按 content 字符数派生。
|
||
"""
|
||
|
||
project_id: uuid.UUID
|
||
chapter_no: int
|
||
volume: int
|
||
status: str
|
||
version: int
|
||
content: str
|
||
length: int
|
||
|
||
|
||
# ---- 审稿(T2.5)----
|
||
|
||
|
||
class ReviewRequest(BaseModel):
|
||
"""POST /projects/:id/chapters/:no/review:可选携带待审草稿正文。
|
||
|
||
不传 `draft` 时端点回退到已保存的草稿(chapter_repo)。
|
||
"""
|
||
|
||
draft: str | None = None
|
||
|
||
|
||
class ReviewConflictView(BaseModel):
|
||
"""单条一致性冲突(chapter_reviews.conflicts JSONB 子项;形对齐 SSE `conflict` 事件)。
|
||
|
||
强类型化此前的裸 `dict[str, Any]`,给 TS 客户端真实字段类型。容忍历史/缺省(默认值)。
|
||
"""
|
||
|
||
type: str = ""
|
||
where: str = ""
|
||
refs: list[str] = Field(default_factory=list)
|
||
suggestion: str = ""
|
||
# 一键采纳补丁对(可缺):留痕里带则前端「采纳改法」可把 original→replacement 改入终稿。
|
||
original: str | None = None
|
||
replacement: str | None = None
|
||
|
||
|
||
class ReviewHistoryItem(BaseModel):
|
||
"""单条审稿留痕(GET .../reviews 历史项;snake_case)。"""
|
||
|
||
id: uuid.UUID
|
||
project_id: uuid.UUID
|
||
chapter_no: int
|
||
chapter_version: int | None = None
|
||
conflicts: list[ReviewConflictView] = Field(default_factory=list)
|
||
foreshadow_sug: list[dict[str, Any]] = Field(default_factory=list)
|
||
style: dict[str, Any] | None = None
|
||
pace: dict[str, Any] | None = None
|
||
# 灵感③ advisory 人物塑造审留痕(issues 列表 dict);仅建议、不阻断验收。
|
||
characterization: dict[str, Any] | None = None
|
||
health_score: int | None = None
|
||
decisions: dict[str, Any] | None = None
|
||
|
||
|
||
class ReviewHistoryResponse(BaseModel):
|
||
"""GET /projects/:id/chapters/:no/reviews:审稿历史(新→旧)。"""
|
||
|
||
reviews: list[ReviewHistoryItem] = Field(default_factory=list)
|
||
|
||
|
||
# ---- 验收(T2.4)----
|
||
|
||
# 每个冲突的裁决:采纳改法 / 忽略 / 手改(R5)。
|
||
Verdict = Literal["accept", "ignore", "manual"]
|
||
|
||
|
||
class ConflictDecision(BaseModel):
|
||
"""对最近一次审稿留痕里**某个冲突**(按其在 conflicts 列表的下标定位)的裁决。"""
|
||
|
||
conflict_index: int = Field(ge=0, description="冲突在最近审稿 conflicts 列表中的下标")
|
||
verdict: Verdict = Field(description="采纳改法 / 忽略 / 手改")
|
||
note: str | None = Field(default=None, description="可选裁决备注(如手改说明)")
|
||
|
||
|
||
class AcceptRequest(BaseModel):
|
||
"""POST /projects/:id/chapters/:no/accept:裁决清单 + 可能改过的终稿。"""
|
||
|
||
final_text: str = Field(min_length=1, description="作者裁决/改稿后的最终验收文本")
|
||
decisions: list[ConflictDecision] = Field(
|
||
default_factory=list, description="对最近审稿每个冲突的裁决(每冲突必有其一,R5)"
|
||
)
|
||
|
||
|
||
class AcceptResponse(BaseModel):
|
||
"""验收「本次将更新」清单(ARCH §7.2 写回结果)。"""
|
||
|
||
project_id: uuid.UUID
|
||
chapter_no: int
|
||
accepted_version: int = Field(description="晋升到的 accepted 版次(max+1)")
|
||
digest_added: bool = Field(description="是否新增了一行 chapter_digests")
|
||
decisions_recorded: int = Field(description="本次写回的裁决条数")
|
||
review_id: uuid.UUID | None = Field(default=None, description="写回裁决的审稿留痕行 id")
|