// 文风纯逻辑:指纹归一(dims+evidence)、漂移归一(从 chapter_reviews.style)、 // 学文风/回炉请求体组装。纯逻辑,便于 node 环境单测(C3 扩 T4.3 / C4 扩 T4.2)。 import type { ReviewHistoryItem, StyleFingerprintResponse, StyleLearnRequest, RefineRequest, } from "@/lib/api/types"; import type { StyleDriftReport, StyleDriftSegment } from "@/lib/review/sse"; export type { StyleDriftReport, StyleDriftSegment } from "@/lib/review/sse"; // ── 指纹(16 维 + 证据,UX §6.9)───────────────────────────────── // 后端 dimensions={name:value}、evidence={name:[quotes]}(松散 JSONB)。 export interface FingerprintDimension { name: string; value: string; evidence: string[]; } export interface Fingerprint { dimensions: FingerprintDimension[]; version: number; } function asStringArray(v: unknown): string[] { if (!Array.isArray(v)) return []; return v.filter((x): x is string => typeof x === "string"); } function asDisplayValue(v: unknown): string { if (typeof v === "string") return v; if (typeof v === "number" || typeof v === "boolean") return String(v); return ""; } // 把松散 GET /style 响应收窄成 Fingerprint。 // dimensions 的 key 顺序即维度顺序(身份);evidence 按维度名对齐。 export function normalizeFingerprint( resp: StyleFingerprintResponse | undefined, ): Fingerprint | null { if (!resp) return null; const dims = resp.dimensions ?? {}; const evid = resp.evidence ?? {}; const names = Object.keys(dims); const dimensions: FingerprintDimension[] = names.map((name) => ({ name, value: asDisplayValue(dims[name]), evidence: asStringArray(evid[name]), })); return { dimensions, version: resp.version }; } // ── 漂移(第四审,C4 扩 T4.2)──────────────────────────────────── // chapter_reviews.style = {score:int, segments:[{idx,score,label?}]}。 // 类型 StyleDriftReport/StyleDriftSegment 在 lib/review/sse.ts(reduce 复用),上面已 re-export。 function asInt(v: unknown, fallback = 0): number { return typeof v === "number" && Number.isFinite(v) ? Math.round(v) : fallback; } function asOptionalString(v: unknown): string | null { return typeof v === "string" ? v : null; } // 把松散 style dict 收窄成漂移报告;缺失/非 dict → null(不渲染)。 export function normalizeStyleDrift( item: ReviewHistoryItem | undefined, ): StyleDriftReport | null { const raw = item?.style; if (typeof raw !== "object" || raw === null) return null; const dict = raw as Record; const segRaw = Array.isArray(dict["segments"]) ? dict["segments"] : []; const segments: StyleDriftSegment[] = segRaw .filter((s): s is Record => typeof s === "object" && s !== null) .map((s) => ({ idx: asInt(s["idx"]), score: asInt(s["score"], 100), label: asOptionalString(s["label"]), })); // score 缺省 100(无指纹降级态,对齐后端默认)。 return { score: asInt(dict["score"], 100), segments }; } // 同 style SSE 事件 data 也用此收窄(reduceReview case "style" 复用)。 export function narrowStyleEvent(data: unknown): StyleDriftReport { if (typeof data !== "object" || data === null) { return { score: 100, segments: [] }; } const dict = data as Record; const segRaw = Array.isArray(dict["segments"]) ? dict["segments"] : []; const segments: StyleDriftSegment[] = segRaw .filter((s): s is Record => typeof s === "object" && s !== null) .map((s) => ({ idx: asInt(s["idx"]), score: asInt(s["score"], 100), label: asOptionalString(s["label"]), })); return { score: asInt(dict["score"], 100), segments }; } // ── 请求体组装 ────────────────────────────────────────────────── export type StyleLearnMode = "create" | "update"; // 组装学文风请求体:去掉空白样本;mode 透传(首学/更新仅前端语义)。 export function buildLearnRequest( samples: readonly string[], mode: StyleLearnMode, ): StyleLearnRequest { const cleaned = samples.map((s) => s.trim()).filter((s) => s.length > 0); return { samples: cleaned, mode }; } // 至少一条非空样本才可提交(对齐后端 min_length=1)。 export function hasUsableSamples(samples: readonly string[]): boolean { return samples.some((s) => s.trim().length > 0); } // 组装回炉请求体(段去空白;空指令省略)。 export function buildRefineRequest( segment: string, instruction?: string | null, ): RefineRequest { const body: RefineRequest = { segment: segment.trim() }; const trimmed = instruction?.trim(); if (trimmed) body.instruction = trimmed; return body; }