- injection.ts 加 override 纯函数(currentOverride/withPinToggled/withExcluded/withRestored/withRecentN/isPinned,钳 1..20)+ author_pin 徽标 - useInjection 加 saving 态 + togglePin/exclude/restore/setRecentN,PUT 后以服务端确定结果为准(不变量 #6);写失败可读文案、不改本地态(天然回滚) - ChapterAssistant:每实体置顶/排除按钮 + 「近 N 章」步进器 + 「已排除」恢复区 - 13 新 vitest(override 纯函数);前端门禁绿(lint/tsc/vitest 183/build) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
117 lines
3.8 KiB
TypeScript
117 lines
3.8 KiB
TypeScript
// 本章注入透明(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<string, string> = {
|
||
explicit_beat: "本章点名",
|
||
main_character: "主角常驻",
|
||
recent_digest: "近章出现",
|
||
foreshadow_window: "伏笔窗口",
|
||
author_pin: "作者置顶",
|
||
};
|
||
|
||
// EntityKind → 中文。
|
||
export const KIND_LABELS: Record<string, string> = {
|
||
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 };
|