Files
Yaojia Wang a5351320a2 feat(skills): Scope B A2 — 4 竞品生成器注册表+上下文分派+端点接线
注册表 TOOLBOX +4 entry(continue/expand/de-ai/teardown,全 preview-only writes=[]);
ContextStrategy +with_prior_chapter/text_input;ToolGenerateRequest +text 字段。
toolbox_context 分派新策略(续写→build_continuation_context、原文→build_text_input_context);
路由续写经 chapter_repo 读最新 accepted/draft 正文注入 builder(core 不 import apps/api,仿 accept_op)。
预览仅 commit ledger 不写业务表(#3);只声明 tier(#2);system_prompt 进缓存前缀(#9)。
单测:4 生成器 generate 预览 + 续写读前文/草稿路由 + 扩写/降AI text 路由 + 拆书结构化;
更新 registry/strategy 计数测试(15 工具 / 6 策略)。门禁绿:ruff/format/mypy 196 files/pytest 168(api)+32(skills)。
2026-06-23 19:10:35 +02:00

108 lines
4.9 KiB
Python
Raw Permalink 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.

"""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
# 注入策略:决定为该生成器组哪些材料喂给网关(覆盖全部生成器)。
# - brief_only项目立意 + 用户一句话(脑洞/书名);
# - with_project+ 主线/卖点(简介);
# - with_world+ world_entities 卡(名字/金手指/词条——须契合世界观硬规则);
# - with_outline_chapter+ 指定章 outline beats细纲/黄金开篇);
# - with_prior_chapter+ 最新已写正文(续写——端点先读最新 accepted/draft 正文注入);
# - text_input+ 作者提供的原文(扩写/降AI率/拆书样本——经请求 text 字段注入)。
ContextStrategy = Literal[
"brief_only",
"with_project",
"with_world",
"with_outline_chapter",
"with_prior_chapter",
"text_input",
]
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 # 题材适用(卡片徽标/过滤)