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:
207
packages/skills/tests/test_toolbox.py
Normal file
207
packages/skills/tests/test_toolbox.py
Normal file
@@ -0,0 +1,207 @@
|
||||
"""T6 创作工具箱声明式描述符类型单测(Wave-0 契约)。
|
||||
|
||||
仅校验描述符**类型**本身(无 TOOLBOX 注册表 seeding——那是 Wave A):
|
||||
- `ContextStrategy`:4 种注入策略字面量;
|
||||
- `InputField`:声明式表单字段(label/type/required/default/help);
|
||||
- `IngestSpec`:可选入库目标(table 必须在 KNOWN_TABLES,None=纯预览);
|
||||
- `GeneratorTool`:生成器声明(legacy 带 legacy_route 且 spec 可为 None;新工具带 spec+schema)。
|
||||
|
||||
全部 frozen(不可变,全局 immutability 约定)——构造后改字段须抛错。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
|
||||
import pytest
|
||||
from pydantic import BaseModel, ValidationError
|
||||
from ww_agents import AgentSpec
|
||||
from ww_shared import AppError, ErrorCode
|
||||
from ww_skills import (
|
||||
ContextStrategy,
|
||||
GeneratorTool,
|
||||
IngestSpec,
|
||||
InputField,
|
||||
)
|
||||
|
||||
|
||||
class _FakeOut(BaseModel):
|
||||
"""构造测试用的 output_schema 占位(Pydantic 模型类)。"""
|
||||
|
||||
value: str
|
||||
|
||||
|
||||
def _spec() -> AgentSpec:
|
||||
return AgentSpec(
|
||||
name="brainstorm",
|
||||
tier="light",
|
||||
system_prompt="x",
|
||||
output_schema=_FakeOut,
|
||||
reads=["projects"],
|
||||
writes=[],
|
||||
)
|
||||
|
||||
|
||||
# ---- ContextStrategy ----
|
||||
|
||||
|
||||
def test_context_strategy_has_four_values() -> None:
|
||||
# Arrange / Act
|
||||
from typing import get_args
|
||||
|
||||
values = set(get_args(ContextStrategy))
|
||||
|
||||
# Assert
|
||||
assert values == {
|
||||
"brief_only",
|
||||
"with_project",
|
||||
"with_world",
|
||||
"with_outline_chapter",
|
||||
}
|
||||
|
||||
|
||||
# ---- InputField ----
|
||||
|
||||
|
||||
def test_input_field_required_fields_and_defaults() -> None:
|
||||
# Arrange / Act
|
||||
field = InputField(name="brief", label="一句话需求", type="textarea")
|
||||
|
||||
# Assert: required 默认 True,其余可空
|
||||
assert field.name == "brief"
|
||||
assert field.label == "一句话需求"
|
||||
assert field.type == "textarea"
|
||||
assert field.required is True
|
||||
assert field.default is None
|
||||
assert field.help is None
|
||||
|
||||
|
||||
def test_input_field_accepts_optional_metadata() -> None:
|
||||
field = InputField(
|
||||
name="count",
|
||||
label="数量",
|
||||
type="number",
|
||||
required=False,
|
||||
default="3",
|
||||
help="一次生成几条",
|
||||
)
|
||||
assert field.required is False
|
||||
assert field.default == "3"
|
||||
assert field.help == "一次生成几条"
|
||||
|
||||
|
||||
def test_input_field_is_frozen() -> None:
|
||||
field = InputField(name="brief", label="需求", type="text")
|
||||
with pytest.raises(ValidationError):
|
||||
field.label = "改名"
|
||||
|
||||
|
||||
# ---- IngestSpec ----
|
||||
|
||||
|
||||
def test_ingest_spec_accepts_known_table() -> None:
|
||||
spec = IngestSpec(table="world_entities")
|
||||
assert spec.table == "world_entities"
|
||||
|
||||
|
||||
def test_ingest_spec_rejects_unknown_table() -> None:
|
||||
# 越权/不存在的入库表 → VALIDATION(不可信声明的第一道闸)
|
||||
with pytest.raises(AppError) as exc:
|
||||
IngestSpec(table="users")
|
||||
assert exc.value.code is ErrorCode.VALIDATION
|
||||
|
||||
|
||||
def test_ingest_spec_is_frozen() -> None:
|
||||
spec = IngestSpec(table="outline")
|
||||
with pytest.raises(ValidationError):
|
||||
spec.table = "characters"
|
||||
|
||||
|
||||
# ---- GeneratorTool ----
|
||||
|
||||
|
||||
def test_generator_tool_new_tool_with_spec_and_schema() -> None:
|
||||
# Arrange / Act:新工具带 spec + output_schema,无 legacy_route
|
||||
tool = GeneratorTool(
|
||||
key="brainstorm",
|
||||
title="脑洞生成器",
|
||||
subtitle="突破想象,脑洞大开",
|
||||
spec=_spec(),
|
||||
output_schema=_FakeOut,
|
||||
context_strategy="brief_only",
|
||||
input_fields=[InputField(name="brief", label="需求", type="textarea", required=False)],
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert tool.key == "brainstorm"
|
||||
assert tool.spec is not None
|
||||
assert tool.output_schema is _FakeOut
|
||||
assert tool.context_strategy == "brief_only"
|
||||
assert tool.ingest is None
|
||||
assert tool.legacy_route is None
|
||||
assert tool.genre is None
|
||||
assert len(tool.input_fields) == 1
|
||||
|
||||
|
||||
def test_generator_tool_legacy_tool_without_spec() -> None:
|
||||
# legacy 工具:spec=None + 指向现有页面/端点
|
||||
tool = GeneratorTool(
|
||||
key="worldbuilding",
|
||||
title="世界观生成器",
|
||||
subtitle="构建自洽世界",
|
||||
spec=None,
|
||||
output_schema=None,
|
||||
context_strategy="with_project",
|
||||
input_fields=[],
|
||||
legacy_route="/projects/{id}/world",
|
||||
)
|
||||
assert tool.spec is None
|
||||
assert tool.output_schema is None
|
||||
assert tool.legacy_route == "/projects/{id}/world"
|
||||
|
||||
|
||||
def test_generator_tool_with_ingest_and_genre() -> None:
|
||||
tool = GeneratorTool(
|
||||
key="glossary",
|
||||
title="词条生成器",
|
||||
subtitle="批量造词条",
|
||||
spec=_spec(),
|
||||
output_schema=_FakeOut,
|
||||
context_strategy="with_world",
|
||||
input_fields=[],
|
||||
ingest=IngestSpec(table="world_entities"),
|
||||
genre="xuanhuan",
|
||||
)
|
||||
assert tool.ingest is not None
|
||||
assert tool.ingest.table == "world_entities"
|
||||
assert tool.genre == "xuanhuan"
|
||||
|
||||
|
||||
def test_generator_tool_is_frozen() -> None:
|
||||
tool = GeneratorTool(
|
||||
key="brainstorm",
|
||||
title="脑洞生成器",
|
||||
subtitle="x",
|
||||
spec=_spec(),
|
||||
output_schema=_FakeOut,
|
||||
context_strategy="brief_only",
|
||||
input_fields=[],
|
||||
)
|
||||
with pytest.raises(ValidationError):
|
||||
tool.title = "改名"
|
||||
|
||||
|
||||
def test_generator_tool_input_fields_default_empty() -> None:
|
||||
tool = GeneratorTool(
|
||||
key="x",
|
||||
title="t",
|
||||
subtitle="s",
|
||||
spec=None,
|
||||
output_schema=None,
|
||||
context_strategy="brief_only",
|
||||
input_fields=[],
|
||||
legacy_route="/x",
|
||||
)
|
||||
# input_fields 是显式入参;frozen 后整体不可重绑
|
||||
assert tool.input_fields == []
|
||||
assert not dataclasses.is_dataclass(tool) # 用 Pydantic,非 dataclass
|
||||
85
packages/skills/tests/test_toolbox_registry.py
Normal file
85
packages/skills/tests/test_toolbox_registry.py
Normal file
@@ -0,0 +1,85 @@
|
||||
"""T6.2 创作工具箱注册表(`TOOLBOX`)单测。
|
||||
|
||||
校验:11 条齐全(legacy 3 + 新 8);legacy 携 legacy_route 且 spec=None;新工具携 spec +
|
||||
output_schema(与 spec.output_schema 一致);ingest 工具的 table 在白名单内;`get_tool` 解析。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ww_skills import TOOLBOX, get_tool
|
||||
|
||||
_LEGACY_KEYS = {"worldbuilding", "character", "outline"}
|
||||
_NEW_KEYS = {
|
||||
"brainstorm",
|
||||
"book-title",
|
||||
"blurb",
|
||||
"name",
|
||||
"golden-finger",
|
||||
"glossary",
|
||||
"opening",
|
||||
"fine-outline",
|
||||
}
|
||||
_INGEST_KEYS = {"golden-finger", "glossary", "fine-outline"}
|
||||
|
||||
|
||||
def test_toolbox_has_all_eleven_tools() -> None:
|
||||
# Arrange / Act
|
||||
keys = set(TOOLBOX.keys())
|
||||
|
||||
# Assert
|
||||
assert keys == _LEGACY_KEYS | _NEW_KEYS
|
||||
assert len(TOOLBOX) == 11
|
||||
|
||||
|
||||
def test_legacy_tools_have_route_and_no_spec() -> None:
|
||||
for key in _LEGACY_KEYS:
|
||||
tool = TOOLBOX[key]
|
||||
assert tool.spec is None, key
|
||||
assert tool.output_schema is None, key
|
||||
assert tool.legacy_route is not None, key
|
||||
assert "{id}" in tool.legacy_route, key
|
||||
|
||||
|
||||
def test_new_tools_carry_spec_and_matching_schema() -> None:
|
||||
for key in _NEW_KEYS:
|
||||
tool = TOOLBOX[key]
|
||||
assert tool.spec is not None, key
|
||||
assert tool.output_schema is not None, key
|
||||
# 描述符的 output_schema 必须与其 spec 的 output_schema 一致(同一真类)。
|
||||
assert tool.output_schema is tool.spec.output_schema, key
|
||||
assert tool.legacy_route is None, key
|
||||
# 每个新工具的 key 与 spec.name 一致(路由稳定)。
|
||||
assert tool.key == tool.spec.name, key
|
||||
|
||||
|
||||
def test_ingest_tools_declare_known_table() -> None:
|
||||
for key, tool in TOOLBOX.items():
|
||||
if key in _INGEST_KEYS:
|
||||
assert tool.ingest is not None, key
|
||||
else:
|
||||
assert tool.ingest is None, key
|
||||
assert TOOLBOX["golden-finger"].ingest is not None
|
||||
assert TOOLBOX["golden-finger"].ingest.table == "world_entities"
|
||||
assert TOOLBOX["glossary"].ingest is not None
|
||||
assert TOOLBOX["glossary"].ingest.table == "world_entities"
|
||||
assert TOOLBOX["fine-outline"].ingest is not None
|
||||
assert TOOLBOX["fine-outline"].ingest.table == "outline"
|
||||
|
||||
|
||||
def test_chapter_tools_have_chapter_no_field() -> None:
|
||||
for key in ("opening", "fine-outline"):
|
||||
field_names = {f.name for f in TOOLBOX[key].input_fields}
|
||||
assert "chapter_no" in field_names, key
|
||||
|
||||
|
||||
def test_every_tool_has_brief_field_where_applicable() -> None:
|
||||
# 新工具均带 brief textarea(声明驱动表单的统一入口)。
|
||||
for key in _NEW_KEYS:
|
||||
field_names = {f.name for f in TOOLBOX[key].input_fields}
|
||||
assert "brief" in field_names, key
|
||||
|
||||
|
||||
def test_get_tool_resolves_known_and_unknown() -> None:
|
||||
assert get_tool("brainstorm") is TOOLBOX["brainstorm"]
|
||||
assert get_tool("worldbuilding") is TOOLBOX["worldbuilding"]
|
||||
assert get_tool("does-not-exist") is None
|
||||
@@ -22,6 +22,13 @@ from ww_skills.skill_registry import (
|
||||
SkillRepo,
|
||||
SqlSkillRepo,
|
||||
)
|
||||
from ww_skills.toolbox import (
|
||||
ContextStrategy,
|
||||
GeneratorTool,
|
||||
IngestSpec,
|
||||
InputField,
|
||||
)
|
||||
from ww_skills.toolbox_registry import TOOLBOX, get_tool
|
||||
|
||||
__all__ = [
|
||||
"KNOWN_TABLES",
|
||||
@@ -32,4 +39,10 @@ __all__ = [
|
||||
"SkillRegistry",
|
||||
"SkillRepo",
|
||||
"SqlSkillRepo",
|
||||
"ContextStrategy",
|
||||
"GeneratorTool",
|
||||
"IngestSpec",
|
||||
"InputField",
|
||||
"TOOLBOX",
|
||||
"get_tool",
|
||||
]
|
||||
|
||||
103
packages/skills/ww_skills/toolbox.py
Normal file
103
packages/skills/ww_skills/toolbox.py
Normal 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):
|
||||
"""声明式表单字段描述符(前端据此渲染输入控件;frozen;snake_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/writes);legacy 为 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 # 题材适用(卡片徽标/过滤)
|
||||
187
packages/skills/ww_skills/toolbox_registry.py
Normal file
187
packages/skills/ww_skills/toolbox_registry.py
Normal file
@@ -0,0 +1,187 @@
|
||||
"""T6 创作工具箱 · 生成器注册表(`TOOLBOX`:把每个生成器声明成一条 `GeneratorTool`)。
|
||||
|
||||
「加一个生成器」= 在本表加一份声明(descriptor 在代码、不进 DB——保 Pydantic schema
|
||||
为真类,避免迁移;DB `skills` 表与只读注册表/权限故事不变)。
|
||||
|
||||
两类形态:
|
||||
- **legacy**(已上架的 世界观/人设/大纲):`spec=None`,带 `legacy_route` 指向现有更丰富的
|
||||
入库/冲突页面,前端据 `legacy_route` 直接跳页,不回归既测代码(通用 generate/ingest
|
||||
端点对它们返 404/422);
|
||||
- **新工具**(8 个):带 `spec`(§5.1 声明)+ `output_schema`(结构化产物真类)+
|
||||
`context_strategy`(注入策略)+ `input_fields`(声明式表单),走通用执行器;声明 `ingest`
|
||||
者可经通用 ingest 端点入库(复用既有 continuity 预检 + 白名单 gate,不变量 #3)。
|
||||
|
||||
不变量 #2(只声明 tier,spec 已守)/ #3(AI 产出入库必经验收 gate,预览不写库)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ww_agents import (
|
||||
blurb_spec,
|
||||
book_title_spec,
|
||||
brainstorm_spec,
|
||||
fine_outline_spec,
|
||||
glossary_spec,
|
||||
golden_finger_spec,
|
||||
name_spec,
|
||||
opening_spec,
|
||||
)
|
||||
from ww_agents.schemas import (
|
||||
BlurbResult,
|
||||
DetailedOutlineResult,
|
||||
GlossaryResult,
|
||||
GoldenFingerResult,
|
||||
IdeaListResult,
|
||||
NameListResult,
|
||||
OpeningResult,
|
||||
TitleListResult,
|
||||
)
|
||||
|
||||
from ww_skills.toolbox import GeneratorTool, IngestSpec, InputField
|
||||
|
||||
# 复用的常见输入字段(DRY:每个生成器都有「一句话需求」textarea)。
|
||||
_BRIEF_FIELD = InputField(
|
||||
name="brief",
|
||||
label="一句话需求",
|
||||
type="textarea",
|
||||
required=False,
|
||||
help="描述创作方向/约束;留空则按作品设定与题材自由发散",
|
||||
)
|
||||
|
||||
|
||||
def _chapter_no_field() -> InputField:
|
||||
"""按章展开的生成器(开篇/细纲)所需的章号输入。"""
|
||||
return InputField(
|
||||
name="chapter_no",
|
||||
label="章号",
|
||||
type="number",
|
||||
required=True,
|
||||
default="1",
|
||||
help="要生成/展开的章节号(读取该章大纲节拍)",
|
||||
)
|
||||
|
||||
|
||||
# 创作工具箱注册表:key → 描述符。legacy 3 + 新 8 = 11 条。
|
||||
TOOLBOX: dict[str, GeneratorTool] = {
|
||||
# ---- legacy(spec=None,前端走 legacy_route 跳现有页面)----
|
||||
"worldbuilding": GeneratorTool(
|
||||
key="worldbuilding",
|
||||
title="世界观生成器",
|
||||
subtitle="构建内部自洽的世界观与硬规则",
|
||||
spec=None,
|
||||
output_schema=None,
|
||||
context_strategy="with_project",
|
||||
input_fields=[],
|
||||
legacy_route="/projects/{id}/codex?gen=world",
|
||||
),
|
||||
"character": GeneratorTool(
|
||||
key="character",
|
||||
title="人设生成器",
|
||||
subtitle="群像防雷同的结构化角色卡",
|
||||
spec=None,
|
||||
output_schema=None,
|
||||
context_strategy="with_world",
|
||||
input_fields=[],
|
||||
legacy_route="/projects/{id}/codex?gen=character",
|
||||
),
|
||||
"outline": GeneratorTool(
|
||||
key="outline",
|
||||
title="大纲生成器",
|
||||
subtitle="分卷分章 + 伏笔回收窗口",
|
||||
spec=None,
|
||||
output_schema=None,
|
||||
context_strategy="with_project",
|
||||
input_fields=[],
|
||||
legacy_route="/projects/{id}/outline",
|
||||
),
|
||||
# ---- 新工具(走通用执行器)----
|
||||
"brainstorm": GeneratorTool(
|
||||
key="brainstorm",
|
||||
title="脑洞生成器",
|
||||
subtitle="突破想象,脑洞大开",
|
||||
spec=brainstorm_spec,
|
||||
output_schema=IdeaListResult,
|
||||
context_strategy="brief_only",
|
||||
input_fields=[_BRIEF_FIELD],
|
||||
),
|
||||
"book-title": GeneratorTool(
|
||||
key="book-title",
|
||||
title="书名生成器",
|
||||
subtitle="一秒生成抓人书名",
|
||||
spec=book_title_spec,
|
||||
output_schema=TitleListResult,
|
||||
context_strategy="brief_only",
|
||||
input_fields=[_BRIEF_FIELD],
|
||||
),
|
||||
"blurb": GeneratorTool(
|
||||
key="blurb",
|
||||
title="简介生成器",
|
||||
subtitle="多版差异化书页文案",
|
||||
spec=blurb_spec,
|
||||
output_schema=BlurbResult,
|
||||
context_strategy="with_project",
|
||||
input_fields=[_BRIEF_FIELD],
|
||||
),
|
||||
"name": GeneratorTool(
|
||||
key="name",
|
||||
title="名字生成器",
|
||||
subtitle="契合世界观的人/物/地命名",
|
||||
spec=name_spec,
|
||||
output_schema=NameListResult,
|
||||
context_strategy="with_world",
|
||||
input_fields=[
|
||||
_BRIEF_FIELD,
|
||||
InputField(
|
||||
name="kind",
|
||||
label="命名对象",
|
||||
type="text",
|
||||
required=False,
|
||||
help="人物 / 势力 / 地点 / 功法 / 物品 等",
|
||||
),
|
||||
],
|
||||
),
|
||||
"golden-finger": GeneratorTool(
|
||||
key="golden-finger",
|
||||
title="金手指生成器",
|
||||
subtitle="自洽机制 + 成长 + 限制代价",
|
||||
spec=golden_finger_spec,
|
||||
output_schema=GoldenFingerResult,
|
||||
context_strategy="with_world",
|
||||
input_fields=[_BRIEF_FIELD],
|
||||
ingest=IngestSpec(table="world_entities"),
|
||||
),
|
||||
"glossary": GeneratorTool(
|
||||
key="glossary",
|
||||
title="词条生成器",
|
||||
subtitle="带硬规则的世界观术语表",
|
||||
spec=glossary_spec,
|
||||
output_schema=GlossaryResult,
|
||||
context_strategy="with_world",
|
||||
input_fields=[_BRIEF_FIELD],
|
||||
ingest=IngestSpec(table="world_entities"),
|
||||
),
|
||||
"opening": GeneratorTool(
|
||||
key="opening",
|
||||
title="黄金开篇生成器",
|
||||
subtitle="多版高代入感开篇正文",
|
||||
spec=opening_spec,
|
||||
output_schema=OpeningResult,
|
||||
context_strategy="with_outline_chapter",
|
||||
input_fields=[_chapter_no_field(), _BRIEF_FIELD],
|
||||
),
|
||||
"fine-outline": GeneratorTool(
|
||||
key="fine-outline",
|
||||
title="细纲生成器",
|
||||
subtitle="把章节粗节拍展开为场景序列",
|
||||
spec=fine_outline_spec,
|
||||
output_schema=DetailedOutlineResult,
|
||||
context_strategy="with_outline_chapter",
|
||||
input_fields=[_chapter_no_field(), _BRIEF_FIELD],
|
||||
ingest=IngestSpec(table="outline"),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def get_tool(key: str) -> GeneratorTool | None:
|
||||
"""按 key 取生成器描述符;未知 key → None(端点据此返 404)。"""
|
||||
return TOOLBOX.get(key)
|
||||
Reference in New Issue
Block a user