@webskill/sdk 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/agent.d.ts +3 -3
  2. package/dist/agent.js +2442 -3
  3. package/dist/{dist-Bfga1TmS.js → approval-BnLLCOG0.js} +235 -2932
  4. package/dist/browser.d.ts +106 -5
  5. package/dist/browser.js +364 -10
  6. package/dist/{openUiLibrary-BKXW7Iwx-DjvEnMlJ.js → dist-CTsxblSD.js} +496 -28
  7. package/dist/{echarts-De78wXqV.js → echarts-BE7oV_Dl.js} +1 -1
  8. package/dist/{env-8cY40DXB-CGnEVZby.js → env-Bj1MI2Ww.js} +1 -1
  9. package/dist/errors-BDZNpC13.js +22 -0
  10. package/dist/{eventTypes-FllCrX-Z-DNDeHWoG.js → eventTypes-Y07N8IAC.js} +14 -2
  11. package/dist/external-_ZRQe-V9.js +53 -0
  12. package/dist/governance.d.ts +15 -3
  13. package/dist/governance.js +86 -4
  14. package/dist/{index-DQwGvHAI.d.ts → index-CQYo4tcT.d.ts} +85 -6
  15. package/dist/{index-sd-gVKey.d.ts → index-CU2md8R1.d.ts} +26 -18
  16. package/dist/{index-CWoKOFBP.d.ts → index-Oc3H89TJ.d.ts} +159 -3
  17. package/dist/index.d.ts +4 -4
  18. package/dist/index.js +1001 -5
  19. package/dist/kind-DxgS8LM-.js +77 -0
  20. package/dist/linkedDocument-xNXF-z8n.js +94 -0
  21. package/dist/llm-B7lLH0ZI.js +1767 -0
  22. package/dist/mcp.d.ts +2 -2
  23. package/dist/mcp.js +41 -4
  24. package/dist/memoryArtifactStore-D6dLeY55.js +48 -0
  25. package/dist/miniChart-owdCdaw-.js +533 -0
  26. package/dist/node.d.ts +3 -3
  27. package/dist/node.js +106 -9
  28. package/dist/openUiLibrary-CY_ADoe0.js +30 -0
  29. package/dist/openUiSpecLang-DuOCdw9G.js +1849 -0
  30. package/dist/pathSecurity-B1owvJAF.js +65 -0
  31. package/dist/processSandboxEntry.js +5 -1
  32. package/dist/renderResult-D9Q-Vu2x.js +143 -0
  33. package/dist/{rolldown-runtime-BOF7iYI8.js → rolldown-runtime-BHkdefai.js} +1 -2
  34. package/dist/sandboxWorkerEntry.js +5 -1
  35. package/dist/{dist-D5YhlCdY.js → skill-CAJMsLod.js} +44 -372
  36. package/dist/{skillVersionStore-D-qHk9ZE-C0OFDTQY.d.ts → skillVersionStore-B24Jjjry-C4zomhMZ.d.ts} +14 -2
  37. package/dist/surface-DVGiCmwq.js +145 -0
  38. package/dist/testing.d.ts +1 -1
  39. package/dist/testing.js +64 -3
  40. package/dist/{types-C3V6lAeO-BgeOvc1Y.d.ts → types-C3V6lAeO-BIf7UB2e.d.ts} +17 -1
  41. package/dist/types-TUydnxaj.js +78 -0
  42. package/dist/ui-react.d.ts +12 -5
  43. package/dist/ui-react.js +128548 -4729
  44. package/dist/ui-vue.d.ts +1 -1
  45. package/dist/ui-vue.js +11 -2
  46. package/dist/ui.d.ts +3 -3
  47. package/dist/ui.js +1254 -3
  48. package/dist/urlSafety-CiSuCJvX.js +65 -0
  49. package/dist/webSkillApi-DOOyz2G5.js +106 -0
  50. package/dist/{webskillLitCatalog-D_zCqeQF-swXsWoAe.js → webskillLitCatalog-DSqVFQjK.js} +11 -3
  51. package/package.json +1 -1
  52. package/dist/catalogComponents-BgAJN0p8-Cr55ouOg.js +0 -124384
  53. package/dist/client-BCM6Z3yq-qUQNkKrK.js +0 -7787
  54. package/dist/dist-BnqAkSS6.js +0 -2384
  55. package/dist/dist-CiaDIqkV.js +0 -3509
  56. package/dist/dist-DxyxmXPU-DGC-dpXf.js +0 -5621
  57. package/dist/memoryArtifactStore-52Zn9npI-BxtUkrgV.js +0 -86
  58. package/dist/stdio-CFMoANJJ-BxrTeXh7.js +0 -31
  59. package/dist/testing-Cm8MseLF.js +0 -142
  60. package/dist/types-WovEf4ED-CZSDiiBU.js +0 -6215
@@ -0,0 +1,1849 @@
1
+ import { u as interactionToFormModel } from "./miniChart-owdCdaw-.js";
2
+ import { toJSONSchema, z } from "zod";
3
+
4
+ //#region ../ui/src/chart/echart.ts
5
+ /** 与 catalog `Chart` 的 props 枚举逐字一致(`ui/catalog/uiCatalog.ts`) */
6
+ const KINDS = /* @__PURE__ */ new Set([
7
+ "bar",
8
+ "line",
9
+ "area",
10
+ "pie",
11
+ "scatter",
12
+ "stacked-bar",
13
+ "dual-axis"
14
+ ]);
15
+ const list = (value) => Array.isArray(value) ? value : [];
16
+ /**
17
+ * catalog `Chart` 节点 props → `ChartSpec`。四档共用这一份转换:
18
+ * 此前 native / OpenUI / json-render / A2UI 各抄了一遍,改一处就会漂移。
19
+ * 非法枚举**原样保留**,由 `toEchartsOption` 给出可见降级提示——
20
+ * 此前偷偷改写成 `'line'`,画出的图与模型的意图无关且不可察觉。
21
+ */
22
+ function chartSpecFromProps(props) {
23
+ const series = list(props["series"]);
24
+ return {
25
+ kind: typeof props["type"] === "string" ? props["type"] : "",
26
+ labels: list(props["labels"]),
27
+ series: series.map((item) => ({
28
+ ...typeof item.name === "string" ? { name: item.name } : {},
29
+ data: list(item.values)
30
+ })),
31
+ ...typeof props["title"] === "string" ? { title: props["title"] } : {}
32
+ };
33
+ }
34
+ const seriesNames = (chart) => chart.series.map((series) => series.name ?? "");
35
+ /** 字号默认值的**全仓唯一来源**;`12` 是 echarts 的全局默认,显式写出后渲染不变 */
36
+ const DEFAULT_CHART_FONT_SIZES = {
37
+ title: 14,
38
+ axisLabel: 12,
39
+ legend: 12
40
+ };
41
+ /** 逐字段兜底:宿主通常只想调大其中一两项 */
42
+ function resolveChartFontSizes(sizes) {
43
+ return {
44
+ ...DEFAULT_CHART_FONT_SIZES,
45
+ ...sizes
46
+ };
47
+ }
48
+ /** echarts 的图例条目来源:饼图按标签分项,其余按系列名 */
49
+ const legendEntries = (chart) => chart.kind === "pie" ? chart.labels : seriesNames(chart);
50
+ /**
51
+ * 非饼图的 series 形状表:catalog 声明的每种类型对应「基础 type + 零到一个修饰」。
52
+ * 饼图不在表内——它的 data 要按 labels 重组,形状与其余六种不同。
53
+ */
54
+ const SERIES_SHAPE = {
55
+ bar: { type: "bar" },
56
+ line: { type: "line" },
57
+ area: {
58
+ type: "line",
59
+ areaStyle: {}
60
+ },
61
+ scatter: { type: "scatter" },
62
+ "stacked-bar": {
63
+ type: "bar",
64
+ stack: "total"
65
+ },
66
+ "dual-axis": { type: "line" }
67
+ };
68
+ const FALLBACK_KIND = "bar";
69
+ const fallbackNotice = (kind) => kind === "" ? `Missing chart type; rendered as a ${FALLBACK_KIND} chart.` : `Unsupported chart type "${kind}"; rendered as a ${FALLBACK_KIND} chart.`;
70
+ /** `ChartSpec` → ECharts option:图表类型、数据系列、图例的唯一定义处 */
71
+ function toEchartsOption(chart, fontSizes) {
72
+ const font = resolveChartFontSizes(fontSizes);
73
+ const known = KINDS.has(chart.kind);
74
+ const shape = SERIES_SHAPE[known ? chart.kind : FALLBACK_KIND];
75
+ const notice = known ? void 0 : fallbackNotice(chart.kind);
76
+ const valueAxis = {
77
+ type: "value",
78
+ axisLabel: { fontSize: font.axisLabel }
79
+ };
80
+ return {
81
+ animation: false,
82
+ title: chart.title || notice ? {
83
+ text: chart.title ?? "",
84
+ left: "center",
85
+ textStyle: { fontSize: font.title },
86
+ ...notice ? { subtext: notice } : {}
87
+ } : void 0,
88
+ tooltip: { trigger: chart.kind === "pie" ? "item" : "axis" },
89
+ legend: {
90
+ bottom: 0,
91
+ textStyle: { fontSize: font.legend }
92
+ },
93
+ xAxis: chart.kind === "pie" ? void 0 : {
94
+ type: "category",
95
+ data: chart.labels,
96
+ axisLabel: { fontSize: font.axisLabel }
97
+ },
98
+ yAxis: chart.kind === "pie" ? void 0 : chart.kind === "dual-axis" ? [valueAxis, { ...valueAxis }] : valueAxis,
99
+ series: chart.kind === "pie" ? chart.series.map((series) => ({
100
+ type: "pie",
101
+ name: series.name,
102
+ data: chart.labels.map((label, index) => ({
103
+ name: label,
104
+ value: series.data[index] ?? 0
105
+ }))
106
+ })) : chart.series.map((series, index) => ({
107
+ ...shape,
108
+ name: series.name,
109
+ data: series.data,
110
+ ...chart.kind === "dual-axis" ? { yAxisIndex: index === 0 ? 0 : 1 } : {}
111
+ }))
112
+ };
113
+ }
114
+ /**
115
+ * 图表的 DOM 契约。echarts 画在 canvas 上,DOM 里读不到图表类型/系列/图例,
116
+ * 「同一份 ChartSpec 在各档下一致」只能靠宿主元素上的这三个属性对拍。
117
+ */
118
+ function applyChartDomContract(container, chart) {
119
+ container.setAttribute("data-webskill-chart", chart.kind);
120
+ container.setAttribute("data-webskill-chart-series", seriesNames(chart).join("|"));
121
+ container.setAttribute("data-webskill-chart-legend", legendEntries(chart).join("|"));
122
+ if (chart.title) container.setAttribute("aria-label", chart.title);
123
+ else container.removeAttribute("aria-label");
124
+ }
125
+ /**
126
+ * 一篇文档常同时挂载多张图表;共用同一个模块 promise,
127
+ * 避免每个挂载点各自发起一次动态 import。
128
+ */
129
+ let echartsModule;
130
+ const loadEcharts = () => echartsModule ??= import("./echarts-BE7oV_Dl.js");
131
+ /**
132
+ * 框架无关的 echarts 挂载点:三档的 React 组件与 A2UI 档的 Lit 元素共用同一实现,
133
+ * 图表不再因档位而异。echarts 走动态 import,不打进首屏。
134
+ */
135
+ function mountEchart(container) {
136
+ let disposed = false;
137
+ let instance;
138
+ let queued;
139
+ let observer;
140
+ const apply = (chart, fontSizes) => {
141
+ instance?.setOption(toEchartsOption(chart, fontSizes), {
142
+ notMerge: true,
143
+ lazyUpdate: true
144
+ });
145
+ };
146
+ loadEcharts().then((echarts) => {
147
+ if (disposed) return;
148
+ instance = echarts.init(container);
149
+ if (typeof ResizeObserver !== "undefined") {
150
+ observer = new ResizeObserver(() => instance?.resize());
151
+ observer.observe(container);
152
+ }
153
+ if (queued) apply(queued.chart, queued.fontSizes);
154
+ });
155
+ return {
156
+ setChart(chart, fontSizes) {
157
+ applyChartDomContract(container, chart);
158
+ queued = {
159
+ chart,
160
+ ...fontSizes ? { fontSizes } : {}
161
+ };
162
+ apply(chart, fontSizes);
163
+ },
164
+ dispose() {
165
+ disposed = true;
166
+ observer?.disconnect();
167
+ observer = void 0;
168
+ instance?.dispose();
169
+ instance = void 0;
170
+ }
171
+ };
172
+ }
173
+
174
+ //#endregion
175
+ //#region ../ui/src/catalog/gauge.ts
176
+ /**
177
+ * `Gauge` 的百分比换算(0.11.0 分册 19 FR-19.2)。
178
+ *
179
+ * catalog 的 React `SpecGauge` 与文档面的预置组件都读这一份:
180
+ * 换算里有两个容易各写各的判断——`max === min` 时不能除零、越界值要夹紧——
181
+ * 两处各写一遍就会在边界值上给出不同的条。
182
+ */
183
+ function gaugePercent(value, min, max) {
184
+ const span = max - min;
185
+ if (span <= 0) return 0;
186
+ return Math.max(0, Math.min(100, (value - min) / span * 100));
187
+ }
188
+
189
+ //#endregion
190
+ //#region ../ui/src/catalog/budget.ts
191
+ /**
192
+ * catalog 请求体积上限(0.11.0 分册 14 / AC-G20)的**数字单一来源**。
193
+ *
194
+ * 度量与报错格式在 `scripts/catalogBudget.mjs`(`check:contracts` 与 vitest 共用);
195
+ * 那边不重复这些数字,一律从本模块读——两处各写一份阈值就等于没有阈值。
196
+ *
197
+ * catalog 进入每一次 LLM 请求的有两处:系统提示词 `toPrompt()` 与
198
+ * `render_ui` 入参 `toJsonSchema()`。只量前者会漏掉约 2/3 的成本。
199
+ *
200
+ * **超阈值的处置是砍组件或收窄枚举取值,不是抬高这里的数字。**
201
+ */
202
+ /**
203
+ * 预算阶段。0.11.0 分册 12 是那一版最后一处 catalog 增量,故切到 `'final'`:
204
+ * 阈值收紧到实测终态 + 余量,且余量小于一个平均组件的成本
205
+ * ——余量大于一个组件,等于默许下一版无声塞进一个。
206
+ * 0.15.0 分册 13 新增 `OpenDocument` 后仍是 `'final'`,只是实测基线换了一份。
207
+ */
208
+ const CATALOG_BUDGET_STAGE = "final";
209
+ /**
210
+ * 阈值 = **已落地实测** + 余量,余量须小于一个平均组件的成本。
211
+ *
212
+ * 2026-08-26 实测终态(0.15.0 分册 13 落地后):prompt 13 372 / schema 22 184,31 个组件。
213
+ * 平均组件成本 prompt 431 / schema 715。
214
+ * 当前余量 prompt 278 / schema 316,均小于一个平均组件——再加一个组件必红。
215
+ *
216
+ * ⚠️ **这两个数字被上调过三次**,三次都由需求方裁决并改写了需求文档,
217
+ * 不是实现阶段自行放宽:
218
+ *
219
+ * 1. 11 200 → 12 200 / 20 500 → 21 200(0.11.0 分册 29 §2 第 7 条):分册 13 组件描述由短改长。
220
+ * `example` 是既有护栏的硬要求(`catalog.test.ts` 与 `catalogMatrix.test.tsx`
221
+ * 逐个组件校验并四档渲染),靠省掉示例腾出的空间收不回来。
222
+ * 2. 12 200 → 13 000 / 21 200 → 21 800(0.11.0 分册 29 §2.2):分册 12 的 `Split` 实测 490 + 694,
223
+ * 超出计划增量(403 + 1 047)的 prompt 侧。需求方裁定
224
+ * 「描述要长,可用性大于预算阈值」——收紧措辞可省回 158 字节,
225
+ * 但砍的正是各组件「何时改用 X」的辨析句,即选型准确率本身。
226
+ * 3. 13 000 → 13 650 / 21 800 → 22 500(0.15.0 分册 13 §3,2026-08-26 裁决):
227
+ * 新增内置组件 `OpenDocument`,实测 prompt +372 / schema +384。
228
+ * 该组件是本册消灭「站点技能在扩展里必然落到红框」的唯一手段,
229
+ * 砍它等于砍掉整册需求;改用「catalog 里有些组件不广告」则引入一个新概念,
230
+ * 模型看不见的组件与不存在的组件在它眼里没区别。需求方裁定取组件可用性、上调阈值,
231
+ * 幅度以落地后实测为准、不得预留宽裕。
232
+ *
233
+ * 三次上调合计把单请求成本从 21.7 KB 抬到约 35.3 KB。
234
+ * **AC-G20 原文「超了砍组件、不抬阈值」在这三处被推翻,每次都限于当版;
235
+ * 后续再撞顶仍须回到需求方,本版同样不得成为第四次上调的先例。**
236
+ */
237
+ const CATALOG_PROMPT_MAX = 13650;
238
+ const CATALOG_SCHEMA_MAX = 22500;
239
+ /**
240
+ * **尚未落地**的计划增量。0.15.0 分册 13 已落地,故仍归零;
241
+ * `CATALOG_BUDGET_STAGE` 保持 `'final'`,余量改由「小于一个平均组件」判定。
242
+ */
243
+ const PLANNED_INCREMENT = {
244
+ prompt: 0,
245
+ schema: 0
246
+ };
247
+
248
+ //#endregion
249
+ //#region ../ui/src/catalog/degradationCode.ts
250
+ /** 码与人话之间的分隔符。放在消息最前面,`codeOfIssue` 才能从 zod 的 issue 里认出它。 */
251
+ const SEPARATOR = ": ";
252
+ /**
253
+ * 给 zod 的 `error` 文案打上稳定码。
254
+ *
255
+ * 组件自己在 schema 里声明,sanitize 只负责把码搬到 `UiSpecDegradation.code` 上——
256
+ * 这样新增一条策略性约束不需要去 sanitize 里加组件名分支(写死清单是本仓反复出问题的地方)。
257
+ */
258
+ function codedError(code, message) {
259
+ return `${code}${SEPARATOR}${message}`;
260
+ }
261
+ /** 从 zod issue 的消息里取回码;没有码就返回 undefined(普通类型错误就是这种)。 */
262
+ function codeOfIssue(message) {
263
+ const index = message.indexOf(SEPARATOR);
264
+ if (index <= 0) return void 0;
265
+ const head = message.slice(0, index);
266
+ return /^[A-Z][A-Z0-9_]*$/.test(head) ? head : void 0;
267
+ }
268
+
269
+ //#endregion
270
+ //#region ../ui/src/catalog/sanitize.ts
271
+ function isRecord$2(value) {
272
+ return typeof value === "object" && value !== null && !Array.isArray(value);
273
+ }
274
+ /**
275
+ * 逐节点降级(FR-23.3)。
276
+ *
277
+ * 与 `validate` 的关系见 `UiCatalog.sanitize` 的注释:一个整块拒绝、一个逐节点救。
278
+ *
279
+ * **不写回 `safeParse` 的结果**是刻意的:zod 会剥离未声明的键,
280
+ * 而渲染层确实在读若干未进 zod 的 prop(如 `suggestion`,由 SDK 侧下发而非模型生成)。
281
+ * 写回等于把它们悄悄删掉。这里只删「确实解析失败」的那几个键。
282
+ */
283
+ function sanitizeNode(catalog, byName, node, path, degradations) {
284
+ if (!isRecord$2(node)) {
285
+ degradations.push({
286
+ path,
287
+ kind: "node-dropped",
288
+ component: "unknown",
289
+ detail: "Node is not an object."
290
+ });
291
+ return;
292
+ }
293
+ const name = node["component"];
294
+ if (typeof name !== "string") {
295
+ degradations.push({
296
+ path,
297
+ kind: "node-dropped",
298
+ component: "unknown",
299
+ detail: "Node has no \"component\" name."
300
+ });
301
+ return;
302
+ }
303
+ const def = byName.get(name);
304
+ if (!def) {
305
+ degradations.push({
306
+ path,
307
+ kind: "unknown-component",
308
+ component: name,
309
+ detail: `Unknown component "${name}"; the catalog declares: ${catalog.components.map((c) => c.name).join(", ")}`
310
+ });
311
+ return;
312
+ }
313
+ const rawProps = isRecord$2(node["props"]) ? node["props"] : {};
314
+ let props = rawProps;
315
+ const parsed = def.props.safeParse(rawProps);
316
+ if (!parsed.success) {
317
+ const offending = new Set(parsed.error.issues.map((issue) => String(issue.path[0] ?? "")).filter((key) => key !== ""));
318
+ const codes = /* @__PURE__ */ new Map();
319
+ for (const issue of parsed.error.issues) {
320
+ const key = String(issue.path[0] ?? "");
321
+ const code = codeOfIssue(issue.message);
322
+ if (key !== "" && code !== void 0) codes.set(key, code);
323
+ }
324
+ const kept = {};
325
+ for (const [key, value] of Object.entries(rawProps)) if (!offending.has(key)) kept[key] = value;
326
+ const retry = def.props.safeParse(kept);
327
+ if (!retry.success) {
328
+ const carried = [...codes.values()].find((code) => code !== void 0);
329
+ degradations.push({
330
+ path,
331
+ kind: "node-dropped",
332
+ component: name,
333
+ detail: `Props cannot be repaired by dropping invalid keys: ${retry.error.issues.map((i) => i.message).join("; ")}`,
334
+ ...carried === void 0 ? {} : { code: carried }
335
+ });
336
+ return;
337
+ }
338
+ for (const key of offending) {
339
+ const code = codes.get(key);
340
+ degradations.push({
341
+ path: `${path}.props.${key}`,
342
+ kind: "prop-dropped",
343
+ component: name,
344
+ detail: `Dropped invalid prop "${key}".`,
345
+ ...code === void 0 ? {} : { code }
346
+ });
347
+ }
348
+ props = kept;
349
+ }
350
+ const sanitized = {
351
+ component: name,
352
+ props
353
+ };
354
+ if (typeof node["id"] === "string") sanitized.id = node["id"];
355
+ const children = node["children"];
356
+ if (children === void 0) return sanitized;
357
+ if (!Array.isArray(children)) {
358
+ degradations.push({
359
+ path: `${path}.children`,
360
+ kind: "constraint-ignored",
361
+ component: name,
362
+ detail: "children is not an array; rendered without children."
363
+ });
364
+ return sanitized;
365
+ }
366
+ if (def.children === void 0) {
367
+ degradations.push({
368
+ path: `${path}.children`,
369
+ kind: "constraint-ignored",
370
+ component: name,
371
+ detail: `Component "${name}" is a leaf; its children were dropped.`
372
+ });
373
+ return sanitized;
374
+ }
375
+ const allowed = def.children;
376
+ const singletons = /* @__PURE__ */ new Map();
377
+ const kept = [];
378
+ children.forEach((child, index) => {
379
+ const childPath = `${path}.children[${index}]`;
380
+ const childName = isRecord$2(child) && typeof child["component"] === "string" ? child["component"] : void 0;
381
+ if (allowed !== "any" && childName !== void 0 && !allowed.includes(childName)) {
382
+ degradations.push({
383
+ path: childPath,
384
+ kind: byName.has(childName) ? "node-dropped" : "unknown-component",
385
+ component: childName,
386
+ detail: byName.has(childName) ? `Component "${name}" only accepts children: ${[...allowed].join(", ")}` : `Unknown component "${childName}"; the catalog declares: ${catalog.components.map((c) => c.name).join(", ")}`
387
+ });
388
+ return;
389
+ }
390
+ if (childName !== void 0 && byName.get(childName)?.singletonPerContainer === true) {
391
+ const seen = (singletons.get(childName) ?? 0) + 1;
392
+ singletons.set(childName, seen);
393
+ if (seen > 1) {
394
+ degradations.push({
395
+ path: childPath,
396
+ kind: "node-dropped",
397
+ component: childName,
398
+ detail: `At most one "${childName}" per container; the extra one was dropped.`
399
+ });
400
+ return;
401
+ }
402
+ }
403
+ const sanitizedChild = sanitizeNode(catalog, byName, child, childPath, degradations);
404
+ if (sanitizedChild) kept.push(sanitizedChild);
405
+ });
406
+ sanitized.children = kept;
407
+ return sanitized;
408
+ }
409
+ function sanitizeUiSpec(catalog, byName, spec) {
410
+ const degradations = [];
411
+ return {
412
+ node: sanitizeNode(catalog, byName, spec, "root", degradations),
413
+ degradations
414
+ };
415
+ }
416
+
417
+ //#endregion
418
+ //#region ../ui/src/catalog/defineCatalog.ts
419
+ function componentSchema(def, rootDefs) {
420
+ const props = liftDefs(toJSONSchema(def.props, { io: "input" }), def.name, rootDefs);
421
+ const node = {
422
+ type: "object",
423
+ description: def.description,
424
+ properties: {
425
+ component: {
426
+ type: "string",
427
+ const: def.name
428
+ },
429
+ id: {
430
+ type: "string",
431
+ description: "Stable node id, reused on replay"
432
+ },
433
+ props
434
+ },
435
+ required: ["component"]
436
+ };
437
+ if (def.children !== void 0) node.properties = {
438
+ ...node.properties,
439
+ children: {
440
+ type: "array",
441
+ description: def.children === "any" ? "Any catalog component" : `One of: ${[...def.children].join(", ")}`,
442
+ items: { $ref: "#" }
443
+ }
444
+ };
445
+ return node;
446
+ }
447
+ function isRecord$1(value) {
448
+ return typeof value === "object" && value !== null && !Array.isArray(value);
449
+ }
450
+ const DEFS_REF_PREFIX = "#/$defs/";
451
+ function rewriteDefRefs(node, renames) {
452
+ if (Array.isArray(node)) return node.map((child) => rewriteDefRefs(child, renames));
453
+ if (!isRecord$1(node)) return node;
454
+ const out = {};
455
+ for (const [key, value] of Object.entries(node)) {
456
+ if (key === "$ref" && typeof value === "string" && value.startsWith(DEFS_REF_PREFIX)) {
457
+ const [head, ...tail] = value.slice(8).split("/");
458
+ const mapped = head === void 0 ? void 0 : renames.get(head);
459
+ out[key] = mapped === void 0 ? value : [`${DEFS_REF_PREFIX}${mapped}`, ...tail].join("/");
460
+ continue;
461
+ }
462
+ out[key] = rewriteDefRefs(value, renames);
463
+ }
464
+ return out;
465
+ }
466
+ /**
467
+ * zod 每个组件各自产出 `$defs.__schemaN`,其 `#/$defs/...` 指的是那次转换自己的根;
468
+ * 直接嵌进 catalog 大 schema 后那个 `#` 变成了 catalog 根,引用从产物根解析不到。
469
+ * 提到根并以组件名限定,避免不同组件的同名定义互相覆盖。
470
+ */
471
+ function liftDefs(schema, owner, rootDefs) {
472
+ const defs = schema["$defs"];
473
+ if (!isRecord$1(defs)) return schema;
474
+ const renames = /* @__PURE__ */ new Map();
475
+ for (const name of Object.keys(defs)) renames.set(name, `${owner}_${name}`);
476
+ for (const [name, def] of Object.entries(defs)) rootDefs[renames.get(name)] = rewriteDefRefs(def, renames);
477
+ const body = {};
478
+ for (const [key, value] of Object.entries(schema)) if (key !== "$defs") body[key] = value;
479
+ return rewriteDefRefs(body, renames);
480
+ }
481
+ function typeOf(schema) {
482
+ if (Array.isArray(schema.enum)) return schema.enum.map((option) => JSON.stringify(option)).join(" | ");
483
+ if (schema["const"] !== void 0) return JSON.stringify(schema["const"]);
484
+ const union = schema["anyOf"] ?? schema["oneOf"];
485
+ if (Array.isArray(union)) return union.map(typeOf).join(" | ");
486
+ if (schema.type === "array" && schema.items) return `${typeOf(schema.items)}[]`;
487
+ return typeof schema.type === "string" ? schema.type : "unknown";
488
+ }
489
+ function describeProps(def) {
490
+ const schema = toJSONSchema(def.props, { io: "input" });
491
+ const properties = schema.properties ?? {};
492
+ const required = new Set(schema.required ?? []);
493
+ const entries = Object.entries(properties).map(([key, value]) => `${key}${required.has(key) ? "" : "?"}: ${typeOf(value)}`);
494
+ return entries.length === 0 ? "—" : entries.join(", ");
495
+ }
496
+ function promptFor(catalog, options) {
497
+ const lines = [(options.mode === "tool-description" ? "" : `# ${catalog.title} (${catalog.version})\n\n`) + catalog.instructions.trim(), ""];
498
+ const groups = /* @__PURE__ */ new Map();
499
+ for (const def of catalog.components) groups.set(def.group, [...groups.get(def.group) ?? [], def]);
500
+ for (const [group, defs] of groups) {
501
+ lines.push(`## ${group}`, "");
502
+ for (const def of defs) {
503
+ lines.push(`### ${def.name}`, def.description, "", `props: ${describeProps(def)}`);
504
+ if (def.children !== void 0) lines.push(`children: ${def.children === "any" ? "any component" : [...def.children].join(" | ")}`);
505
+ for (const constraint of def.constraints ?? []) lines.push(`- ${constraint}`);
506
+ if (def.example) lines.push("", "```json", JSON.stringify(def.example), "```");
507
+ lines.push("");
508
+ }
509
+ }
510
+ if (catalog.actions.length > 0) {
511
+ lines.push("## actions", "");
512
+ for (const action of catalog.actions) lines.push(`- \`${action.name}\` — ${action.description}`);
513
+ lines.push("");
514
+ }
515
+ return lines.join("\n").trimEnd();
516
+ }
517
+ function validateNode(catalog, byName, node, path, issues) {
518
+ if (!isRecord$1(node)) {
519
+ issues.push({
520
+ path,
521
+ message: "Node must be an object"
522
+ });
523
+ return;
524
+ }
525
+ const name = node["component"];
526
+ if (typeof name !== "string") {
527
+ issues.push({
528
+ path,
529
+ message: "Node is missing a \"component\" name"
530
+ });
531
+ return;
532
+ }
533
+ const def = byName.get(name);
534
+ if (!def) {
535
+ issues.push({
536
+ path,
537
+ message: `Unknown component "${name}"; the catalog declares: ${catalog.components.map((c) => c.name).join(", ")}`
538
+ });
539
+ return;
540
+ }
541
+ const parsed = def.props.safeParse(node["props"] ?? {});
542
+ if (!parsed.success) for (const issue of parsed.error.issues) issues.push({
543
+ path: `${path}.props${issue.path.map((part) => `.${String(part)}`).join("")}`,
544
+ message: issue.message
545
+ });
546
+ const children = node["children"];
547
+ if (children === void 0) return;
548
+ if (!Array.isArray(children)) {
549
+ issues.push({
550
+ path: `${path}.children`,
551
+ message: "children must be an array"
552
+ });
553
+ return;
554
+ }
555
+ if (def.children === void 0) {
556
+ issues.push({
557
+ path: `${path}.children`,
558
+ message: `Component "${name}" is a leaf and cannot have children`
559
+ });
560
+ return;
561
+ }
562
+ const allowed = def.children;
563
+ const singletons = /* @__PURE__ */ new Map();
564
+ children.forEach((child, index) => {
565
+ const childPath = `${path}.children[${index}]`;
566
+ if (allowed !== "any" && isRecord$1(child) && typeof child["component"] === "string") {
567
+ if (!allowed.includes(child["component"])) {
568
+ issues.push({
569
+ path: childPath,
570
+ message: `Component "${name}" only accepts children: ${[...allowed].join(", ")}`
571
+ });
572
+ return;
573
+ }
574
+ }
575
+ if (isRecord$1(child) && typeof child["component"] === "string") {
576
+ const childName = child["component"];
577
+ if (byName.get(childName)?.singletonPerContainer === true) {
578
+ const seen = (singletons.get(childName) ?? 0) + 1;
579
+ singletons.set(childName, seen);
580
+ if (seen > 1) issues.push({
581
+ path: childPath,
582
+ message: `At most one "${childName}" per container; "${name}" already contains one`
583
+ });
584
+ }
585
+ }
586
+ validateNode(catalog, byName, child, childPath, issues);
587
+ });
588
+ }
589
+ /**
590
+ * 同一份定义派生出 JSON Schema / prompt / validator 三种投影,
591
+ * 不允许任何一方手写副本(21 号文档 §1)。
592
+ */
593
+ function defineUiCatalog(input) {
594
+ const byName = new Map(input.components.map((def) => [def.name, def]));
595
+ if (byName.size !== input.components.length) {
596
+ const seen = /* @__PURE__ */ new Set();
597
+ const duplicate = input.components.find((def) => seen.has(def.name) ? true : (seen.add(def.name), false));
598
+ throw new Error(`Duplicate component name "${duplicate?.name}" in UI catalog "${input.id}"`);
599
+ }
600
+ return {
601
+ ...input,
602
+ componentNames: input.components.map((def) => def.name),
603
+ actionNames: input.actions.map((action) => action.name),
604
+ component: (name) => byName.get(name),
605
+ toJsonSchema: () => {
606
+ const $defs = {};
607
+ const oneOf = input.components.map((def) => componentSchema(def, $defs));
608
+ return {
609
+ $schema: "https://json-schema.org/draft/2020-12/schema",
610
+ $id: input.id,
611
+ title: input.title,
612
+ description: input.instructions,
613
+ ...Object.keys($defs).length > 0 ? { $defs } : {},
614
+ oneOf
615
+ };
616
+ },
617
+ toPrompt: (options = {}) => promptFor(input, options),
618
+ validate: (spec) => {
619
+ const issues = [];
620
+ validateNode(input, byName, spec, "root", issues);
621
+ return issues.length === 0 ? { ok: true } : {
622
+ ok: false,
623
+ issues
624
+ };
625
+ },
626
+ sanitize: (spec) => sanitizeUiSpec(input, byName, spec)
627
+ };
628
+ }
629
+
630
+ //#endregion
631
+ //#region ../ui/src/catalog/uiCatalog.ts
632
+ const tone = z.enum([
633
+ "neutral",
634
+ "success",
635
+ "warning",
636
+ "info",
637
+ "destructive"
638
+ ]);
639
+ /**
640
+ * 装饰色。与 `tone` 语义**不重叠**:tone 表状态(success 是「成功」),
641
+ * palette 表美观(「行程第三天」用绿色不代表它成功了)。
642
+ * 取值是色板名不是颜色值——必须映射到 design token,深浅色主题才能自动跟随。
643
+ */
644
+ const palette = z.enum([
645
+ "sky",
646
+ "violet",
647
+ "amber",
648
+ "rose",
649
+ "emerald"
650
+ ]);
651
+ /**
652
+ * surface 级视觉档(FR-13.4)。只在**根节点**上有意义,切的是一组 token
653
+ * (圆角/阴影/底色),不是逐组件改样式;作用域限于该 surface 子树。
654
+ * `office` 是缺省,与本版之前逐字节一致——它是回归基线。
655
+ */
656
+ const surfaceTheme = z.enum([
657
+ "office",
658
+ "vivid",
659
+ "dashboard-dark"
660
+ ]);
661
+ /** 图标白名单:任意名字会让模型产出渲染不出来的节点,且枚举长度直接计入每次请求 */
662
+ const iconName = z.enum([
663
+ "info",
664
+ "check",
665
+ "alert",
666
+ "x",
667
+ "clock",
668
+ "calendar",
669
+ "user",
670
+ "star",
671
+ "search",
672
+ "settings",
673
+ "download",
674
+ "link",
675
+ "chart",
676
+ "file",
677
+ "folder",
678
+ "mail"
679
+ ]);
680
+ /**
681
+ * `Image.src` 的同源约束(AC-13.4)。**判定放在 schema 上,`validate` 与 `sanitize` 两条路径就都覆盖**;
682
+ * 只写进组件描述等于把安全交给模型自觉——提示注入可以用 `src` 把数据带到任意主机上去。
683
+ *
684
+ * 同源按**语法**判:`data:image/` 与相对路径放行,任何带 scheme 的绝对 URL
685
+ * 以及协议相对的 `//host/...` 一律拒。这样判定不需要知道当前 origin,
686
+ * `@webskill/ui` 得以保持环境无关。
687
+ */
688
+ const imageSrc = z.string().refine((src) => /^data:image\//i.test(src) || !/^([a-z][a-z0-9+.-]*:|\/\/)/i.test(src), { error: codedError("IMAGE_SRC_NOT_ALLOWED", "Image.src must be a same-origin path or a data:image URL; remote URLs are rejected by policy.") });
689
+ const actionRef = z.object({
690
+ /** catalog actions 里声明过的名字 */
691
+ action: z.string().describe("Action name declared by the catalog"),
692
+ label: z.string(),
693
+ disabled: z.boolean().optional()
694
+ });
695
+ const CONTENT = [
696
+ "Stack",
697
+ "Card",
698
+ "Separator",
699
+ "Heading",
700
+ "Text",
701
+ "Markdown",
702
+ "Badge"
703
+ ];
704
+ const DATA = [
705
+ "Metric",
706
+ "Table",
707
+ "Chart",
708
+ "Timeline",
709
+ "FileLink"
710
+ ];
711
+ const INPUT = [
712
+ "Form",
713
+ "Field",
714
+ "FieldArray",
715
+ "Button"
716
+ ];
717
+ /**
718
+ * `visibleWhen` 的结构化条件(设计 23 §1.2)。用 `z.lazy` 自引用;
719
+ * 语义深度上限不在 zod 里卡,由 `evaluateFieldCondition` 按 `MAX_CONDITION_DEPTH` 降级处理。
720
+ */
721
+ const fieldCondition = z.lazy(() => z.union([
722
+ z.object({
723
+ field: z.string(),
724
+ equals: z.union([
725
+ z.string(),
726
+ z.number(),
727
+ z.boolean()
728
+ ])
729
+ }),
730
+ z.object({
731
+ field: z.string(),
732
+ in: z.array(z.union([z.string(), z.number()]))
733
+ }),
734
+ z.object({
735
+ field: z.string(),
736
+ notEmpty: z.literal(true)
737
+ }),
738
+ z.object({ allOf: z.array(fieldCondition).min(1) }),
739
+ z.object({ anyOf: z.array(fieldCondition).min(1) })
740
+ ]));
741
+ /** 选项既可以写死,也可以声明成异步取值(取值通道属分册 24) */
742
+ const fieldOptions = z.array(z.object({
743
+ label: z.string(),
744
+ value: z.union([z.string(), z.number()])
745
+ }));
746
+ /** Tabs 的面板与 Grid 的格子必须自成容器,否则「每容器至多一个表单」就没有落脚点 */
747
+ const PANEL = ["Card", "Stack"];
748
+ /**
749
+ * v3 首发 catalog:覆盖旧六类 surface(metric / chart / table / form / file / custom)的全部表达力,先窄后宽。
750
+ * 只依赖 zod —— 不 import 任何 UI framework(21 号文档 §5.1)。
751
+ */
752
+ const uiCatalog = defineUiCatalog({
753
+ id: "https://webskill.dev/catalogs/v1",
754
+ version: "1.0.0",
755
+ title: "WebSkill AI Component Catalog",
756
+ instructions: [
757
+ "Compose the answer UI from the components below. Rules:",
758
+ "",
759
+ "- The root node of a surface must be `Stack` or `Card`.",
760
+ "- Ask for user input with `Form` + `Field`; never describe an input in `Text`.",
761
+ "- A `Button`'s `action` must be one of the catalog actions.",
762
+ "- Never emit HTML or scripts. `Image.src` must be same-origin or a data URL.",
763
+ "- Prefer `Table` for many rows, `Chart` for trends, `Metric` for a single number,",
764
+ " `Timeline` for ordered steps and `Progress` for how far a long task has come.",
765
+ "- Match `Field.type` to the question instead of using `text` for everything:",
766
+ " `select` / `multi-select` for a fixed set of options, `date` for dates,",
767
+ " `toggle` for yes/no, `number` for quantities, `textarea` for several sentences.",
768
+ "- Group independent sections with `Tabs` or `Grid` instead of one long column.",
769
+ "- At most one `Form` per container; a `Tabs` panel and a `Grid` cell are separate containers."
770
+ ].join("\n"),
771
+ components: [
772
+ {
773
+ name: "Stack",
774
+ group: "layout",
775
+ description: "Vertical or horizontal container. Use it as the root of most surfaces.",
776
+ props: z.object({
777
+ direction: z.enum(["vertical", "horizontal"]).default("vertical"),
778
+ gap: z.enum([
779
+ "sm",
780
+ "md",
781
+ "lg"
782
+ ]).default("md"),
783
+ align: z.enum([
784
+ "start",
785
+ "center",
786
+ "end",
787
+ "stretch"
788
+ ]).optional(),
789
+ theme: surfaceTheme.optional()
790
+ }),
791
+ children: "any",
792
+ example: {
793
+ component: "Stack",
794
+ props: { direction: "vertical" },
795
+ children: [{
796
+ component: "Heading",
797
+ props: {
798
+ level: 2,
799
+ text: "Q4 results"
800
+ }
801
+ }]
802
+ }
803
+ },
804
+ {
805
+ name: "Card",
806
+ group: "layout",
807
+ description: "A titled section that groups related content.",
808
+ props: z.object({
809
+ title: z.string(),
810
+ description: z.string().optional(),
811
+ palette: palette.optional(),
812
+ theme: surfaceTheme.optional(),
813
+ span: z.union([z.literal(1), z.literal(2)]).default(1),
814
+ density: z.enum([
815
+ "compact",
816
+ "default",
817
+ "comfortable"
818
+ ]).default("default")
819
+ }),
820
+ children: "any",
821
+ example: {
822
+ component: "Card",
823
+ props: { title: "Revenue" },
824
+ children: [{
825
+ component: "Metric",
826
+ props: {
827
+ label: "MRR",
828
+ value: "12,400"
829
+ }
830
+ }]
831
+ }
832
+ },
833
+ {
834
+ name: "Separator",
835
+ group: "layout",
836
+ description: "A horizontal rule between sections.",
837
+ props: z.object({}),
838
+ example: { component: "Separator" }
839
+ },
840
+ {
841
+ name: "Tabs",
842
+ group: "layout",
843
+ description: "Switchable panels. Each child is one panel and its own form container.",
844
+ props: z.object({ labels: z.array(z.string()).min(2) }),
845
+ children: PANEL,
846
+ constraints: ["One child per label, in the same order"],
847
+ example: {
848
+ component: "Tabs",
849
+ props: { labels: ["Filters", "Export"] },
850
+ children: [{
851
+ component: "Stack",
852
+ children: [{
853
+ component: "Text",
854
+ props: { text: "Pick a date range." }
855
+ }]
856
+ }, {
857
+ component: "Stack",
858
+ children: [{
859
+ component: "Text",
860
+ props: { text: "Choose a file format." }
861
+ }]
862
+ }]
863
+ }
864
+ },
865
+ {
866
+ name: "Split",
867
+ group: "layout",
868
+ description: "Two panes side by side at a fixed ratio — a chart next to its takeaways, a form next to its help text, a summary next to the detail it summarises. Takes exactly two children and stacks them vertically when the container is narrow, so the main pane always comes first. Use `Grid` when the cells are peers rather than a main/side pair.",
869
+ props: z.object({ ratio: z.enum([
870
+ "1:1",
871
+ "2:1",
872
+ "3:1",
873
+ "1:2"
874
+ ]).default("1:1") }),
875
+ children: PANEL,
876
+ example: {
877
+ component: "Split",
878
+ props: { ratio: "2:1" },
879
+ children: [{
880
+ component: "Card",
881
+ props: { title: "Trend" }
882
+ }, {
883
+ component: "Card",
884
+ props: { title: "Notes" }
885
+ }]
886
+ }
887
+ },
888
+ {
889
+ name: "Grid",
890
+ group: "layout",
891
+ description: "Dashboard grid: several tiles, metrics or charts the reader scans in parallel. Each child is one cell and its own form container. Prefer `columns: 'auto'` with a `minColumnWidth` — it fits as many cells as the container allows and reflows when narrow, which a fixed count cannot do. Set a fixed count only when the layout really needs exactly N.",
892
+ props: z.object({
893
+ columns: z.union([
894
+ z.literal(2),
895
+ z.literal(3),
896
+ z.literal(4),
897
+ z.literal("auto")
898
+ ]).default(2),
899
+ minColumnWidth: z.enum([
900
+ "sm",
901
+ "md",
902
+ "lg"
903
+ ]).default("md")
904
+ }),
905
+ children: PANEL,
906
+ example: {
907
+ component: "Grid",
908
+ props: { columns: 2 },
909
+ children: [{
910
+ component: "Card",
911
+ props: { title: "Revenue" },
912
+ children: [{
913
+ component: "Metric",
914
+ props: {
915
+ label: "MRR",
916
+ value: "12,400"
917
+ }
918
+ }]
919
+ }, {
920
+ component: "Card",
921
+ props: { title: "Churn" },
922
+ children: [{
923
+ component: "Metric",
924
+ props: {
925
+ label: "Rate",
926
+ value: "2.1%"
927
+ }
928
+ }]
929
+ }]
930
+ }
931
+ },
932
+ {
933
+ name: "Heading",
934
+ group: "content",
935
+ description: "Section heading. Level 2 is the largest allowed inside a message.",
936
+ props: z.object({
937
+ level: z.union([
938
+ z.literal(2),
939
+ z.literal(3),
940
+ z.literal(4)
941
+ ]).default(3),
942
+ text: z.string(),
943
+ palette: palette.optional()
944
+ }),
945
+ example: {
946
+ component: "Heading",
947
+ props: {
948
+ level: 3,
949
+ text: "Summary"
950
+ }
951
+ }
952
+ },
953
+ {
954
+ name: "Text",
955
+ group: "content",
956
+ description: "A paragraph of plain text.",
957
+ props: z.object({
958
+ text: z.string(),
959
+ tone: z.enum(["default", "muted"]).default("default")
960
+ }),
961
+ example: {
962
+ component: "Text",
963
+ props: { text: "Revenue grew 12% quarter over quarter." }
964
+ }
965
+ },
966
+ {
967
+ name: "Markdown",
968
+ group: "content",
969
+ description: "Restricted Markdown (no HTML). Use for lists and emphasis, not for tables.",
970
+ props: z.object({ text: z.string() }),
971
+ constraints: ["HTML tags are rejected; use Table for tabular data"],
972
+ example: {
973
+ component: "Markdown",
974
+ props: { text: "- first\n- second" }
975
+ }
976
+ },
977
+ {
978
+ name: "Badge",
979
+ group: "content",
980
+ description: "A short status label.",
981
+ props: z.object({
982
+ text: z.string(),
983
+ tone: tone.default("neutral"),
984
+ palette: palette.optional()
985
+ }),
986
+ example: {
987
+ component: "Badge",
988
+ props: {
989
+ text: "active",
990
+ tone: "success"
991
+ }
992
+ }
993
+ },
994
+ {
995
+ name: "Metric",
996
+ group: "data",
997
+ description: "A single headline number with an optional change indicator.",
998
+ props: z.object({
999
+ label: z.string(),
1000
+ value: z.union([z.string(), z.number()]),
1001
+ change: z.string().optional(),
1002
+ trend: z.enum([
1003
+ "up",
1004
+ "down",
1005
+ "neutral"
1006
+ ]).optional()
1007
+ }),
1008
+ example: {
1009
+ component: "Metric",
1010
+ props: {
1011
+ label: "MRR",
1012
+ value: "12,400",
1013
+ trend: "up",
1014
+ change: "+12%"
1015
+ }
1016
+ }
1017
+ },
1018
+ {
1019
+ name: "Table",
1020
+ group: "data",
1021
+ description: "Tabular data with explicit columns. Prefer it over Markdown tables.",
1022
+ props: z.object({
1023
+ title: z.string().optional(),
1024
+ columns: z.array(z.string()).min(1),
1025
+ rows: z.array(z.array(z.union([
1026
+ z.string(),
1027
+ z.number(),
1028
+ z.boolean(),
1029
+ z.null()
1030
+ ]))),
1031
+ /** 列宽权重,长度需与 columns 一致;不一致时整项忽略而不拒绝渲染 */
1032
+ columnWidths: z.array(z.number().positive()).optional()
1033
+ }),
1034
+ example: {
1035
+ component: "Table",
1036
+ props: {
1037
+ columns: ["Quarter", "Revenue"],
1038
+ rows: [["Q3", 9800], ["Q4", 12400]]
1039
+ }
1040
+ }
1041
+ },
1042
+ {
1043
+ name: "Chart",
1044
+ group: "data",
1045
+ description: "A bar, line, area, pie, scatter, stacked or dual-axis chart over labelled series.",
1046
+ props: z.object({
1047
+ type: z.enum([
1048
+ "bar",
1049
+ "line",
1050
+ "area",
1051
+ "pie",
1052
+ "scatter",
1053
+ "stacked-bar",
1054
+ "dual-axis"
1055
+ ]),
1056
+ title: z.string().optional(),
1057
+ labels: z.array(z.string()).min(1),
1058
+ series: z.array(z.object({
1059
+ name: z.string(),
1060
+ values: z.array(z.number())
1061
+ })).min(1)
1062
+ }),
1063
+ example: {
1064
+ component: "Chart",
1065
+ props: {
1066
+ type: "bar",
1067
+ labels: ["Q3", "Q4"],
1068
+ series: [{
1069
+ name: "Revenue",
1070
+ values: [9800, 12400]
1071
+ }]
1072
+ }
1073
+ }
1074
+ },
1075
+ {
1076
+ name: "Timeline",
1077
+ group: "data",
1078
+ description: "Ordered steps or events, earliest first.",
1079
+ props: z.object({ items: z.array(z.object({
1080
+ title: z.string(),
1081
+ time: z.string().optional(),
1082
+ description: z.string().optional(),
1083
+ state: z.enum([
1084
+ "done",
1085
+ "active",
1086
+ "pending"
1087
+ ]).default("pending")
1088
+ })).min(1) }),
1089
+ example: {
1090
+ component: "Timeline",
1091
+ props: { items: [{
1092
+ title: "Plan",
1093
+ time: "Mon",
1094
+ state: "done"
1095
+ }, {
1096
+ title: "Build",
1097
+ time: "Tue",
1098
+ state: "active"
1099
+ }] }
1100
+ }
1101
+ },
1102
+ {
1103
+ name: "FileLink",
1104
+ group: "data",
1105
+ description: "A download entry for a file. The host automatically renders every artifact the run produced as a download card, so never create a FileLink for those; use it only for files that are not run artifacts.",
1106
+ props: z.object({
1107
+ path: z.string(),
1108
+ label: z.string().optional(),
1109
+ mimeType: z.string().optional(),
1110
+ /** 字节数;人类可读格式化是渲染层的事,不进 props */
1111
+ size: z.number().int().nonnegative().optional(),
1112
+ actions: z.array(actionRef).optional()
1113
+ }),
1114
+ constraints: ["size is a byte count, not a formatted string"],
1115
+ example: {
1116
+ component: "FileLink",
1117
+ props: {
1118
+ path: "report.csv",
1119
+ label: "Download report",
1120
+ size: 2411724
1121
+ }
1122
+ }
1123
+ },
1124
+ {
1125
+ name: "Form",
1126
+ group: "input",
1127
+ description: "Collects user input. Children must be Field nodes; actions go in the buttons prop.",
1128
+ props: z.object({
1129
+ title: z.string().optional(),
1130
+ submitLabel: z.string().default("Submit"),
1131
+ cancelLabel: z.string().optional()
1132
+ }),
1133
+ children: ["Field", "FieldArray"],
1134
+ singletonPerContainer: true,
1135
+ constraints: ["At most one Form per container", "Every Field name must be unique inside the Form"],
1136
+ example: {
1137
+ component: "Form",
1138
+ props: { title: "Schedule report" },
1139
+ children: [{
1140
+ component: "Field",
1141
+ props: {
1142
+ name: "email",
1143
+ label: "Email",
1144
+ type: "text",
1145
+ required: true
1146
+ }
1147
+ }]
1148
+ }
1149
+ },
1150
+ {
1151
+ name: "Field",
1152
+ group: "input",
1153
+ description: "A single form control. Only valid inside a Form.",
1154
+ props: z.object({
1155
+ name: z.string(),
1156
+ label: z.string(),
1157
+ type: z.enum([
1158
+ "text",
1159
+ "number",
1160
+ "date",
1161
+ "textarea",
1162
+ "select",
1163
+ "multi-select",
1164
+ "toggle",
1165
+ "file",
1166
+ "password"
1167
+ ]),
1168
+ required: z.boolean().optional(),
1169
+ description: z.string().optional(),
1170
+ defaultValue: z.unknown().optional(),
1171
+ options: fieldOptions.optional(),
1172
+ /** 声明后选项由宕主异步提供;与写死的 options 互斥 */
1173
+ optionsSource: z.string().optional(),
1174
+ visibleWhen: fieldCondition.optional()
1175
+ }),
1176
+ constraints: [
1177
+ "select / multi-select must provide options or optionsSource",
1178
+ "password values are never persisted, never echoed and never enter the user profile",
1179
+ "visibleWhen may only reference fields declared in the same Form"
1180
+ ],
1181
+ example: {
1182
+ component: "Field",
1183
+ props: {
1184
+ name: "range",
1185
+ label: "Range",
1186
+ type: "select",
1187
+ options: []
1188
+ }
1189
+ }
1190
+ },
1191
+ {
1192
+ name: "FieldArray",
1193
+ group: "input",
1194
+ description: "A repeatable group of fields. Only valid inside a Form; children must be Field nodes.",
1195
+ props: z.object({
1196
+ name: z.string(),
1197
+ label: z.string(),
1198
+ minItems: z.number().int().min(0).optional(),
1199
+ maxItems: z.number().int().min(1).optional(),
1200
+ addLabel: z.string().optional(),
1201
+ removeLabel: z.string().optional()
1202
+ }),
1203
+ children: ["Field"],
1204
+ constraints: ["Only valid inside a Form", "Submitted as an array of objects keyed by the inner Field names"],
1205
+ example: {
1206
+ component: "FieldArray",
1207
+ props: {
1208
+ name: "recipients",
1209
+ label: "Recipients",
1210
+ minItems: 1
1211
+ },
1212
+ children: [{
1213
+ component: "Field",
1214
+ props: {
1215
+ name: "email",
1216
+ label: "Email",
1217
+ type: "text"
1218
+ }
1219
+ }]
1220
+ }
1221
+ },
1222
+ {
1223
+ name: "Button",
1224
+ group: "input",
1225
+ description: "Triggers one of the catalog actions.",
1226
+ props: actionRef,
1227
+ example: {
1228
+ component: "Button",
1229
+ props: {
1230
+ action: "submit",
1231
+ label: "Send"
1232
+ }
1233
+ }
1234
+ },
1235
+ {
1236
+ name: "Progress",
1237
+ group: "feedback",
1238
+ description: "How far a long-running task has come, in percent.",
1239
+ props: z.object({
1240
+ label: z.string().optional(),
1241
+ value: z.number().min(0).max(100),
1242
+ tone: z.enum([
1243
+ "neutral",
1244
+ "success",
1245
+ "warning"
1246
+ ]).default("neutral")
1247
+ }),
1248
+ example: {
1249
+ component: "Progress",
1250
+ props: {
1251
+ label: "Indexing",
1252
+ value: 42
1253
+ }
1254
+ }
1255
+ },
1256
+ {
1257
+ name: "Icon",
1258
+ group: "content",
1259
+ description: "A small semantic icon, picked from the catalog whitelist. Use it to mark an item inside a list or next to a heading, never on its own as decoration. Names outside the whitelist are dropped by the renderer, so choose the closest listed name instead of inventing one.",
1260
+ props: z.object({
1261
+ name: iconName,
1262
+ palette: palette.optional()
1263
+ }),
1264
+ example: {
1265
+ component: "Icon",
1266
+ props: {
1267
+ name: "check",
1268
+ palette: "emerald"
1269
+ }
1270
+ }
1271
+ },
1272
+ {
1273
+ name: "Image",
1274
+ group: "content",
1275
+ description: "An image the host already serves. `src` must be same-origin or a data URL — remote URLs are rejected by policy, so never link to an image you found on the web. `alt` is required: it is what a screen reader and a later model read instead of the pixels.",
1276
+ props: z.object({
1277
+ src: imageSrc,
1278
+ alt: z.string(),
1279
+ ratio: z.enum([
1280
+ "auto",
1281
+ "1:1",
1282
+ "4:3",
1283
+ "16:9"
1284
+ ]).default("auto")
1285
+ }),
1286
+ example: {
1287
+ component: "Image",
1288
+ props: {
1289
+ src: "/chart.png",
1290
+ alt: "Revenue by region",
1291
+ ratio: "16:9"
1292
+ }
1293
+ }
1294
+ },
1295
+ {
1296
+ name: "Quote",
1297
+ group: "content",
1298
+ description: "Someone else's words, set apart from your own. Use it when you are repeating a customer comment, a policy sentence or a source document, so the reader can tell it apart from your summary. Do not use it for emphasis.",
1299
+ props: z.object({
1300
+ text: z.string(),
1301
+ by: z.string().optional()
1302
+ }),
1303
+ example: {
1304
+ component: "Quote",
1305
+ props: {
1306
+ text: "Delivery was two days late.",
1307
+ by: "Order A-1002"
1308
+ }
1309
+ }
1310
+ },
1311
+ {
1312
+ name: "Callout",
1313
+ group: "content",
1314
+ description: "A side note the reader may want but does not have to act on — background, a caveat, a definition. Use `Alert` instead when it is a state they must react to, and plain `Text` when it belongs in the flow of the answer.",
1315
+ props: z.object({
1316
+ title: z.string().optional(),
1317
+ text: z.string(),
1318
+ palette: palette.optional()
1319
+ }),
1320
+ example: {
1321
+ component: "Callout",
1322
+ props: {
1323
+ title: "How this is counted",
1324
+ text: "Refunds are excluded."
1325
+ }
1326
+ }
1327
+ },
1328
+ {
1329
+ name: "KeyValue",
1330
+ group: "data",
1331
+ description: "Facts about one thing, as label/value pairs. Use it when a `Table` would have a single row, for example the details of one order. For several comparable rows use `Table`, and for one headline number use `Metric`.",
1332
+ props: z.object({ items: z.array(z.object({
1333
+ label: z.string(),
1334
+ value: z.string()
1335
+ })).min(1) }),
1336
+ example: {
1337
+ component: "KeyValue",
1338
+ props: { items: [{
1339
+ label: "Status",
1340
+ value: "Shipped"
1341
+ }, {
1342
+ label: "Carrier",
1343
+ value: "DHL"
1344
+ }] }
1345
+ }
1346
+ },
1347
+ {
1348
+ name: "Gauge",
1349
+ group: "data",
1350
+ description: "One value read against a range — capacity used, budget spent, a score out of a maximum. Give `min` and `max` when the range is not 0-100. Use `Progress` for how far a task has come, and `Metric` when there is no range to compare against.",
1351
+ props: z.object({
1352
+ label: z.string().optional(),
1353
+ value: z.number(),
1354
+ min: z.number().default(0),
1355
+ max: z.number().default(100),
1356
+ tone: z.enum([
1357
+ "neutral",
1358
+ "success",
1359
+ "warning"
1360
+ ]).default("neutral")
1361
+ }),
1362
+ example: {
1363
+ component: "Gauge",
1364
+ props: {
1365
+ label: "Disk used",
1366
+ value: 82,
1367
+ tone: "warning"
1368
+ }
1369
+ }
1370
+ },
1371
+ {
1372
+ name: "Accordion",
1373
+ group: "layout",
1374
+ description: "Sections the reader opens one at a time. Use it for a long report where most readers only need one part, so the answer stays scannable instead of becoming a wall of text. Use `Tabs` when the sections are alternatives rather than a sequence, and plain `Card`s when there are only two or three short ones.",
1375
+ props: z.object({ items: z.array(z.object({
1376
+ title: z.string(),
1377
+ text: z.string()
1378
+ })).min(1) }),
1379
+ example: {
1380
+ component: "Accordion",
1381
+ props: { items: [{
1382
+ title: "Method",
1383
+ text: "Sampled hourly."
1384
+ }, {
1385
+ title: "Caveats",
1386
+ text: "Week 12 is partial."
1387
+ }] }
1388
+ }
1389
+ },
1390
+ {
1391
+ name: "Carousel",
1392
+ group: "layout",
1393
+ description: "Slides the reader scrolls through sideways — an itinerary day by day, a set of options, a small gallery. Each slide carries its own title and optional image, and only one is in view at a time. Use `Grid` when the reader should compare the items side by side instead of one at a time.",
1394
+ props: z.object({ items: z.array(z.object({
1395
+ title: z.string(),
1396
+ text: z.string().optional(),
1397
+ src: z.string().optional()
1398
+ })).min(1) }),
1399
+ example: {
1400
+ component: "Carousel",
1401
+ props: { items: [{
1402
+ title: "Day 1",
1403
+ text: "Arrival"
1404
+ }, {
1405
+ title: "Day 2",
1406
+ text: "Old town"
1407
+ }] }
1408
+ }
1409
+ },
1410
+ {
1411
+ name: "Alert",
1412
+ group: "feedback",
1413
+ description: "Draws attention to a state the user must know about.",
1414
+ props: z.object({
1415
+ tone: z.enum([
1416
+ "info",
1417
+ "warning",
1418
+ "destructive"
1419
+ ]).default("info"),
1420
+ title: z.string(),
1421
+ description: z.string().optional()
1422
+ }),
1423
+ example: {
1424
+ component: "Alert",
1425
+ props: {
1426
+ tone: "warning",
1427
+ title: "Partial data"
1428
+ }
1429
+ }
1430
+ },
1431
+ {
1432
+ name: "Custom",
1433
+ group: "layout",
1434
+ description: "Host extension point; the name must be registered in the host SurfaceRegistry.",
1435
+ props: z.object({
1436
+ name: z.string(),
1437
+ props: z.record(z.string(), z.unknown()).default({})
1438
+ }),
1439
+ constraints: ["Rejected when the host has not registered the component name"],
1440
+ example: {
1441
+ component: "Custom",
1442
+ props: {
1443
+ name: "sales-map",
1444
+ props: {}
1445
+ }
1446
+ }
1447
+ },
1448
+ {
1449
+ name: "OpenDocument",
1450
+ group: "layout",
1451
+ description: "A button that asks the host to open a document artifact produced by this run in an isolated viewer. Use it for print-ready pages that do not fit inside the chat column; use Card/Table/Chart instead when the content belongs in the conversation.",
1452
+ props: z.object({
1453
+ artifact: z.string(),
1454
+ style: z.string().optional(),
1455
+ label: z.string().optional(),
1456
+ dataSource: z.string()
1457
+ }),
1458
+ constraints: ["The artifact name is resolved inside the producing run artifact directory; path separators are rejected", "Renders nothing when the host has not wired a document surface"],
1459
+ example: {
1460
+ component: "OpenDocument",
1461
+ props: {
1462
+ artifact: "report.html",
1463
+ dataSource: "fetchData: bugs / metrics"
1464
+ }
1465
+ }
1466
+ }
1467
+ ],
1468
+ actions: [
1469
+ {
1470
+ name: "submit",
1471
+ description: "Submit the surface form values back to the waiting run."
1472
+ },
1473
+ {
1474
+ name: "cancel",
1475
+ description: "Dismiss the surface without submitting."
1476
+ },
1477
+ {
1478
+ name: "select",
1479
+ description: "Report a selected row or option."
1480
+ },
1481
+ {
1482
+ name: "download",
1483
+ description: "Download the referenced artifact."
1484
+ },
1485
+ {
1486
+ name: "refresh",
1487
+ description: "Ask the run to recompute the surface."
1488
+ }
1489
+ ]
1490
+ });
1491
+ /** prompt / 文档里引用的分组顺序(Console 展示同序) */
1492
+ const UI_CATALOG_GROUPS = {
1493
+ content: CONTENT,
1494
+ data: DATA,
1495
+ input: INPUT
1496
+ };
1497
+ /**
1498
+ * catalog 系统提示的体积上限(UTF-8 字节,FR-6.6)。
1499
+ *
1500
+ * **0.11.0 起只是 `CATALOG_PROMPT_MAX` 的别名**:旧值 20 480 相对实际 7 788 宽了 2.6 倍,
1501
+ * 拦不住任何东西;且它只量提示词,漏掉了 `render_ui` 入参 Schema 那约两分之一的成本。
1502
+ * @deprecated 用 `CATALOG_PROMPT_MAX` / `CATALOG_SCHEMA_MAX`(`./budget`)。
1503
+ */
1504
+ const UI_CATALOG_PROMPT_BUDGET_BYTES = CATALOG_PROMPT_MAX;
1505
+
1506
+ //#endregion
1507
+ //#region ../ui/src/catalog/formScopes.ts
1508
+ function fieldNamesOf(form) {
1509
+ const names = [];
1510
+ const walk = (node) => {
1511
+ const name = node.props?.["name"];
1512
+ if (node.component === "Field" && typeof name === "string") names.push(name);
1513
+ for (const child of node.children ?? []) walk(child);
1514
+ };
1515
+ walk(form);
1516
+ return names;
1517
+ }
1518
+ /**
1519
+ * 按出现序列出声明树里的每个 `Form`。
1520
+ *
1521
+ * 单表单时 action id 保持 `submit` / `cancel`,回传形状与容器级放宽前逐字节一致——
1522
+ * 向后兼容是 FR-6.3 的硬约束,不能让既有单表单卡片跟着改。
1523
+ * 多表单时每个表单拿到独立的 action id 与 `scopeId`,渲染层据此只收集本表单子树内的字段。
1524
+ */
1525
+ function collectFormScopes(spec) {
1526
+ const forms = [];
1527
+ const walk = (node) => {
1528
+ if (node.component === "Form") forms.push(node);
1529
+ for (const child of node.children ?? []) walk(child);
1530
+ };
1531
+ walk(spec);
1532
+ if (forms.length === 1) {
1533
+ const form = forms[0];
1534
+ return [{
1535
+ scopeId: form.id ?? "form",
1536
+ submitActionId: "submit",
1537
+ cancelActionId: "cancel",
1538
+ fieldNames: fieldNamesOf(form),
1539
+ form
1540
+ }];
1541
+ }
1542
+ return forms.map((form, index) => {
1543
+ const scopeId = form.id ?? `form-${index}`;
1544
+ return {
1545
+ scopeId,
1546
+ submitActionId: `${scopeId}:submit`,
1547
+ cancelActionId: `${scopeId}:cancel`,
1548
+ fieldNames: fieldNamesOf(form),
1549
+ form
1550
+ };
1551
+ });
1552
+ }
1553
+ /**
1554
+ * 多表单时字段值在渲染层的存储键。
1555
+ *
1556
+ * 跨表单重名过去不可能发生,容器级放宽后它变成合法输入:两个表单各有一个 `email`,
1557
+ * 共用一张扁平表就是后写的覆盖先写的,用户在 A 里输入会串到 B。
1558
+ * 因此多表单时按作用域限定键;单表单不加前缀,存储与草稿形状保持不变。
1559
+ */
1560
+ function qualifyFieldName(name, scopeId) {
1561
+ return scopeId === void 0 ? name : `${scopeId}.${name}`;
1562
+ }
1563
+ /**
1564
+ * 提交时回传的字段值。
1565
+ *
1566
+ * 不给 `scope` → 整张 surface 的扁平值,与容器级放宽前完全一致(FR-6.3 第 3 条)。
1567
+ * 给了 `scope` → 只取该表单子树内的字段,键回落成裸字段名,
1568
+ * 因此接收端看到的载荷形状与单表单时相同。
1569
+ */
1570
+ function collectScopedValues(values, scope) {
1571
+ if (!scope) return { ...values };
1572
+ const out = {};
1573
+ for (const name of scope.fieldNames) {
1574
+ const key = qualifyFieldName(name, scope.scopeId);
1575
+ if (key in values) out[name] = values[key];
1576
+ else if (name in values) out[name] = values[name];
1577
+ }
1578
+ return out;
1579
+ }
1580
+
1581
+ //#endregion
1582
+ //#region ../ui/src/catalog/fieldCondition.ts
1583
+ /**
1584
+ * 条件树的语义深度上限。
1585
+ * `MAX_JSON_DEPTH` 管的是 props 的 JSON 深度,管不到这里——超它之前就能先把栈打爆。
1586
+ */
1587
+ const MAX_CONDITION_DEPTH = 8;
1588
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1589
+ const isEmpty = (value) => value === void 0 || value === null || value === "" || Array.isArray(value) && value.length === 0;
1590
+ function invalid(path, detail) {
1591
+ return {
1592
+ visible: true,
1593
+ degraded: {
1594
+ path,
1595
+ kind: "condition-invalid",
1596
+ detail
1597
+ }
1598
+ };
1599
+ }
1600
+ function evaluate(condition, context, depth) {
1601
+ if (depth > 8) return invalid(context.path, `Condition nesting exceeds ${8} levels.`);
1602
+ if (!isRecord(condition)) return invalid(context.path, "Condition must be an object.");
1603
+ if (Array.isArray(condition["allOf"]) || Array.isArray(condition["anyOf"])) {
1604
+ const isAll = Array.isArray(condition["allOf"]);
1605
+ const branches = isAll ? condition["allOf"] : condition["anyOf"];
1606
+ if (branches.length === 0) return invalid(context.path, `"${isAll ? "allOf" : "anyOf"}" must not be empty.`);
1607
+ let visible = isAll;
1608
+ for (const branch of branches) {
1609
+ const result = evaluate(branch, context, depth + 1);
1610
+ if (result.degraded) return result;
1611
+ visible = isAll ? visible && result.visible : visible || result.visible;
1612
+ }
1613
+ return { visible };
1614
+ }
1615
+ const field = condition["field"];
1616
+ if (typeof field !== "string" || field === "") return invalid(context.path, "Condition needs a \"field\" name.");
1617
+ const value = context.resolve(field);
1618
+ if ("equals" in condition) {
1619
+ const expected = condition["equals"];
1620
+ if (typeof expected !== "string" && typeof expected !== "number" && typeof expected !== "boolean") return invalid(context.path, "\"equals\" must be a string, number or boolean.");
1621
+ return { visible: value === expected };
1622
+ }
1623
+ if ("in" in condition) {
1624
+ const options = condition["in"];
1625
+ if (!Array.isArray(options)) return invalid(context.path, "\"in\" must be an array.");
1626
+ return { visible: options.some((option) => option === value) };
1627
+ }
1628
+ if ("notEmpty" in condition) {
1629
+ if (condition["notEmpty"] !== true) return invalid(context.path, "\"notEmpty\" must be true.");
1630
+ return { visible: !isEmpty(value) };
1631
+ }
1632
+ return invalid(context.path, "Condition has no recognised operator (equals / in / notEmpty / allOf / anyOf).");
1633
+ }
1634
+ /**
1635
+ * 全仓唯一的条件求值实现(AC-23.4)。纯函数、无 React 依赖——
1636
+ * `useSurfaceForm` 在 ui-react 且是 hook,ui-vue 用不了它。
1637
+ */
1638
+ function evaluateFieldCondition(condition, values, options = {}) {
1639
+ const resolveField = options.resolveField;
1640
+ return evaluate(condition, {
1641
+ values,
1642
+ resolve: (field) => values[resolveField ? resolveField(field) : field],
1643
+ path: options.path ?? "visibleWhen"
1644
+ }, 1);
1645
+ }
1646
+
1647
+ //#endregion
1648
+ //#region ../ui/src/catalog/tableColumns.ts
1649
+ /**
1650
+ * 生成式表格的列模型(分册 15-03 定义 `priority`,分册 25 在同一结构上追加 `weight`)。
1651
+ *
1652
+ * 四个渲染档共用这一份:native / json-render / OpenUI 走 TS,a2ui 是 Lit,
1653
+ * 后者读不到 TS 常量,所以最小列宽以 **CSS 变量**交付,两侧引用同一个名字。
1654
+ */
1655
+ /** 最小列宽的分档取值。`desktop` 沿用历史值,改它会动所有既有表格的视觉基线 */
1656
+ const SPEC_TABLE_MIN_COLUMN_WIDTH = {
1657
+ desktop: "8rem",
1658
+ mobile: "5rem"
1659
+ };
1660
+ /** CSS 侧的引用名:`surfaces.css` 与 a2ui 的 Lit 样式都只写这个变量,不写字面量 */
1661
+ const SPEC_TABLE_MIN_COLUMN_VAR = "--webskill-table-min-col";
1662
+ /**
1663
+ * 校验并归一化 `columnWidths`。
1664
+ *
1665
+ * **整项忽略**是刻意的:部分采纳(例如只丢掉那个负数)会让模型拿到一个
1666
+ * 「看起来生效了」的结果,更难发现自己写错了(FR-25.3)。
1667
+ */
1668
+ function normalizeColumnWidths(raw, columnCount) {
1669
+ const equal = () => Array.from({ length: columnCount }, () => 1);
1670
+ if (raw === void 0) return { weights: equal() };
1671
+ if (raw.length !== columnCount) return {
1672
+ weights: equal(),
1673
+ rejected: "length-mismatch"
1674
+ };
1675
+ if (raw.some((value) => typeof value !== "number" || !Number.isFinite(value))) return {
1676
+ weights: equal(),
1677
+ rejected: "non-finite"
1678
+ };
1679
+ if (raw.some((value) => value < 0)) return {
1680
+ weights: equal(),
1681
+ rejected: "negative"
1682
+ };
1683
+ if (raw.every((value) => value === 0)) return {
1684
+ weights: equal(),
1685
+ rejected: "all-zero"
1686
+ };
1687
+ return { weights: [...raw] };
1688
+ }
1689
+ /**
1690
+ * 按权重分配列宽,且每列不低于 `minWidth`。
1691
+ *
1692
+ * 朴素实现(按权重算一遍、低于下限的抬到下限)会让总宽超出容器,
1693
+ * 白白产生本可避免的横滚。这里迭代到不动点:每轮把触底的列钉住,
1694
+ * 剩余空间在剩余列间按权重重新分配(FR-25.2)。
1695
+ *
1696
+ * 容器本身放不下 `columnCount * minWidth` 时无解——此时全部取下限并返回,
1697
+ * 由调用方决定横滚(AC-25.4)。
1698
+ */
1699
+ function resolveColumnWidths(weights, available, minWidth) {
1700
+ const count = weights.length;
1701
+ if (count === 0) return [];
1702
+ if (available <= count * minWidth) return Array.from({ length: count }, () => minWidth);
1703
+ const result = Array.from({ length: count }, () => 0);
1704
+ const pinned = new Array(count).fill(false);
1705
+ for (let round = 0; round <= count; round += 1) {
1706
+ const freeIndexes = result.map((_, index) => index).filter((index) => !pinned[index]);
1707
+ const remaining = available - result.reduce((sum, value, index) => pinned[index] ? sum + value : sum, 0);
1708
+ const weightTotal = freeIndexes.reduce((sum, index) => sum + (weights[index] ?? 0), 0);
1709
+ let changed = false;
1710
+ for (const index of freeIndexes) {
1711
+ const share = weightTotal > 0 ? remaining * (weights[index] ?? 0) / weightTotal : remaining / freeIndexes.length;
1712
+ if (share < minWidth) {
1713
+ result[index] = minWidth;
1714
+ pinned[index] = true;
1715
+ changed = true;
1716
+ } else result[index] = share;
1717
+ }
1718
+ if (!changed) break;
1719
+ }
1720
+ return result;
1721
+ }
1722
+
1723
+ //#endregion
1724
+ //#region ../ui/src/catalog/toJsonRenderSpec.ts
1725
+ /**
1726
+ * catalog 声明树 → json-render 扁平 spec。纯数据变换,不 import json-render
1727
+ * (`@webskill/ui` 保持环境无关)。
1728
+ */
1729
+ function toJsonRenderSpec(spec) {
1730
+ const elements = {};
1731
+ let counter = 0;
1732
+ const walk = (node) => {
1733
+ const key = node.id ?? `${node.component.toLowerCase()}-${counter++}`;
1734
+ const children = (node.children ?? []).map(walk);
1735
+ elements[key] = {
1736
+ type: node.component,
1737
+ props: node.props ?? {},
1738
+ ...children.length > 0 ? { children } : {}
1739
+ };
1740
+ return key;
1741
+ };
1742
+ return {
1743
+ root: walk(spec),
1744
+ elements
1745
+ };
1746
+ }
1747
+
1748
+ //#endregion
1749
+ //#region ../ui/src/catalog/interactionToUiSpec.ts
1750
+ /** ControlModel 的控件词汇 → catalog `Field.type`(catalog 的 9 种是超集) */
1751
+ const FIELD_TYPE = {
1752
+ text: "text",
1753
+ number: "number",
1754
+ boolean: "toggle",
1755
+ select: "select",
1756
+ textarea: "textarea",
1757
+ file: "file",
1758
+ password: "password",
1759
+ date: "date"
1760
+ };
1761
+ function toOptions(options) {
1762
+ return (options ?? []).map((option) => ({
1763
+ label: option.label,
1764
+ value: typeof option.value === "number" ? option.value : String(option.value)
1765
+ }));
1766
+ }
1767
+ function toField(control) {
1768
+ return {
1769
+ component: "Field",
1770
+ props: {
1771
+ name: control.name,
1772
+ label: control.label,
1773
+ type: FIELD_TYPE[control.control],
1774
+ ...control.required ? { required: true } : {},
1775
+ ...control.description ? { description: control.description } : {},
1776
+ ...control.defaultValue !== void 0 ? { defaultValue: control.defaultValue } : {},
1777
+ ...control.suggestion ? { suggestion: control.suggestion } : {},
1778
+ ...control.accept ? { accept: [...control.accept] } : {},
1779
+ ...control.multiple ? { multiple: true } : {},
1780
+ ...control.options ? { options: toOptions(control.options) } : {}
1781
+ }
1782
+ };
1783
+ }
1784
+ /**
1785
+ * 五类 `InteractionRequest` → catalog 声明树。
1786
+ *
1787
+ * 路径 A(runtime 发起的交互请求)与路径 B(模型产出的 surface)的差别只在「谁产出这棵树」;
1788
+ * 产出之后四档走的是同一条 spec 渲染路径,因此这份生成器全仓只写一份。
1789
+ */
1790
+ function interactionToUiSpec(request, labels = {}) {
1791
+ const model = interactionToFormModel(request);
1792
+ const title = labels.title ?? model.title;
1793
+ const cancelLabel = labels.cancelLabel ?? model.cancelLabel;
1794
+ const form = {
1795
+ component: "Form",
1796
+ props: {
1797
+ ...title ? { title } : {},
1798
+ submitLabel: labels.submitLabel ?? model.submitLabel,
1799
+ ...cancelLabel ? { cancelLabel } : {}
1800
+ },
1801
+ children: model.controls.map(toField)
1802
+ };
1803
+ if (model.controls.length > 0 || !model.message) return form;
1804
+ return {
1805
+ component: "Stack",
1806
+ children: [{
1807
+ component: "Text",
1808
+ props: { text: model.message }
1809
+ }, form]
1810
+ };
1811
+ }
1812
+
1813
+ //#endregion
1814
+ //#region ../ui/src/adapters/openui/openUiSpecLang.ts
1815
+ const literal = (value) => JSON.stringify(value ?? null);
1816
+ /** 组件的位置参数顺序 = catalog props 的 schema key 顺序(OpenUI 的 param map 同源) */
1817
+ function propOrder(catalog) {
1818
+ const branches = catalog.toJsonSchema()["oneOf"] ?? [];
1819
+ const order = /* @__PURE__ */ new Map();
1820
+ for (const branch of branches) {
1821
+ const name = (branch.properties?.["component"])?.["const"];
1822
+ const props = branch.properties?.["props"];
1823
+ if (typeof name === "string") order.set(name, Object.keys(props?.properties ?? {}));
1824
+ }
1825
+ return order;
1826
+ }
1827
+ /**
1828
+ * catalog 声明树 → openui-lang 文本。语法为每行一条 `id = Component(位置参数…)`,
1829
+ * `root = …` 为入口;容器组件的子节点作为最后一个参数传 id 数组。
1830
+ */
1831
+ function toOpenUiSpecLang(spec, catalog = uiCatalog) {
1832
+ const order = propOrder(catalog);
1833
+ const lines = [];
1834
+ let counter = 0;
1835
+ const walk = (node) => {
1836
+ const id = node.id ?? `n${counter++}`;
1837
+ const childIds = (node.children ?? []).map(walk);
1838
+ const def = catalog.component(node.component);
1839
+ const args = (order.get(node.component) ?? []).map((key) => literal(node.props?.[key]));
1840
+ if (def?.children !== void 0) args.push(`[${childIds.join(", ")}]`);
1841
+ lines.push(`${id} = ${node.component}(${args.join(", ")})`);
1842
+ return id;
1843
+ };
1844
+ const rootId = walk(spec);
1845
+ return [...lines.filter((line) => !line.startsWith(`${rootId} =`)), `root = ${lines.find((line) => line.startsWith(`${rootId} =`)).slice(rootId.length + 3)}`].join("\n");
1846
+ }
1847
+
1848
+ //#endregion
1849
+ export { chartSpecFromProps as C, toEchartsOption as E, DEFAULT_CHART_FONT_SIZES as S, resolveChartFontSizes as T, CATALOG_BUDGET_STAGE as _, SPEC_TABLE_MIN_COLUMN_WIDTH as a, PLANNED_INCREMENT as b, MAX_CONDITION_DEPTH as c, collectScopedValues as d, qualifyFieldName as f, defineUiCatalog as g, uiCatalog as h, SPEC_TABLE_MIN_COLUMN_VAR as i, evaluateFieldCondition as l, UI_CATALOG_PROMPT_BUDGET_BYTES as m, interactionToUiSpec as n, normalizeColumnWidths as o, UI_CATALOG_GROUPS as p, toJsonRenderSpec as r, resolveColumnWidths as s, toOpenUiSpecLang as t, collectFormScopes as u, CATALOG_PROMPT_MAX as v, mountEchart as w, gaugePercent as x, CATALOG_SCHEMA_MAX as y };