// 本章注入透明(B0/F1)纯逻辑:入选理由 / 实体类型 → 中文徽标。 // 纯函数,便于 node 环境单测;渲染层只管样式。 import type { components } from "@/lib/api/schema"; export type InjectionResponse = components["schemas"]["InjectionResponse"]; export type InjectionEntity = components["schemas"]["InjectionEntity"]; export type InjectionEntityRef = components["schemas"]["InjectionEntityRef"]; export type InjectionOverrideRequest = components["schemas"]["InjectionOverrideRequest"]; // recent_n 合法区间(与后端 RECENT_N_MIN/MAX 对齐;越界后端 422)。 export const RECENT_N_MIN = 1; export const RECENT_N_MAX = 20; // SelectionReason → 作者可读徽标(对齐 ARCH §3.4 的四来源 + 作者置顶)。 export const REASON_LABELS: Record = { explicit_beat: "本章点名", main_character: "主角常驻", recent_digest: "近章出现", foreshadow_window: "伏笔窗口", author_pin: "作者置顶", }; // EntityKind → 中文。 export const KIND_LABELS: Record = { character: "角色", world_entity: "设定", }; // 未知 code 时回退原值(不吞,便于发现新理由)。 export function reasonLabel(reason: string): string { return REASON_LABELS[reason] ?? reason; } export function kindLabel(kind: string): string { return KIND_LABELS[kind] ?? kind; } // ---- 作者覆盖(B0 可控版)纯函数:从响应推导当前覆盖 + 不可变地施加单个改动 ---- const refKey = (r: InjectionEntityRef): string => `${r.kind}:${r.name}`; const sameRef = (a: InjectionEntityRef, b: InjectionEntityRef): boolean => a.kind === b.kind && a.name === b.name; // 从注入响应回显字段抽出当前覆盖(PUT 回放服务端确定结果后驱动 UI)。 export function currentOverride( data: InjectionResponse, ): InjectionOverrideRequest { return { pinned: [...(data.pinned ?? [])], excluded: [...(data.excluded ?? [])], recent_n: data.recent_n, }; } export function isPinned( override: InjectionOverrideRequest, ref: InjectionEntityRef, ): boolean { return (override.pinned ?? []).some((r) => sameRef(r, ref)); } const without = ( list: InjectionEntityRef[] | undefined, ref: InjectionEntityRef, ): InjectionEntityRef[] => (list ?? []).filter((r) => !sameRef(r, ref)); // 切换 pin:已置顶 → 取消;否则置顶并从排除列表移除(一个实体不同时 pin+排除)。 export function withPinToggled( override: InjectionOverrideRequest, ref: InjectionEntityRef, ): InjectionOverrideRequest { const pinnedNow = isPinned(override, ref); return { ...override, pinned: pinnedNow ? without(override.pinned, ref) : [...(override.pinned ?? []), { kind: ref.kind, name: ref.name }], excluded: without(override.excluded, ref), }; } // 排除:加入排除列表,并从置顶列表移除。重复排除幂等(先去重再加)。 export function withExcluded( override: InjectionOverrideRequest, ref: InjectionEntityRef, ): InjectionOverrideRequest { return { ...override, pinned: without(override.pinned, ref), excluded: [ ...without(override.excluded, ref), { kind: ref.kind, name: ref.name }, ], }; } // 恢复被排除的实体(回到纯自动选择判定)。 export function withRestored( override: InjectionOverrideRequest, ref: InjectionEntityRef, ): InjectionOverrideRequest { return { ...override, excluded: without(override.excluded, ref) }; } // 设置近况回看章数(钳到合法区间,避免后端 422)。 export function withRecentN( override: InjectionOverrideRequest, n: number, ): InjectionOverrideRequest { const clamped = Math.max(RECENT_N_MIN, Math.min(RECENT_N_MAX, Math.round(n))); return { ...override, recent_n: clamped }; } export { refKey };