// 工具箱入库纯逻辑:把结构化预览产物按 output_kind 形变为 ToolIngestRequest 的入库项。 // 复用既有入库双 gate(continuity 预检 409 + partition_writes 白名单),故仅负责 body 组装。 // 对齐契约:world_entities(金手指/词条)贴 WorldEntityCardView;outline(细纲)贴 OutlineSceneIngestView。 import type { OutlineSceneIngestView, TeardownIngestView, 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"]), }; } // 拆书结构化产物 → TeardownIngestView(贴后端 schema;后端再拍平为可读 rules 条目)。 // 单对象产物:整个 preview 对象即一条入库项(无逐行勾选)。 function rowToTeardown(row: Record): TeardownIngestView { return { themes: asStringArray(row["themes"]), archetypes: asStringArray(row["archetypes"]), structure: asString(row["structure"]), hooks: asStringArray(row["hooks"]), }; } export interface IngestBuildInput { outputKind: string; // 作者勾选要入库的预览行(结构化产物原始记录)。 rows: readonly Record[]; // 细纲入库的目标章号(outputKind=DetailedOutlineResult 时必填)。 chapterNo?: number | null; acknowledgeConflicts?: boolean; } // 单对象入库的 output_kind:整个 preview 即一条入库项(无逐行勾选)。 // 当前仅拆书(结构化结论拍平为 rules)。行式产物(金手指/词条/细纲)不在此列。 const SINGLE_OBJECT_INGEST_KINDS = new Set(["BookTeardownResult"]); // 该产物是否「单对象入库」(UI 据此隐藏勾选框、改文案、用整个 rawPreview 作唯一行)。 export function isSingleObjectIngest(outputKind: string): boolean { return SINGLE_OBJECT_INGEST_KINDS.has(outputKind); } // 入库目标表(供 UI 文案 / 判定)。未知 → null(不可入库)。 export function ingestTable(outputKind: string): string | null { switch (outputKind) { case "GoldenFingerResult": case "GlossaryResult": return "world_entities"; case "DetailedOutlineResult": return "outline"; case "BookTeardownResult": return "rules"; 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, }; case "BookTeardownResult": { // 单对象产物:取首行(整个拆书结论)作 teardown;无产物 → null。 const row = input.rows[0]; if (!row) return null; return { teardown: rowToTeardown(row), acknowledge_conflicts }; } default: return null; } }