- teardown_spec.writes=["rules"](只声明 tier,入库仍经白名单 gate,不变量 #3) - toolbox_registry teardown entry 加 ingest=IngestSpec(table="rules") - routers/toolbox.py 通用 ingest dispatcher 加 _ingest_rules: BookTeardownResult(themes/archetypes/structure/hooks) 拍平为可读 rules 条目, 落项目级(无 continuity 预检,仿 _ingest_outline 无 409) - schemas/toolbox.py 加 TeardownIngestView + ToolIngestRequest.teardown - 单测:teardown ingest→rules 真落行 + 预览(generate)不写库 后端门禁绿:ruff/format/mypy clean · alembic no drift · pytest 644 passed
161 lines
6.6 KiB
Python
161 lines
6.6 KiB
Python
"""创作工具箱通用端点的请求/响应 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
|