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,169 @@
import { describe, expect, it } from "vitest";
import type { ToolDescriptorView, ToolInputFieldView } from "@/lib/api/types";
import {
buildGenerateRequest,
initialFieldValues,
isLegacyTool,
mapPreview,
missingRequiredFields,
previewRows,
resolveLegacyRoute,
} from "./toolbox";
const field = (over: Partial<ToolInputFieldView> = {}): ToolInputFieldView => ({
name: "brief",
label: "需求",
type: "textarea",
required: true,
...over,
});
describe("initialFieldValues", () => {
it("seeds defaults and empty strings", () => {
const values = initialFieldValues([
field({ name: "brief", default: undefined }),
field({ name: "count", type: "number", default: "3", required: false }),
]);
expect(values).toEqual({ brief: "", count: "3" });
});
it("handles undefined fields", () => {
expect(initialFieldValues(undefined)).toEqual({});
});
});
describe("missingRequiredFields", () => {
it("reports labels of blank required fields only", () => {
const fields = [
field({ name: "brief", label: "需求", required: true }),
field({ name: "count", label: "数量", required: false }),
];
expect(missingRequiredFields(fields, { brief: " ", count: "" })).toEqual([
"需求",
]);
});
it("passes when all required present", () => {
expect(
missingRequiredFields([field()], { brief: "一个脑洞" }),
).toEqual([]);
});
});
describe("buildGenerateRequest", () => {
it("trims brief and maps known number fields", () => {
expect(
buildGenerateRequest({ brief: " 修真 ", count: "5", chapter_no: "3" }),
).toEqual({ brief: "修真", count: 5, chapter_no: 3 });
});
it("omits blank/invalid numbers and empty kind", () => {
expect(
buildGenerateRequest({ brief: "x", count: "", chapter_no: "abc", kind: " " }),
).toEqual({ brief: "x" });
});
it("includes kind when present", () => {
expect(buildGenerateRequest({ brief: "x", kind: "门派" })).toEqual({
brief: "x",
kind: "门派",
});
});
it("defaults brief to empty string", () => {
expect(buildGenerateRequest({})).toEqual({ brief: "" });
});
});
describe("previewRows", () => {
it("picks the first array field as records", () => {
const rows = previewRows({ ideas: [{ premise: "a" }, { premise: "b" }] });
expect(rows).toHaveLength(2);
expect(rows[0]).toEqual({ premise: "a" });
});
it("returns empty for missing/non-array", () => {
expect(previewRows(undefined)).toEqual([]);
expect(previewRows({ note: "x" })).toEqual([]);
});
});
describe("mapPreview", () => {
it("maps IdeaListResult to heading + fields", () => {
const model = mapPreview("IdeaListResult", {
ideas: [{ premise: "废柴逆袭", hook: "系统觉醒", genre_fit: "玄幻" }],
});
expect(model.kind).toBe("IdeaListResult");
expect(model.items[0]?.heading).toBe("废柴逆袭");
expect(model.items[0]?.fields).toEqual([
{ label: "爽点", value: "系统觉醒" },
{ label: "题材契合", value: "玄幻" },
]);
});
it("maps BlurbResult text into body and angle into heading", () => {
const model = mapPreview("BlurbResult", {
variants: [{ angle: "悬念向", text: "一段简介" }],
});
expect(model.items[0]?.heading).toBe("悬念向");
expect(model.items[0]?.body).toBe("一段简介");
});
it("maps GlossaryResult rules into a joined field", () => {
const model = mapPreview("GlossaryResult", {
terms: [{ name: "灵气", type: "概念", rules: ["不可逆", "有上限"] }],
});
const ruleField = model.items[0]?.fields.find((f) => f.label === "硬规则");
expect(ruleField?.value).toBe("不可逆;有上限");
});
it("maps OpeningResult as pure body", () => {
const model = mapPreview("OpeningResult", {
variants: [{ text: "开篇第一段" }],
});
expect(model.items[0]?.heading).toBeNull();
expect(model.items[0]?.body).toBe("开篇第一段");
});
it("falls back gracefully for unknown output_kind", () => {
const model = mapPreview("MysteryResult", {
things: [{ a: "first", b: "second" }],
});
expect(model.items[0]?.heading).toBe("first");
expect(model.items[0]?.fields).toEqual([{ label: "b", value: "second" }]);
});
it("yields no items for empty preview", () => {
expect(mapPreview("IdeaListResult", undefined).items).toEqual([]);
});
});
describe("resolveLegacyRoute", () => {
it("substitutes the {id} placeholder", () => {
expect(resolveLegacyRoute("/projects/{id}/codex?gen=world", "p9")).toBe(
"/projects/p9/codex?gen=world",
);
});
});
describe("isLegacyTool", () => {
const tool = (over: Partial<ToolDescriptorView>): ToolDescriptorView => ({
key: "k",
title: "t",
subtitle: "s",
is_legacy: false,
ingestable: false,
...over,
});
it("is true only with legacy flag and a route", () => {
expect(
isLegacyTool(tool({ is_legacy: true, legacy_route: "/x/{id}" })),
).toBe(true);
expect(isLegacyTool(tool({ is_legacy: true }))).toBe(false);
expect(
isLegacyTool(tool({ is_legacy: false, legacy_route: "/x/{id}" })),
).toBe(false);
});
});