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>
This commit is contained in:
Yaojia Wang
2026-06-22 20:37:55 +02:00
parent 1f1afa37b6
commit f43ccd293f
46 changed files with 4848 additions and 12 deletions

View File

@@ -0,0 +1,103 @@
"""T6 创作工具箱 · 声明式生成器描述符类型(创作工具箱通用框架的契约)。
竞品把创作辅助做成一排「XX 生成器」卡片;本仓的 `AgentSpec`ARCH §5.1)本就是
「内置 Agent 与用户 Skill 同构声明」。这里再加一层**生成器描述符**:把「一个生成器」
声明成一条 `GeneratorTool`——复用 §5.1 的 `spec`tier/system_prompt/reads/writes
再补前端渲染所需的卡片文案 + 表单字段 + 注入策略 + 可选入库目标。
之后「加一个生成器」= 加一份 `GeneratorTool` 声明descriptor 在代码、不进 DB——
保 Pydantic schema 为真类避免迁移DB `skills` 表与只读注册表/权限故事不变)。
本模块只定义**类型**Wave-0 契约);`TOOLBOX: dict[str, GeneratorTool]` 注册表 seeding
属 Wave A需 @llm 落地的 8 个新 spec/output_schema
不可变:所有描述符 frozen——构造后不得改动全局 immutability 约定)。
"""
from __future__ import annotations
from typing import Literal
from pydantic import BaseModel, ConfigDict, model_validator
from ww_agents import AgentSpec
from ww_shared import AppError, ErrorCode
from ww_skills.skill_permissions import KNOWN_TABLES
# 注入策略:决定为该生成器组哪些材料喂给网关(覆盖全部生成器,~4 种)。
# - brief_only项目立意 + 用户一句话(脑洞/书名);
# - with_project+ 主线/卖点(简介);
# - with_world+ world_entities 卡(名字/金手指/词条——须契合世界观硬规则);
# - with_outline_chapter+ 指定章 outline beats细纲/黄金开篇)。
ContextStrategy = Literal[
"brief_only",
"with_project",
"with_world",
"with_outline_chapter",
]
class InputField(BaseModel):
"""声明式表单字段描述符前端据此渲染输入控件frozensnake_case
`type` 是前端控件类型("text"/"textarea"/"number"/"select" 等);`default` 统一以
字符串携带(声明层不区分数值/枚举的运行期类型,前端按 `type` 解释)。
"""
model_config = ConfigDict(frozen=True)
name: str # 表单字段名(提交时的 key
label: str # 展示标签
type: str # 控件类型text / textarea / number / select ...
required: bool = True
default: str | None = None
help: str | None = None # 字段说明/占位提示
class IngestSpec(BaseModel):
"""可选入库目标描述符None 表示纯预览不写库frozen
`table` 必须在 `KNOWN_TABLES` 创作表白名单内——驱动既有入库 gate
continuity 预检 + `partition_writes` 白名单 + schema→JSONB 形变,不变量 #3
越权/不存在的入库表 → AppError(VALIDATION)(不可信声明的第一道闸)。
KISS当前只需 `table` 即可驱动 gate更多形变字段属 Wave A 按需再加。
"""
model_config = ConfigDict(frozen=True)
table: str
@model_validator(mode="after")
def _check_table(self) -> IngestSpec:
if self.table not in KNOWN_TABLES:
raise AppError(
ErrorCode.VALIDATION,
f"IngestSpec 声明了未知入库表:{self.table}",
{"table": self.table},
)
return self
class GeneratorTool(BaseModel):
"""单个生成器的声明式描述符(创作工具箱卡片 + 执行路径的真源frozen
两类形态:
- 新工具:带 `spec`§5.1 声明)+ `output_schema`(结构化产出真类),走通用执行器;
- legacy 工具(已上架的 世界观/人设/大纲):`spec=None`,带 `legacy_route` 指向
现有更丰富的入库/冲突页面,不回归既测代码。
`ingest=None` 表示纯预览(不写库);`genre` 标注题材适用(前端徽标/过滤)。
"""
model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True)
key: str # 路由用稳定标识("brainstorm" / "book-title" ...
title: str # 卡片标题("脑洞生成器"
subtitle: str # 卡片副标题("突破想象,脑洞大开"
spec: AgentSpec | None # §5.1 声明tier/system_prompt/reads/writeslegacy 为 None
output_schema: type[BaseModel] | None # 结构化产出真类legacy/纯文本可为 None
context_strategy: ContextStrategy # 注入策略(组哪些材料)
input_fields: list[InputField] # 声明式表单
ingest: IngestSpec | None = None # 可入库目标None=纯预览
legacy_route: str | None = None # legacy 工具指向的现有页面/端点
genre: str | None = None # 题材适用(卡片徽标/过滤)