diff --git a/apps/web/lib/workbench/clarify.test.ts b/apps/web/lib/workbench/clarify.test.ts index 4031491..e411806 100644 --- a/apps/web/lib/workbench/clarify.test.ts +++ b/apps/web/lib/workbench/clarify.test.ts @@ -1,8 +1,10 @@ import { describe, expect, it } from "vitest"; import { + PROCEED, foldClarifications, needsClarifyGate, + toVM, type ClarifyAnswer, } from "./clarify"; @@ -67,6 +69,70 @@ describe("foldClarifications", () => { }); }); +describe("toVM", () => { + it("snake_case ClarifyDecision → camelCase VM,逐字段映射", () => { + const vm = toVM({ + need_clarification: true, + questions: [ + { + question: "想更冷峻还是更抒情?", + options: [ + { label: "冷峻", value: "改得更冷峻克制" }, + { label: "抒情", value: "改得更抒情" }, + ], + allow_free_text: true, + }, + ], + verification: "我这样理解对吗", + }); + + expect(vm).toEqual({ + needClarification: true, + questions: [ + { + question: "想更冷峻还是更抒情?", + options: [ + { label: "冷峻", value: "改得更冷峻克制" }, + { label: "抒情", value: "改得更抒情" }, + ], + allowFreeText: true, + }, + ], + verification: "我这样理解对吗", + }); + }); + + it("字段缺省时取安全默认(空/false/undefined),不信任外部数据", () => { + const vm = toVM({}); + expect(vm).toEqual({ + needClarification: false, + questions: [], + verification: undefined, + }); + }); + + it("questions/options 内字段缺省时逐项补默认", () => { + const vm = toVM({ + need_clarification: true, + questions: [{ question: undefined, options: [{}], allow_free_text: undefined }], + verification: null, + }); + expect(vm).toEqual({ + needClarification: true, + questions: [ + { question: "", options: [{ label: "", value: "" }], allowFreeText: false }, + ], + verification: undefined, + }); + }); +}); + +describe("PROCEED", () => { + it("放行常量:不反问、无问题", () => { + expect(PROCEED).toEqual({ needClarification: false, questions: [] }); + }); +}); + describe("needsClarifyGate", () => { it("去空白后长度小于 minChars 时需要预检", () => { expect(needsClarifyGate("改一下", 8)).toBe(true); diff --git a/apps/web/lib/workbench/clarify.ts b/apps/web/lib/workbench/clarify.ts index e023507..725e39f 100644 --- a/apps/web/lib/workbench/clarify.ts +++ b/apps/web/lib/workbench/clarify.ts @@ -62,3 +62,33 @@ export function foldClarifications( export function needsClarifyGate(instruction: string, minChars: number): boolean { return instruction.trim().length < minChars; } + +// 澄清预检结论为「放行」(直接去 refine/rewrite,不反问)——后端 error/前端异常时的确定性回退。 +export const PROCEED: ClarifyDecisionVM = { needClarification: false, questions: [] }; + +// 后端 snake_case ClarifyDecision 的原始形状(外部数据,全部可选——不信任、显式取值 + 默认)。 +export interface ClarifyDecisionRaw { + need_clarification?: boolean; + questions?: { + question?: string; + options?: { label?: string; value?: string }[]; + allow_free_text?: boolean; + }[]; + verification?: string | null; +} + +// 后端 snake_case ClarifyDecision → 前端 camelCase VM(纯映射,refine/rewrite 两侧预检共用)。 +export function toVM(data: ClarifyDecisionRaw): ClarifyDecisionVM { + return { + needClarification: Boolean(data.need_clarification), + questions: (data.questions ?? []).map((q) => ({ + question: q.question ?? "", + options: (q.options ?? []).map((o) => ({ + label: o.label ?? "", + value: o.value ?? "", + })), + allowFreeText: Boolean(q.allow_free_text), + })), + verification: data.verification ?? undefined, + }; +} diff --git a/apps/web/lib/workbench/useClarify.ts b/apps/web/lib/workbench/useClarify.ts index 5e56f18..07f06e1 100644 --- a/apps/web/lib/workbench/useClarify.ts +++ b/apps/web/lib/workbench/useClarify.ts @@ -3,10 +3,7 @@ import { useCallback, useState } from "react"; import { api } from "@/lib/api/client"; -import type { ClarifyDecisionVM } from "./clarify"; - -// 澄清预检结论为「放行」(直接去 refine,不反问)——后端 error/前端异常时的确定性回退。 -const PROCEED: ClarifyDecisionVM = { needClarification: false, questions: [] }; +import { PROCEED, toVM, type ClarifyDecisionVM } from "./clarify"; export type ClarifyStatus = "idle" | "checking" | "asking" | "proceed" | "error"; @@ -24,30 +21,6 @@ export interface UseClarify { reset: () => void; } -// 后端 snake_case ClarifyDecision → 前端 camelCase VM(不信任外部数据,全部显式取值 + 默认)。 -function toVM(data: { - need_clarification?: boolean; - questions?: { - question?: string; - options?: { label?: string; value?: string }[]; - allow_free_text?: boolean; - }[]; - verification?: string | null; -}): ClarifyDecisionVM { - return { - needClarification: Boolean(data.need_clarification), - questions: (data.questions ?? []).map((q) => ({ - question: q.question ?? "", - options: (q.options ?? []).map((o) => ({ - label: o.label ?? "", - value: o.value ?? "", - })), - allowFreeText: Boolean(q.allow_free_text), - })), - verification: data.verification ?? undefined, - }; -} - // AI 反问澄清预检(WFW-9 M1,路线A 两阶段之「问题」阶段)。 // 「再沟通」意见含糊/极短时先调独立非流式端点让 AI 反问;needClarification=false 或失败=放行去既有 refine。 export function useClarify(): UseClarify {