通用执行路径驱动全部生成器("加生成器=加一份声明"):
- @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>
114 lines
3.6 KiB
TypeScript
114 lines
3.6 KiB
TypeScript
// 工具箱入库纯逻辑:把结构化预览产物按 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;
|
||
}
|
||
}
|