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:
113
apps/web/lib/toolbox/ingest.ts
Normal file
113
apps/web/lib/toolbox/ingest.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
// 工具箱入库纯逻辑:把结构化预览产物按 output_kind 形变为 ToolIngestRequest 的入库项。
|
||||
// 复用既有入库双 gate(continuity 预检 409 + partition_writes 白名单),故仅负责 body 组装。
|
||||
// 对齐契约:world_entities(金手指/词条)贴 WorldEntityCardView;outline(细纲)贴 OutlineSceneIngestView。
|
||||
|
||||
import type {
|
||||
OutlineSceneIngestView,
|
||||
ToolIngestRequest,
|
||||
WorldEntityCardView,
|
||||
} from "@/lib/api/types";
|
||||
|
||||
function asString(v: unknown): string {
|
||||
if (typeof v === "string") return v;
|
||||
if (typeof v === "number" || typeof v === "boolean") return String(v);
|
||||
return "";
|
||||
}
|
||||
|
||||
function asStringArray(v: unknown): string[] {
|
||||
if (!Array.isArray(v)) return [];
|
||||
return v.filter((x): x is string => typeof x === "string");
|
||||
}
|
||||
|
||||
function asInt(v: unknown, fallback: number): number {
|
||||
if (typeof v === "number" && Number.isFinite(v)) return Math.round(v);
|
||||
if (typeof v === "string") {
|
||||
const n = Number(v.trim());
|
||||
if (Number.isFinite(n)) return Math.round(n);
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
// 金手指 → world_entities:type 固定「力量体系」,mechanism/growth/limits 合并入 rules(硬规则供续审比对)。
|
||||
function goldenFingerToEntity(
|
||||
row: Record<string, unknown>,
|
||||
): WorldEntityCardView {
|
||||
const rules = [
|
||||
asString(row["mechanism"]),
|
||||
asString(row["growth"]),
|
||||
asString(row["limits"]),
|
||||
].filter((r) => r.length > 0);
|
||||
return { type: "力量体系", name: asString(row["name"]), rules };
|
||||
}
|
||||
|
||||
// 词条 → world_entities:保留 type/name,rules 取 rules(已是硬规则清单)。
|
||||
function glossaryToEntity(row: Record<string, unknown>): WorldEntityCardView {
|
||||
return {
|
||||
type: asString(row["type"]) || "概念",
|
||||
name: asString(row["name"]),
|
||||
rules: asStringArray(row["rules"]),
|
||||
};
|
||||
}
|
||||
|
||||
// 细纲场景 → outline scenes:贴 OutlineSceneIngestView(idx/beat/purpose/conflict/hook)。
|
||||
function rowToScene(
|
||||
row: Record<string, unknown>,
|
||||
fallbackIdx: number,
|
||||
): OutlineSceneIngestView {
|
||||
return {
|
||||
idx: asInt(row["idx"], fallbackIdx),
|
||||
beat: asString(row["beat"]),
|
||||
purpose: asString(row["purpose"]),
|
||||
conflict: asString(row["conflict"]),
|
||||
hook: asString(row["hook"]),
|
||||
};
|
||||
}
|
||||
|
||||
export interface IngestBuildInput {
|
||||
outputKind: string;
|
||||
// 作者勾选要入库的预览行(结构化产物原始记录)。
|
||||
rows: readonly Record<string, unknown>[];
|
||||
// 细纲入库的目标章号(outputKind=DetailedOutlineResult 时必填)。
|
||||
chapterNo?: number | null;
|
||||
acknowledgeConflicts?: boolean;
|
||||
}
|
||||
|
||||
// 入库目标表(供 UI 文案 / 判定)。未知 → null(不可入库)。
|
||||
export function ingestTable(outputKind: string): string | null {
|
||||
switch (outputKind) {
|
||||
case "GoldenFingerResult":
|
||||
case "GlossaryResult":
|
||||
return "world_entities";
|
||||
case "DetailedOutlineResult":
|
||||
return "outline";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 组装通用入库请求;不可入库的 output_kind → null(调用方据此隐藏入库 UI)。
|
||||
export function buildIngestRequest(
|
||||
input: IngestBuildInput,
|
||||
): ToolIngestRequest | null {
|
||||
const acknowledge_conflicts = input.acknowledgeConflicts ?? false;
|
||||
switch (input.outputKind) {
|
||||
case "GoldenFingerResult":
|
||||
return {
|
||||
world_entities: input.rows.map(goldenFingerToEntity),
|
||||
acknowledge_conflicts,
|
||||
};
|
||||
case "GlossaryResult":
|
||||
return {
|
||||
world_entities: input.rows.map(glossaryToEntity),
|
||||
acknowledge_conflicts,
|
||||
};
|
||||
case "DetailedOutlineResult":
|
||||
return {
|
||||
chapter_no: input.chapterNo ?? null,
|
||||
scenes: input.rows.map((row, i) => rowToScene(row, i)),
|
||||
acknowledge_conflicts,
|
||||
};
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user