// 工具箱入库纯逻辑:把结构化预览产物按 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, ): 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): 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, 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[]; // 细纲入库的目标章号(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; } }