question-cards-content.ts 2.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374
  1. /**
  2. * 问卷内容(`<question-cards>…</question-cards>` 行)的**解析**——纯函数,可被 harness 直接测。
  3. *
  4. * 为什么单独抽出来(2026-09-24):渲染层交给 `QuestionCard` 的 `content` 是
  5. * **带标记的整段**(切行器 `StreamXMLFilter` 给 block 行产出的就是 `openTag + inner + closeTag`,
  6. * 见 `handleBehavior`)。而组件的两个解析函数一个剥标记、一个不剥 ——
  7. * 于是"作答后写回 `{answers:…}`"时只读分支解析失败、交互分支也没有 `questions`,
  8. * **整张卡片渲染成空**(用户报「提交完问卷直接看不见了」)。
  9. * 现在两处都走这里的同一个解包逻辑,并且 harness 会用**真实切行器**跑一遍往返。
  10. */
  11. export interface QuestionOption {
  12. label: string;
  13. description?: string;
  14. recommended?: boolean;
  15. [key: string]: unknown;
  16. }
  17. export interface Question {
  18. header: string;
  19. question: string;
  20. multiSelect?: boolean;
  21. allowFreeformInput?: boolean;
  22. message?: string;
  23. /** 输入框常驻并排在最上(用于补充信息),默认 false 保持原有"其他"行为 */
  24. freeformInputOnTop?: boolean;
  25. /** 上述常驻输入框的 placeholder */
  26. freeformPlaceholder?: string;
  27. options?: QuestionOption[];
  28. }
  29. /** 问卷答案(`QuestionCard` 提交时给出的形状,也是只读渲染读的形状) */
  30. export interface QuestionCardAnswer {
  31. header?: string;
  32. selected?: string[];
  33. freeText?: string;
  34. skipped?: boolean;
  35. }
  36. const MARKER_RE = /<question-cards\b[^>]*>([\s\S]*?)<\/question-cards>/i;
  37. /**
  38. * 取出标记内部的 JSON 文本。
  39. *
  40. * - 内容带 `<question-cards>` 标记 → 取标记内部(两处解析都靠它)
  41. * - 不带标记 → 原样返回(兼容直接传 JSON 的调用方)
  42. */
  43. export const unwrapQuestionCardsContent = (content: string): string => {
  44. const text = String(content ?? "").trim();
  45. const match = text.match(MARKER_RE);
  46. return (match ? match[1] : text).trim();
  47. };
  48. const safeParse = (text: string): any => {
  49. try {
  50. return JSON.parse(text);
  51. } catch {
  52. return null;
  53. }
  54. };
  55. /** 解析问卷题目(交互态渲染用);解析不出来返回空数组 */
  56. export const parseQuestionsFromContent = (content: string): Question[] => {
  57. const parsed = safeParse(unwrapQuestionCardsContent(content));
  58. const questions = parsed?.questions;
  59. return Array.isArray(questions) ? (questions as Question[]) : [];
  60. };
  61. /** 解析已作答结果(只读态渲染用);没有答案返回 null */
  62. export const parseAnswersFromContent = (content: string): QuestionCardAnswer[] | null => {
  63. const parsed = safeParse(unwrapQuestionCardsContent(content));
  64. const answers = parsed?.answers;
  65. return Array.isArray(answers) ? (answers as QuestionCardAnswer[]) : null;
  66. };