Files
writer-work-flow/apps/api/ww_api/schemas/toolbox.py
Yaojia Wang 61398a1452 feat(toolbox): F1 拆书 teardown 落库成项目 rules
- 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
2026-06-23 20:00:25 +02:00

161 lines
6.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 扩 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 + 白名单后入库)。
预览/入库共用既有 schemaworld_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`(贴 WorldEntityCardViewtype/name/rules
- outline细纲→ `chapter_no` + `scenes`(贴 OutlineSceneIngestView
- rules拆书→ `teardown`(贴 TeardownIngestViewthemes/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