M4(文风): style-auditor 双轨(提取指纹/漂移第四审)+ jobs 长任务框架(zombie reaper) + 回炉 refine + GET /style read-back。 M5(生成+扩展): worldbuilder/character-gen(入库 continuity 409 gate + partition_writes 白名单 + schema→JSONB 形变); 网关多 provider 回退链/熔断/能力降级(Anthropic/Gemini 适配器);Skill registry + 表权限沙箱 + 规则; 前端 角色生成器/世界观/Codex/规则页/技能库/⌘K 命令面板。 K1(Kimi Code 订阅接入): OAuth device-flow(kimi-code)+ 静态 Console key(kimi-code-key)两路径; coding 端点 KimiCLI 伪造头(实测 UA allow-list 门禁,缺则 403)+ JSON 模式结构化(thinking ⊥ tool_choice)。 本地联调修复: CORS 中间件;assemble 注入 premise+「写第N章」指令(修空 prompt 400); GET /outline·/draft read-back + 大纲/工作台/审稿页重载;写页 client/server 常量边界 + notFound 健壮化; 字数 toLocaleString locale 水合;审稿页终稿从已存草稿 seed(修 accept 422)。 门禁: backend ruff/mypy(157)/alembic 无漂移/pytest 451 · frontend lint/tsc/vitest/build。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
130 lines
4.9 KiB
TypeScript
130 lines
4.9 KiB
TypeScript
// 文风纯逻辑:指纹归一(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<string, unknown>;
|
||
const segRaw = Array.isArray(dict["segments"]) ? dict["segments"] : [];
|
||
const segments: StyleDriftSegment[] = segRaw
|
||
.filter((s): s is Record<string, unknown> => 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<string, unknown>;
|
||
const segRaw = Array.isArray(dict["segments"]) ? dict["segments"] : [];
|
||
const segments: StyleDriftSegment[] = segRaw
|
||
.filter((s): s is Record<string, unknown> => 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;
|
||
}
|