"""创作工具箱通用端点的请求/响应 schema(C3 扩 T6 / 通用生成器框架)。 snake_case;前端经 OpenAPI 生成 TS 类型消费。改字段 → 前端必须 `pnpm gen:api`。 - `GET /skills/toolbox` → `ToolboxListResponse`(声明式描述符视图,含 legacy_route 供前端路由)。 - `POST .../skills/{tool_key}/generate` → `ToolGeneratePreviewResponse`(结构化预览,不入库)。 - `POST .../skills/{tool_key}/ingest` → `ToolIngestResponse`(过 continuity gate + 白名单后入库)。 预览/入库共用既有 schema:world_entities 入库走 `WorldEntityCardView`(贴 worldbuilder 形), outline 入库走 `OutlineSceneIngestView`(贴 fine-outline 场景行)。 """ from __future__ import annotations from typing import Any from pydantic import BaseModel, Field from ww_api.schemas.generation import IngestConflictView, WorldEntityCardView # 一次性生成数量上限沿用 generation 的保守值(避免巨量调用);章号下限。 MIN_CHAPTER_NO = 1 # ---- GET /skills/toolbox(描述符列表)---- class ToolInputFieldView(BaseModel): """单个声明式表单字段视图(贴 ww_skills.InputField)。""" name: str label: str type: str = Field(description="控件类型:text / textarea / number / select 等") required: bool = True default: str | None = None help: str | None = None class ToolDescriptorView(BaseModel): """单个生成器卡片描述符视图(前端据此渲染卡片栅格 + 输入表单)。""" key: str = Field(description="路由用稳定标识(brainstorm / book-title ...)") title: str subtitle: str genre: str | None = None is_legacy: bool = Field(description="legacy 工具(spec=None)→ 前端走 legacy_route 跳现有页面") ingestable: bool = Field(description="是否可入库(声明了 ingest 目标)") input_fields: list[ToolInputFieldView] = Field(default_factory=list) legacy_route: str | None = Field( default=None, description="legacy 工具指向的现有页面路径(含 {id} 占位)" ) class ToolboxListResponse(BaseModel): """GET /skills/toolbox:创作工具箱全量描述符(legacy + 新工具)。""" tools: list[ToolDescriptorView] = Field(default_factory=list) # ---- POST .../skills/{tool_key}/generate(结构化预览)---- class ToolGenerateRequest(BaseModel): """通用生成请求(覆盖全部工具的可选入参;按工具 input_fields 取用,snake_case)。""" brief: str = Field(default="", description="一句话需求/方向(可空,空则按题材发散)") text: str | None = Field( default=None, description="原文输入(扩写/降AI 的原文、拆书的样本;text_input 策略工具用)", ) chapter_no: int | None = Field( default=None, ge=MIN_CHAPTER_NO, description="按章展开/续写类工具的章号(开篇/细纲/续写)", ) count: int | None = Field(default=None, ge=1, le=12, description="生成数量(部分工具可用)") kind: str | None = Field(default=None, description="命名对象类别等(部分工具可用)") class ToolGeneratePreviewResponse(BaseModel): """通用生成预览:结构化产物(不持久化;仅记 ledger)。 `output_kind` = 该工具产物的 schema 名(如 IdeaListResult),供前端按类型选预览渲染器; `preview` = 解析后的结构化产物(按各工具 output_schema 的形,键名见 ww_agents.schemas)。 """ tool_key: str output_kind: str = Field(description="产物 schema 名(前端按此选预览渲染器)") preview: dict[str, Any] = Field( default_factory=dict, description="结构化产物(各工具 output_schema 的 model_dump;不入库)", ) # ---- POST .../skills/{tool_key}/ingest(入库;过 continuity gate + 白名单)---- class OutlineSceneIngestView(BaseModel): """细纲入库的单章场景行(贴 ww_agents.Scene;落 outline 表)。""" idx: int beat: str purpose: str = "" conflict: str = "" hook: str = "" class TeardownIngestView(BaseModel): """拆书入库产物(贴 ww_agents.BookTeardownResult;落 rules 表)。F1。""" themes: list[str] = Field(default_factory=list, description="核心主题/立意清单") archetypes: list[str] = Field(default_factory=list, description="人物原型/角色模板清单") structure: str = Field(default="", description="叙事结构概述") hooks: list[str] = Field(default_factory=list, description="抓人钩子/爽点套路清单") class ToolIngestRequest(BaseModel): """通用入库请求:待持久化的产物 + 冲突确认。 仅声明了 `ingest` 的工具可用。按入库表取用对应字段: - world_entities(金手指/词条)→ `world_entities`(贴 WorldEntityCardView:type/name/rules); - outline(细纲)→ `chapter_no` + `scenes`(贴 OutlineSceneIngestView); - rules(拆书)→ `teardown`(贴 TeardownIngestView:themes/archetypes/structure/hooks)。 `acknowledge_conflicts`:作者已查看并接受 continuity 预检冲突时置 true 放行(仿 accept gate)。 """ world_entities: list[WorldEntityCardView] = Field( default_factory=list, description="world_entities 入库项(金手指/词条)" ) chapter_no: int | None = Field( default=None, ge=MIN_CHAPTER_NO, description="outline 入库的目标章号(细纲)" ) scenes: list[OutlineSceneIngestView] = Field( default_factory=list, description="outline 入库的场景行(细纲)" ) teardown: TeardownIngestView | None = Field( default=None, description="rules 入库的拆书结论(F1;拍平为可读规则条目)" ) acknowledge_conflicts: bool = Field( default=False, description="作者已知悉并接受 continuity 冲突 → 放行入库" ) class ToolIngestResponse(BaseModel): """通用入库结果(201):写入项标识 + 被白名单丢弃的越权表(审计)。""" table: str = Field(description="入库目标表") created: list[str] = Field( default_factory=list, description="写入项的标识(实体名 / 场景索引),按入参顺序" ) rejected_tables: list[str] = Field( default_factory=list, description="被权限白名单丢弃的越权写表名(审计;正常为空)" ) class ToolIngestConflictDetails(BaseModel): """409 冲突详情(贴 generation 的 IngestConflictView)。""" conflicts: list[IngestConflictView] = Field(default_factory=list) conflict_count: int = 0