/** * 问卷内容(`…` 行)的**解析**——纯函数,可被 harness 直接测。 * * 为什么单独抽出来(2026-09-24):渲染层交给 `QuestionCard` 的 `content` 是 * **带标记的整段**(切行器 `StreamXMLFilter` 给 block 行产出的就是 `openTag + inner + closeTag`, * 见 `handleBehavior`)。而组件的两个解析函数一个剥标记、一个不剥 —— * 于是"作答后写回 `{answers:…}`"时只读分支解析失败、交互分支也没有 `questions`, * **整张卡片渲染成空**(用户报「提交完问卷直接看不见了」)。 * 现在两处都走这里的同一个解包逻辑,并且 harness 会用**真实切行器**跑一遍往返。 */ export interface QuestionOption { label: string; description?: string; recommended?: boolean; [key: string]: unknown; } export interface Question { header: string; question: string; multiSelect?: boolean; allowFreeformInput?: boolean; message?: string; /** 输入框常驻并排在最上(用于补充信息),默认 false 保持原有"其他"行为 */ freeformInputOnTop?: boolean; /** 上述常驻输入框的 placeholder */ freeformPlaceholder?: string; options?: QuestionOption[]; } /** 问卷答案(`QuestionCard` 提交时给出的形状,也是只读渲染读的形状) */ export interface QuestionCardAnswer { header?: string; selected?: string[]; freeText?: string; skipped?: boolean; } const MARKER_RE = /]*>([\s\S]*?)<\/question-cards>/i; /** * 取出标记内部的 JSON 文本。 * * - 内容带 `` 标记 → 取标记内部(两处解析都靠它) * - 不带标记 → 原样返回(兼容直接传 JSON 的调用方) */ export const unwrapQuestionCardsContent = (content: string): string => { const text = String(content ?? "").trim(); const match = text.match(MARKER_RE); return (match ? match[1] : text).trim(); }; const safeParse = (text: string): any => { try { return JSON.parse(text); } catch { return null; } }; /** 解析问卷题目(交互态渲染用);解析不出来返回空数组 */ export const parseQuestionsFromContent = (content: string): Question[] => { const parsed = safeParse(unwrapQuestionCardsContent(content)); const questions = parsed?.questions; return Array.isArray(questions) ? (questions as Question[]) : []; }; /** 解析已作答结果(只读态渲染用);没有答案返回 null */ export const parseAnswersFromContent = (content: string): QuestionCardAnswer[] | null => { const parsed = safeParse(unwrapQuestionCardsContent(content)); const answers = parsed?.answers; return Array.isArray(answers) ? (answers as QuestionCardAnswer[]) : null; };