Files
writer-work-flow/apps/api/ww_api/schemas/toolbox.py
Yaojia Wang f43ccd293f feat(toolbox): T6 创作工具箱通用生成器框架 — 8 新生成器 + 声明驱动落地页 + P2 收尾
通用执行路径驱动全部生成器("加生成器=加一份声明"):
- @llm: ww_agents +7 输出 schema + 7 spec(book-title/blurb/name/golden-finger/
  glossary/opening/fine-outline,只声明 tier)+ build_outline_chapter_context
- @backend: ww_skills GeneratorTool 描述符 + TOOLBOX(11) + get_tool;3 通用端点
  GET /skills/toolbox · POST .../skills/{tool_key}/generate(预览不写库,仅记账) ·
  POST .../ingest(复用 continuity 409 + partition_writes 白名单);纯 context 派发
- @frontend: 工具箱落地页 RSC + 声明驱动 GeneratorRunner + lib/toolbox 纯函数
  + LeftNav「工具箱」+ ⌘K nav-toolbox/action-gen-*;legacy 3 跳现页
- @qa: tests/test_t6_toolbox_e2e.py 5 用例真 pg + mock 网关零 token,无端点 bug
- P2 收尾: 限流→decisions.md 记延后(单用户原型);noopener/Committable 早已修

守不变量 #2(只声明 tier)/#3(预览不写库,入库经验收 gate)/#9(缓存前缀)。无 DB 迁移。
门禁绿: 后端 ruff/format/mypy 195/alembic 无漂移/pytest 583;前端 lint/tsc/vitest 279/build。
spec 回写 PRODUCT_SPEC §7 + ARCHITECTURE §7.2 端点表。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 20:37:55 +02:00

142 lines
5.7 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="一句话需求/方向(可空,空则按题材发散)")
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 ToolIngestRequest(BaseModel):
"""通用入库请求:待持久化的产物 + 冲突确认。
仅声明了 `ingest` 的工具可用。按入库表取用对应字段:
- world_entities金手指/词条)→ `world_entities`(贴 WorldEntityCardViewtype/name/rules
- outline细纲→ `chapter_no` + `scenes`(贴 OutlineSceneIngestView
`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 入库的场景行(细纲)"
)
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