10 小时以前 35722562e9e13f0504acc15b740d042ecb810199
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
/**
 * AI 组件草稿 → 画布数据(语义层的逆运算)
 * <p>
 * 与 use-designer 的 collectSchema 构成往返:collectSchema 从画布收集 {type, props},
 * 本模块把 {type, props} 装回画布。两者必须逐项对齐,否则「AI 生成 → 保存 → 再打开」
 * 会掉组件或错位属性。
 * <p>
 * 刻意放在 core/ 而不是设计器里:它不需要 editor 实例,是纯函数,可单测,
 * 也避免 components/ 反向依赖 #/api。
 */
import type { QualityComponentDefinition, QualityProps } from './types';
 
import { buildQualityContent } from './factory';
import { getQualityComponent } from './registry';
import { validateQualityProps } from './validator';
 
/**
 * AI 草稿里的一个组件。
 * <p>
 * 结构完全扁平:没有 children。props 的 key 由前端积木清单定义,
 * 所以这里是宽松的 Record 而不是强类型——合法 key 的权威在注册表。
 */
export interface QualityDraftComponent {
  type?: string;
  props?: Record<string, unknown>;
}
 
/** 被跳过的草稿组件 */
export interface QualityAssemblySkip {
  type: string;
  /** 展示名;未注册的 type 没有展示名,回退为 type 本身 */
  label: string;
  reason: string;
}
 
export interface QualityAssemblyResult {
  /** 可直接交给 loadProjectData 的 GrapesJS 项目数据 */
  grapes: Record<string, unknown>;
  /** 识别到了但装不进画布的组件 */
  skipped: QualityAssemblySkip[];
  /** 装进去了、但属性不完整或不合法的组件,提示用 */
  problems: string[];
}
 
/**
 * 把草稿组件装配成画布数据。
 * <p>
 * 单个组件坏掉不影响其余:未知类型进 skipped,属性有问题的进 problems,
 * 都不会让整批失败——AI 的产物本来就不可信,全有或全无只会让用户白等一次识别。
 */
export function assembleQualityCanvas(
  components?: QualityDraftComponent[],
): QualityAssemblyResult {
  const skipped: QualityAssemblySkip[] = [];
  const problems: string[] = [];
  const nodes: Record<string, unknown>[] = [];
 
  (components ?? []).forEach((draft, index) => {
    const position = `第 ${index + 1} 个组件`;
    const type = draft?.type ?? '';
    const definition = getQualityComponent(type);
    if (!definition) {
      skipped.push({
        type,
        label: type || '未命名组件',
        reason: `${position}:类型「${type || '空'}」不在当前设计器的组件清单内,已跳过`,
      });
      return;
    }
 
    const normalized = normalizeDraftProps(definition, draft?.props);
    normalized.problems.forEach((message) => problems.push(`${position}:${message}`));
 
    // buildQualityContent 做的正是往返所需的四件事:defaults 打底、buildContent 生成结构、
    // propsToAttributes 只输出声明过的 key 并字符串化、写入 data-quality-type。
    const content = buildQualityContent(definition, normalized.props) as Record<string, unknown>;
    // traits 不进画布数据:载入后由 restoreQualityTraits 按 data-quality-type 重建,
    // 与「打开一份已保存的版本」走的是同一条路(traits 本来也不参与项目数据序列化)。
    delete content.traits;
    nodes.push(content);
 
    validateQualityProps(definition, normalized.props).forEach((message) =>
      problems.push(`${position}:${message}`),
    );
  });
 
  return {
    // pages 必须非空,否则 loadSchema 会走空白页兜底分支,装配好的组件全部丢掉
    grapes: {
      pages: [{ component: { style: { padding: '0' }, components: nodes } }],
    },
    skipped,
    problems,
  };
}
 
/**
 * 把 AI 给的属性收敛成语义层能承载的原始值。
 * <p>
 * 只管一件事:值必须是 boolean / number / string。模型偶尔会把字段写成嵌套对象或数组
 * (比如把检验项数据行整段塞进 itemsPath),这类值经 coerceFieldValue 的 String() 会变成
 * "[object Object]" 写进画布,属性面板里显示一串乱码,还会静默盖掉默认值——所以在这里拦掉并说明。
 * <p>
 * 不检查 key 是否声明过:未声明的 key 由 propsToAttributes 丢弃,本来也进不了画布。
 */
function normalizeDraftProps(
  definition: QualityComponentDefinition,
  props?: Record<string, unknown>,
): { props: QualityProps; problems: string[] } {
  const normalized: QualityProps = {};
  const problems: string[] = [];
  if (!props) {
    return { props: normalized, problems };
  }
  definition.propertySchema.forEach((field) => {
    const value = props[field.key];
    // null 与 undefined 同义:模型没给值,交给 defaults 兜底
    if (value === undefined || value === null) {
      return;
    }
    if (
      typeof value === 'boolean' ||
      typeof value === 'number' ||
      typeof value === 'string'
    ) {
      normalized[field.key] = value;
      return;
    }
    problems.push(
      `${definition.label}的「${field.label}」收到的是结构化数据,语义层只承载单个值,已按默认值处理`,
    );
  });
  return { props: normalized, problems };
}