@webskill/sdk 0.10.0 → 0.12.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.
- package/dist/agent.d.ts +3 -2
- package/dist/agent.js +3 -1102
- package/dist/browser.d.ts +258 -11
- package/dist/browser.js +763 -47
- package/dist/{catalogComponents-DTcYfpLQ-CcoOaz-Z.js → catalogComponents-BgAJN0p8-C3K8klJd.js} +508 -923
- package/dist/dist-DU9KDAuR.js +1468 -0
- package/dist/{dist-1OFC-zax.js → dist-DqcL6jKO.js} +668 -60
- package/dist/{dist-BViUeszk.js → dist-sdKFgERo.js} +956 -115
- package/dist/{echarts-DhNm2ene.js → echarts-De78wXqV.js} +599 -61
- package/dist/{eventTypes-s2uwAcLG-Go3l_dUe.js → eventTypes-FllCrX-Z-DNDeHWoG.js} +6 -2
- package/dist/governance.d.ts +7 -3
- package/dist/governance.js +1 -1
- package/dist/{index-B0QPLWPZ.d.ts → index-3fCHc1mQ.d.ts} +198 -11
- package/dist/{index-DtFdMKBX.d.ts → index-C9pzXLKy.d.ts} +366 -9
- package/dist/{index-BF4E1a9j.d.ts → index-DFhU1uks.d.ts} +286 -19
- package/dist/index.d.ts +4 -4
- package/dist/index.js +3 -3
- package/dist/mcp.d.ts +58 -7
- package/dist/mcp.js +124 -21
- package/dist/node.d.ts +3 -3
- package/dist/node.js +60 -4
- package/dist/{openUiLibrary-CIrV--Ad-B8zG_91e.js → openUiLibrary-BKXW7Iwx-DaymVubt.js} +3 -3
- package/dist/processSandboxEntry.js +8 -1
- package/dist/sandboxWorkerEntry.js +8 -1
- package/dist/{skillVersionStore-D-qHk9ZE-DBsYYCWn.d.ts → skillVersionStore-D-qHk9ZE-DheTIwAB.d.ts} +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/{types-CrRcT-LM-DZAp8sWv.d.ts → types-DLctJep_-B5G4uk2u.d.ts} +22 -5
- package/dist/ui-react.d.ts +13 -4
- package/dist/ui-react.js +93 -259
- package/dist/ui-vue.d.ts +1 -1
- package/dist/ui-vue.js +2 -2
- package/dist/ui.d.ts +4 -4
- package/dist/ui.js +3 -3
- package/dist/{webskillLitCatalog-BJrphK0y-SGmRXaaO.js → webskillLitCatalog-D_zCqeQF-C9lrvvMr.js} +135 -16
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { h as WebSkillError } from "./dist-Bev6i6Ip.js";
|
|
2
2
|
import { toJSONSchema, z } from "zod";
|
|
3
3
|
|
|
4
|
-
//#region ../ui/dist/webskillCatalog-
|
|
4
|
+
//#region ../ui/dist/webskillCatalog-nTgkL9Xe.js
|
|
5
5
|
const DEFAULT_INTERACTION_TEXTS = {
|
|
6
6
|
submit: "Submit",
|
|
7
7
|
cancel: "Cancel",
|
|
@@ -39,6 +39,19 @@ function resolveSurfaceFormTexts(texts) {
|
|
|
39
39
|
...texts
|
|
40
40
|
} : DEFAULT_SURFACE_FORM_TEXTS;
|
|
41
41
|
}
|
|
42
|
+
const DEFAULT_SURFACE_HOST_CONTROL_TEXTS = {
|
|
43
|
+
previousPage: "Previous page",
|
|
44
|
+
previous: "Previous",
|
|
45
|
+
nextPage: "Next page",
|
|
46
|
+
next: "Next",
|
|
47
|
+
carousel: "Carousel"
|
|
48
|
+
};
|
|
49
|
+
function resolveSurfaceHostControlTexts(texts) {
|
|
50
|
+
return texts ? {
|
|
51
|
+
...DEFAULT_SURFACE_HOST_CONTROL_TEXTS,
|
|
52
|
+
...texts
|
|
53
|
+
} : DEFAULT_SURFACE_HOST_CONTROL_TEXTS;
|
|
54
|
+
}
|
|
42
55
|
/** 提交值按请求类型归形(WebFormBridge 与框架组件库共享单一来源) */
|
|
43
56
|
function shapeInteractionValue(model, values) {
|
|
44
57
|
switch (model.kind) {
|
|
@@ -216,23 +229,27 @@ function renderMiniMarkdown(text, doc) {
|
|
|
216
229
|
}
|
|
217
230
|
return root;
|
|
218
231
|
}
|
|
232
|
+
/** 与 catalog `Chart` 的 props 枚举逐字一致(`ui/catalog/uiCatalog.ts`) */
|
|
219
233
|
const KINDS = /* @__PURE__ */ new Set([
|
|
220
234
|
"bar",
|
|
221
235
|
"line",
|
|
222
|
-
"
|
|
236
|
+
"area",
|
|
237
|
+
"pie",
|
|
238
|
+
"scatter",
|
|
239
|
+
"stacked-bar",
|
|
240
|
+
"dual-axis"
|
|
223
241
|
]);
|
|
224
242
|
const list = (value) => Array.isArray(value) ? value : [];
|
|
225
243
|
/**
|
|
226
244
|
* catalog `Chart` 节点 props → `ChartSpec`。四档共用这一份转换:
|
|
227
245
|
* 此前 native / OpenUI / json-render / A2UI 各抄了一遍,改一处就会漂移。
|
|
228
|
-
*
|
|
229
|
-
*
|
|
246
|
+
* 非法枚举**原样保留**,由 `toEchartsOption` 给出可见降级提示——
|
|
247
|
+
* 此前偷偷改写成 `'line'`,画出的图与模型的意图无关且不可察觉。
|
|
230
248
|
*/
|
|
231
249
|
function chartSpecFromProps(props) {
|
|
232
|
-
const type = typeof props["type"] === "string" ? props["type"] : "bar";
|
|
233
250
|
const series = list(props["series"]);
|
|
234
251
|
return {
|
|
235
|
-
kind:
|
|
252
|
+
kind: typeof props["type"] === "string" ? props["type"] : "",
|
|
236
253
|
labels: list(props["labels"]),
|
|
237
254
|
series: series.map((item) => ({
|
|
238
255
|
...typeof item.name === "string" ? { name: item.name } : {},
|
|
@@ -242,24 +259,70 @@ function chartSpecFromProps(props) {
|
|
|
242
259
|
};
|
|
243
260
|
}
|
|
244
261
|
const seriesNames = (chart) => chart.series.map((series) => series.name ?? "");
|
|
262
|
+
/** 字号默认值的**全仓唯一来源**;`12` 是 echarts 的全局默认,显式写出后渲染不变 */
|
|
263
|
+
const DEFAULT_CHART_FONT_SIZES = {
|
|
264
|
+
title: 14,
|
|
265
|
+
axisLabel: 12,
|
|
266
|
+
legend: 12
|
|
267
|
+
};
|
|
268
|
+
/** 逐字段兜底:宿主通常只想调大其中一两项 */
|
|
269
|
+
function resolveChartFontSizes(sizes) {
|
|
270
|
+
return {
|
|
271
|
+
...DEFAULT_CHART_FONT_SIZES,
|
|
272
|
+
...sizes
|
|
273
|
+
};
|
|
274
|
+
}
|
|
245
275
|
/** echarts 的图例条目来源:饼图按标签分项,其余按系列名 */
|
|
246
276
|
const legendEntries = (chart) => chart.kind === "pie" ? chart.labels : seriesNames(chart);
|
|
277
|
+
/**
|
|
278
|
+
* 非饼图的 series 形状表:catalog 声明的每种类型对应「基础 type + 零到一个修饰」。
|
|
279
|
+
* 饼图不在表内——它的 data 要按 labels 重组,形状与其余六种不同。
|
|
280
|
+
*/
|
|
281
|
+
const SERIES_SHAPE = {
|
|
282
|
+
bar: { type: "bar" },
|
|
283
|
+
line: { type: "line" },
|
|
284
|
+
area: {
|
|
285
|
+
type: "line",
|
|
286
|
+
areaStyle: {}
|
|
287
|
+
},
|
|
288
|
+
scatter: { type: "scatter" },
|
|
289
|
+
"stacked-bar": {
|
|
290
|
+
type: "bar",
|
|
291
|
+
stack: "total"
|
|
292
|
+
},
|
|
293
|
+
"dual-axis": { type: "line" }
|
|
294
|
+
};
|
|
295
|
+
const FALLBACK_KIND = "bar";
|
|
296
|
+
const fallbackNotice = (kind) => kind === "" ? `Missing chart type; rendered as a ${FALLBACK_KIND} chart.` : `Unsupported chart type "${kind}"; rendered as a ${FALLBACK_KIND} chart.`;
|
|
247
297
|
/** `ChartSpec` → ECharts option:图表类型、数据系列、图例的唯一定义处 */
|
|
248
|
-
function toEchartsOption(chart) {
|
|
298
|
+
function toEchartsOption(chart, fontSizes) {
|
|
299
|
+
const font = resolveChartFontSizes(fontSizes);
|
|
300
|
+
const known = KINDS.has(chart.kind);
|
|
301
|
+
const shape = SERIES_SHAPE[known ? chart.kind : FALLBACK_KIND];
|
|
302
|
+
const notice = known ? void 0 : fallbackNotice(chart.kind);
|
|
303
|
+
const valueAxis = {
|
|
304
|
+
type: "value",
|
|
305
|
+
axisLabel: { fontSize: font.axisLabel }
|
|
306
|
+
};
|
|
249
307
|
return {
|
|
250
308
|
animation: false,
|
|
251
|
-
title: chart.title ? {
|
|
252
|
-
text: chart.title,
|
|
309
|
+
title: chart.title || notice ? {
|
|
310
|
+
text: chart.title ?? "",
|
|
253
311
|
left: "center",
|
|
254
|
-
textStyle: { fontSize:
|
|
312
|
+
textStyle: { fontSize: font.title },
|
|
313
|
+
...notice ? { subtext: notice } : {}
|
|
255
314
|
} : void 0,
|
|
256
315
|
tooltip: { trigger: chart.kind === "pie" ? "item" : "axis" },
|
|
257
|
-
legend: {
|
|
316
|
+
legend: {
|
|
317
|
+
bottom: 0,
|
|
318
|
+
textStyle: { fontSize: font.legend }
|
|
319
|
+
},
|
|
258
320
|
xAxis: chart.kind === "pie" ? void 0 : {
|
|
259
321
|
type: "category",
|
|
260
|
-
data: chart.labels
|
|
322
|
+
data: chart.labels,
|
|
323
|
+
axisLabel: { fontSize: font.axisLabel }
|
|
261
324
|
},
|
|
262
|
-
yAxis: chart.kind === "pie" ? void 0 :
|
|
325
|
+
yAxis: chart.kind === "pie" ? void 0 : chart.kind === "dual-axis" ? [valueAxis, { ...valueAxis }] : valueAxis,
|
|
263
326
|
series: chart.kind === "pie" ? chart.series.map((series) => ({
|
|
264
327
|
type: "pie",
|
|
265
328
|
name: series.name,
|
|
@@ -267,10 +330,11 @@ function toEchartsOption(chart) {
|
|
|
267
330
|
name: label,
|
|
268
331
|
value: series.data[index] ?? 0
|
|
269
332
|
}))
|
|
270
|
-
})) : chart.series.map((series) => ({
|
|
271
|
-
|
|
333
|
+
})) : chart.series.map((series, index) => ({
|
|
334
|
+
...shape,
|
|
272
335
|
name: series.name,
|
|
273
|
-
data: series.data
|
|
336
|
+
data: series.data,
|
|
337
|
+
...chart.kind === "dual-axis" ? { yAxisIndex: index === 0 ? 0 : 1 } : {}
|
|
274
338
|
}))
|
|
275
339
|
};
|
|
276
340
|
}
|
|
@@ -286,6 +350,12 @@ function applyChartDomContract(container, chart) {
|
|
|
286
350
|
else container.removeAttribute("aria-label");
|
|
287
351
|
}
|
|
288
352
|
/**
|
|
353
|
+
* 一篇文档常同时挂载多张图表;共用同一个模块 promise,
|
|
354
|
+
* 避免每个挂载点各自发起一次动态 import。
|
|
355
|
+
*/
|
|
356
|
+
let echartsModule;
|
|
357
|
+
const loadEcharts = () => echartsModule ??= import("./echarts-De78wXqV.js");
|
|
358
|
+
/**
|
|
289
359
|
* 框架无关的 echarts 挂载点:三档的 React 组件与 A2UI 档的 Lit 元素共用同一实现,
|
|
290
360
|
* 图表不再因档位而异。echarts 走动态 import,不打进首屏。
|
|
291
361
|
*/
|
|
@@ -294,26 +364,29 @@ function mountEchart(container) {
|
|
|
294
364
|
let instance;
|
|
295
365
|
let queued;
|
|
296
366
|
let observer;
|
|
297
|
-
const apply = (chart) => {
|
|
298
|
-
instance?.setOption(toEchartsOption(chart), {
|
|
367
|
+
const apply = (chart, fontSizes) => {
|
|
368
|
+
instance?.setOption(toEchartsOption(chart, fontSizes), {
|
|
299
369
|
notMerge: true,
|
|
300
370
|
lazyUpdate: true
|
|
301
371
|
});
|
|
302
372
|
};
|
|
303
|
-
|
|
373
|
+
loadEcharts().then((echarts) => {
|
|
304
374
|
if (disposed) return;
|
|
305
375
|
instance = echarts.init(container);
|
|
306
376
|
if (typeof ResizeObserver !== "undefined") {
|
|
307
377
|
observer = new ResizeObserver(() => instance?.resize());
|
|
308
378
|
observer.observe(container);
|
|
309
379
|
}
|
|
310
|
-
if (queued) apply(queued);
|
|
380
|
+
if (queued) apply(queued.chart, queued.fontSizes);
|
|
311
381
|
});
|
|
312
382
|
return {
|
|
313
|
-
setChart(chart) {
|
|
383
|
+
setChart(chart, fontSizes) {
|
|
314
384
|
applyChartDomContract(container, chart);
|
|
315
|
-
queued =
|
|
316
|
-
|
|
385
|
+
queued = {
|
|
386
|
+
chart,
|
|
387
|
+
...fontSizes ? { fontSizes } : {}
|
|
388
|
+
};
|
|
389
|
+
apply(chart, fontSizes);
|
|
317
390
|
},
|
|
318
391
|
dispose() {
|
|
319
392
|
disposed = true;
|
|
@@ -324,6 +397,74 @@ function mountEchart(container) {
|
|
|
324
397
|
}
|
|
325
398
|
};
|
|
326
399
|
}
|
|
400
|
+
/**
|
|
401
|
+
* catalog 请求体积上限(0.11.0 分册 14 / AC-G20)的**数字单一来源**。
|
|
402
|
+
*
|
|
403
|
+
* 度量与报错格式在 `scripts/catalogBudget.mjs`(`check:contracts` 与 vitest 共用);
|
|
404
|
+
* 那边不重复这些数字,一律从本模块读——两处各写一份阈值就等于没有阈值。
|
|
405
|
+
*
|
|
406
|
+
* catalog 进入每一次 LLM 请求的有两处:系统提示词 `toPrompt()` 与
|
|
407
|
+
* `render_ui` 入参 `toJsonSchema()`。只量前者会漏掉约 2/3 的成本。
|
|
408
|
+
*
|
|
409
|
+
* **超阈值的处置是砍组件或收窄枚举取值,不是抬高这里的数字。**
|
|
410
|
+
*/
|
|
411
|
+
/**
|
|
412
|
+
* 预算阶段。分册 12 是本版最后一处 catalog 增量(分册 11 只把 `date` 加进
|
|
413
|
+
* 已有的 `Field.type`,分册 23 不碰 catalog),故在此切到 `'final'`:
|
|
414
|
+
* 阈值收紧到实测终态 + 余量,且余量小于一个平均组件的成本
|
|
415
|
+
* ——余量大于一个组件,等于默许下一版无声塞进一个。
|
|
416
|
+
*/
|
|
417
|
+
const CATALOG_BUDGET_STAGE = "final";
|
|
418
|
+
/**
|
|
419
|
+
* 阈值 = **已落地实测** + 余量,余量须小于一个平均组件的成本。
|
|
420
|
+
*
|
|
421
|
+
* 2026-08-14 实测终态(分册 12 落地后):prompt 12 744 / schema 21 470,30 个组件。
|
|
422
|
+
* 平均组件成本 prompt 425 / schema 716。
|
|
423
|
+
* 当前余量 prompt 256 / schema 330,均小于一个平均组件——再加一个组件必红。
|
|
424
|
+
*
|
|
425
|
+
* ⚠️ **本版这两个数字被上调过两次**,两次都由需求方裁决并改写了需求文档
|
|
426
|
+
* (分册 29 §2 范围订正 第 7 条与 §2.2),不是实现阶段自行放宽:
|
|
427
|
+
*
|
|
428
|
+
* 1. 11 200 → 12 200 / 20 500 → 21 200:分册 13 组件描述由短改长。
|
|
429
|
+
* `example` 是既有护栏的硬要求(`catalog.test.ts` 与 `catalogMatrix.test.tsx`
|
|
430
|
+
* 逐个组件校验并四档渲染),靠省掉示例腾出的空间收不回来。
|
|
431
|
+
* 2. 12 200 → 13 000 / 21 200 → 21 800:分册 12 的 `Split` 实测 490 + 694,
|
|
432
|
+
* 超出计划增量(403 + 1 047)的 prompt 侧。需求方裁定
|
|
433
|
+
* 「描述要长,可用性大于预算阈值」——收紧措辞可省回 158 字节,
|
|
434
|
+
* 但砍的正是各组件「何时改用 X」的辨析句,即选型准确率本身。
|
|
435
|
+
*
|
|
436
|
+
* 两次上调合计把单请求成本从 21.7 KB 抬到约 34.2 KB。
|
|
437
|
+
* **AC-G20 原文「超了砍组件、不抬阈值」已被推翻,但仅限本版;
|
|
438
|
+
* 后续再撞顶仍须回到需求方,不得沿用本版作为先例。**
|
|
439
|
+
*/
|
|
440
|
+
const CATALOG_PROMPT_MAX = 13e3;
|
|
441
|
+
const CATALOG_SCHEMA_MAX = 21800;
|
|
442
|
+
/**
|
|
443
|
+
* **尚未落地**的计划增量。本版已无剩余 catalog 增量,故归零;
|
|
444
|
+
* `CATALOG_BUDGET_STAGE` 同时切到 `'final'`,余量改由「小于一个平均组件」判定。
|
|
445
|
+
*/
|
|
446
|
+
const PLANNED_INCREMENT = {
|
|
447
|
+
prompt: 0,
|
|
448
|
+
schema: 0
|
|
449
|
+
};
|
|
450
|
+
/** 码与人话之间的分隔符。放在消息最前面,`codeOfIssue` 才能从 zod 的 issue 里认出它。 */
|
|
451
|
+
const SEPARATOR = ": ";
|
|
452
|
+
/**
|
|
453
|
+
* 给 zod 的 `error` 文案打上稳定码。
|
|
454
|
+
*
|
|
455
|
+
* 组件自己在 schema 里声明,sanitize 只负责把码搬到 `UiSpecDegradation.code` 上——
|
|
456
|
+
* 这样新增一条策略性约束不需要去 sanitize 里加组件名分支(写死清单是本仓反复出问题的地方)。
|
|
457
|
+
*/
|
|
458
|
+
function codedError(code, message) {
|
|
459
|
+
return `${code}${SEPARATOR}${message}`;
|
|
460
|
+
}
|
|
461
|
+
/** 从 zod issue 的消息里取回码;没有码就返回 undefined(普通类型错误就是这种)。 */
|
|
462
|
+
function codeOfIssue(message) {
|
|
463
|
+
const index = message.indexOf(SEPARATOR);
|
|
464
|
+
if (index <= 0) return void 0;
|
|
465
|
+
const head = message.slice(0, index);
|
|
466
|
+
return /^[A-Z][A-Z0-9_]*$/.test(head) ? head : void 0;
|
|
467
|
+
}
|
|
327
468
|
function isRecord$1$1(value) {
|
|
328
469
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
329
470
|
}
|
|
@@ -371,24 +512,36 @@ function sanitizeNode(catalog, byName, node, path, degradations) {
|
|
|
371
512
|
const parsed = def.props.safeParse(rawProps);
|
|
372
513
|
if (!parsed.success) {
|
|
373
514
|
const offending = new Set(parsed.error.issues.map((issue) => String(issue.path[0] ?? "")).filter((key) => key !== ""));
|
|
515
|
+
const codes = /* @__PURE__ */ new Map();
|
|
516
|
+
for (const issue of parsed.error.issues) {
|
|
517
|
+
const key = String(issue.path[0] ?? "");
|
|
518
|
+
const code = codeOfIssue(issue.message);
|
|
519
|
+
if (key !== "" && code !== void 0) codes.set(key, code);
|
|
520
|
+
}
|
|
374
521
|
const kept = {};
|
|
375
522
|
for (const [key, value] of Object.entries(rawProps)) if (!offending.has(key)) kept[key] = value;
|
|
376
523
|
const retry = def.props.safeParse(kept);
|
|
377
524
|
if (!retry.success) {
|
|
525
|
+
const carried = [...codes.values()].find((code) => code !== void 0);
|
|
378
526
|
degradations.push({
|
|
379
527
|
path,
|
|
380
528
|
kind: "node-dropped",
|
|
381
529
|
component: name,
|
|
382
|
-
detail: `Props cannot be repaired by dropping invalid keys: ${retry.error.issues.map((i) => i.message).join("; ")}
|
|
530
|
+
detail: `Props cannot be repaired by dropping invalid keys: ${retry.error.issues.map((i) => i.message).join("; ")}`,
|
|
531
|
+
...carried === void 0 ? {} : { code: carried }
|
|
383
532
|
});
|
|
384
533
|
return;
|
|
385
534
|
}
|
|
386
|
-
for (const key of offending)
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
535
|
+
for (const key of offending) {
|
|
536
|
+
const code = codes.get(key);
|
|
537
|
+
degradations.push({
|
|
538
|
+
path: `${path}.props.${key}`,
|
|
539
|
+
kind: "prop-dropped",
|
|
540
|
+
component: name,
|
|
541
|
+
detail: `Dropped invalid prop "${key}".`,
|
|
542
|
+
...code === void 0 ? {} : { code }
|
|
543
|
+
});
|
|
544
|
+
}
|
|
392
545
|
props = kept;
|
|
393
546
|
}
|
|
394
547
|
const sanitized = {
|
|
@@ -457,8 +610,8 @@ function sanitizeUiSpec(catalog, byName, spec) {
|
|
|
457
610
|
degradations
|
|
458
611
|
};
|
|
459
612
|
}
|
|
460
|
-
function componentSchema(def) {
|
|
461
|
-
const props = toJSONSchema(def.props, { io: "input" });
|
|
613
|
+
function componentSchema(def, rootDefs) {
|
|
614
|
+
const props = liftDefs(toJSONSchema(def.props, { io: "input" }), def.name, rootDefs);
|
|
462
615
|
const node = {
|
|
463
616
|
type: "object",
|
|
464
617
|
description: def.description,
|
|
@@ -488,6 +641,37 @@ function componentSchema(def) {
|
|
|
488
641
|
function isRecord$2(value) {
|
|
489
642
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
490
643
|
}
|
|
644
|
+
const DEFS_REF_PREFIX = "#/$defs/";
|
|
645
|
+
function rewriteDefRefs(node, renames) {
|
|
646
|
+
if (Array.isArray(node)) return node.map((child) => rewriteDefRefs(child, renames));
|
|
647
|
+
if (!isRecord$2(node)) return node;
|
|
648
|
+
const out = {};
|
|
649
|
+
for (const [key, value] of Object.entries(node)) {
|
|
650
|
+
if (key === "$ref" && typeof value === "string" && value.startsWith(DEFS_REF_PREFIX)) {
|
|
651
|
+
const [head, ...tail] = value.slice(8).split("/");
|
|
652
|
+
const mapped = head === void 0 ? void 0 : renames.get(head);
|
|
653
|
+
out[key] = mapped === void 0 ? value : [`${DEFS_REF_PREFIX}${mapped}`, ...tail].join("/");
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
out[key] = rewriteDefRefs(value, renames);
|
|
657
|
+
}
|
|
658
|
+
return out;
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* zod 每个组件各自产出 `$defs.__schemaN`,其 `#/$defs/...` 指的是那次转换自己的根;
|
|
662
|
+
* 直接嵌进 catalog 大 schema 后那个 `#` 变成了 catalog 根,引用从产物根解析不到。
|
|
663
|
+
* 提到根并以组件名限定,避免不同组件的同名定义互相覆盖。
|
|
664
|
+
*/
|
|
665
|
+
function liftDefs(schema, owner, rootDefs) {
|
|
666
|
+
const defs = schema["$defs"];
|
|
667
|
+
if (!isRecord$2(defs)) return schema;
|
|
668
|
+
const renames = /* @__PURE__ */ new Map();
|
|
669
|
+
for (const name of Object.keys(defs)) renames.set(name, `${owner}_${name}`);
|
|
670
|
+
for (const [name, def] of Object.entries(defs)) rootDefs[renames.get(name)] = rewriteDefRefs(def, renames);
|
|
671
|
+
const body = {};
|
|
672
|
+
for (const [key, value] of Object.entries(schema)) if (key !== "$defs") body[key] = value;
|
|
673
|
+
return rewriteDefRefs(body, renames);
|
|
674
|
+
}
|
|
491
675
|
function typeOf(schema) {
|
|
492
676
|
if (Array.isArray(schema.enum)) return schema.enum.map((option) => JSON.stringify(option)).join(" | ");
|
|
493
677
|
if (schema["const"] !== void 0) return JSON.stringify(schema["const"]);
|
|
@@ -612,13 +796,18 @@ function defineUiCatalog(input) {
|
|
|
612
796
|
componentNames: input.components.map((def) => def.name),
|
|
613
797
|
actionNames: input.actions.map((action) => action.name),
|
|
614
798
|
component: (name) => byName.get(name),
|
|
615
|
-
toJsonSchema: () =>
|
|
616
|
-
$
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
799
|
+
toJsonSchema: () => {
|
|
800
|
+
const $defs = {};
|
|
801
|
+
const oneOf = input.components.map((def) => componentSchema(def, $defs));
|
|
802
|
+
return {
|
|
803
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
804
|
+
$id: input.id,
|
|
805
|
+
title: input.title,
|
|
806
|
+
description: input.instructions,
|
|
807
|
+
...Object.keys($defs).length > 0 ? { $defs } : {},
|
|
808
|
+
oneOf
|
|
809
|
+
};
|
|
810
|
+
},
|
|
622
811
|
toPrompt: (options = {}) => promptFor(input, options),
|
|
623
812
|
validate: (spec) => {
|
|
624
813
|
const issues = [];
|
|
@@ -638,6 +827,56 @@ const tone = z.enum([
|
|
|
638
827
|
"info",
|
|
639
828
|
"destructive"
|
|
640
829
|
]);
|
|
830
|
+
/**
|
|
831
|
+
* 装饰色。与 `tone` 语义**不重叠**:tone 表状态(success 是「成功」),
|
|
832
|
+
* palette 表美观(「行程第三天」用绿色不代表它成功了)。
|
|
833
|
+
* 取值是色板名不是颜色值——必须映射到 design token,深浅色主题才能自动跟随。
|
|
834
|
+
*/
|
|
835
|
+
const palette = z.enum([
|
|
836
|
+
"sky",
|
|
837
|
+
"violet",
|
|
838
|
+
"amber",
|
|
839
|
+
"rose",
|
|
840
|
+
"emerald"
|
|
841
|
+
]);
|
|
842
|
+
/**
|
|
843
|
+
* surface 级视觉档(FR-13.4)。只在**根节点**上有意义,切的是一组 token
|
|
844
|
+
* (圆角/阴影/底色),不是逐组件改样式;作用域限于该 surface 子树。
|
|
845
|
+
* `office` 是缺省,与本版之前逐字节一致——它是回归基线。
|
|
846
|
+
*/
|
|
847
|
+
const surfaceTheme = z.enum([
|
|
848
|
+
"office",
|
|
849
|
+
"vivid",
|
|
850
|
+
"dashboard-dark"
|
|
851
|
+
]);
|
|
852
|
+
/** 图标白名单:任意名字会让模型产出渲染不出来的节点,且枚举长度直接计入每次请求 */
|
|
853
|
+
const iconName = z.enum([
|
|
854
|
+
"info",
|
|
855
|
+
"check",
|
|
856
|
+
"alert",
|
|
857
|
+
"x",
|
|
858
|
+
"clock",
|
|
859
|
+
"calendar",
|
|
860
|
+
"user",
|
|
861
|
+
"star",
|
|
862
|
+
"search",
|
|
863
|
+
"settings",
|
|
864
|
+
"download",
|
|
865
|
+
"link",
|
|
866
|
+
"chart",
|
|
867
|
+
"file",
|
|
868
|
+
"folder",
|
|
869
|
+
"mail"
|
|
870
|
+
]);
|
|
871
|
+
/**
|
|
872
|
+
* `Image.src` 的同源约束(AC-13.4)。**判定放在 schema 上,`validate` 与 `sanitize` 两条路径就都覆盖**;
|
|
873
|
+
* 只写进组件描述等于把安全交给模型自觉——提示注入可以用 `src` 把数据带到任意主机上去。
|
|
874
|
+
*
|
|
875
|
+
* 同源按**语法**判:`data:image/` 与相对路径放行,任何带 scheme 的绝对 URL
|
|
876
|
+
* 以及协议相对的 `//host/...` 一律拒。这样判定不需要知道当前 origin,
|
|
877
|
+
* `@webskill/ui` 得以保持环境无关。
|
|
878
|
+
*/
|
|
879
|
+
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.") });
|
|
641
880
|
const actionRef = z.object({
|
|
642
881
|
/** catalog actions 里声明过的名字 */
|
|
643
882
|
action: z.string().describe("Action name declared by the catalog"),
|
|
@@ -711,7 +950,7 @@ const uiCatalog = defineUiCatalog({
|
|
|
711
950
|
"- The root node of a surface must be `Stack` or `Card`.",
|
|
712
951
|
"- Ask for user input with `Form` + `Field`; never describe an input in `Text`.",
|
|
713
952
|
"- A `Button`'s `action` must be one of the catalog actions.",
|
|
714
|
-
"- Never emit HTML
|
|
953
|
+
"- Never emit HTML or scripts. `Image.src` must be same-origin or a data URL.",
|
|
715
954
|
"- Prefer `Table` for many rows, `Chart` for trends, `Metric` for a single number,",
|
|
716
955
|
" `Timeline` for ordered steps and `Progress` for how far a long task has come.",
|
|
717
956
|
"- Match `Field.type` to the question instead of using `text` for everything:",
|
|
@@ -737,7 +976,8 @@ const uiCatalog = defineUiCatalog({
|
|
|
737
976
|
"center",
|
|
738
977
|
"end",
|
|
739
978
|
"stretch"
|
|
740
|
-
]).optional()
|
|
979
|
+
]).optional(),
|
|
980
|
+
theme: surfaceTheme.optional()
|
|
741
981
|
}),
|
|
742
982
|
children: "any",
|
|
743
983
|
example: {
|
|
@@ -758,7 +998,15 @@ const uiCatalog = defineUiCatalog({
|
|
|
758
998
|
description: "A titled section that groups related content.",
|
|
759
999
|
props: z.object({
|
|
760
1000
|
title: z.string(),
|
|
761
|
-
description: z.string().optional()
|
|
1001
|
+
description: z.string().optional(),
|
|
1002
|
+
palette: palette.optional(),
|
|
1003
|
+
theme: surfaceTheme.optional(),
|
|
1004
|
+
span: z.union([z.literal(1), z.literal(2)]).default(1),
|
|
1005
|
+
density: z.enum([
|
|
1006
|
+
"compact",
|
|
1007
|
+
"default",
|
|
1008
|
+
"comfortable"
|
|
1009
|
+
]).default("default")
|
|
762
1010
|
}),
|
|
763
1011
|
children: "any",
|
|
764
1012
|
example: {
|
|
@@ -805,15 +1053,46 @@ const uiCatalog = defineUiCatalog({
|
|
|
805
1053
|
}]
|
|
806
1054
|
}
|
|
807
1055
|
},
|
|
1056
|
+
{
|
|
1057
|
+
name: "Split",
|
|
1058
|
+
group: "layout",
|
|
1059
|
+
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.",
|
|
1060
|
+
props: z.object({ ratio: z.enum([
|
|
1061
|
+
"1:1",
|
|
1062
|
+
"2:1",
|
|
1063
|
+
"3:1",
|
|
1064
|
+
"1:2"
|
|
1065
|
+
]).default("1:1") }),
|
|
1066
|
+
children: PANEL,
|
|
1067
|
+
example: {
|
|
1068
|
+
component: "Split",
|
|
1069
|
+
props: { ratio: "2:1" },
|
|
1070
|
+
children: [{
|
|
1071
|
+
component: "Card",
|
|
1072
|
+
props: { title: "Trend" }
|
|
1073
|
+
}, {
|
|
1074
|
+
component: "Card",
|
|
1075
|
+
props: { title: "Notes" }
|
|
1076
|
+
}]
|
|
1077
|
+
}
|
|
1078
|
+
},
|
|
808
1079
|
{
|
|
809
1080
|
name: "Grid",
|
|
810
1081
|
group: "layout",
|
|
811
|
-
description: "Dashboard grid. Each child is one cell and its own form container.",
|
|
812
|
-
props: z.object({
|
|
813
|
-
z.
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
1082
|
+
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.",
|
|
1083
|
+
props: z.object({
|
|
1084
|
+
columns: z.union([
|
|
1085
|
+
z.literal(2),
|
|
1086
|
+
z.literal(3),
|
|
1087
|
+
z.literal(4),
|
|
1088
|
+
z.literal("auto")
|
|
1089
|
+
]).default(2),
|
|
1090
|
+
minColumnWidth: z.enum([
|
|
1091
|
+
"sm",
|
|
1092
|
+
"md",
|
|
1093
|
+
"lg"
|
|
1094
|
+
]).default("md")
|
|
1095
|
+
}),
|
|
817
1096
|
children: PANEL,
|
|
818
1097
|
example: {
|
|
819
1098
|
component: "Grid",
|
|
@@ -851,7 +1130,8 @@ const uiCatalog = defineUiCatalog({
|
|
|
851
1130
|
z.literal(3),
|
|
852
1131
|
z.literal(4)
|
|
853
1132
|
]).default(3),
|
|
854
|
-
text: z.string()
|
|
1133
|
+
text: z.string(),
|
|
1134
|
+
palette: palette.optional()
|
|
855
1135
|
}),
|
|
856
1136
|
example: {
|
|
857
1137
|
component: "Heading",
|
|
@@ -891,7 +1171,8 @@ const uiCatalog = defineUiCatalog({
|
|
|
891
1171
|
description: "A short status label.",
|
|
892
1172
|
props: z.object({
|
|
893
1173
|
text: z.string(),
|
|
894
|
-
tone: tone.default("neutral")
|
|
1174
|
+
tone: tone.default("neutral"),
|
|
1175
|
+
palette: palette.optional()
|
|
895
1176
|
}),
|
|
896
1177
|
example: {
|
|
897
1178
|
component: "Badge",
|
|
@@ -952,13 +1233,16 @@ const uiCatalog = defineUiCatalog({
|
|
|
952
1233
|
{
|
|
953
1234
|
name: "Chart",
|
|
954
1235
|
group: "data",
|
|
955
|
-
description: "A bar, line, area or
|
|
1236
|
+
description: "A bar, line, area, pie, scatter, stacked or dual-axis chart over labelled series.",
|
|
956
1237
|
props: z.object({
|
|
957
1238
|
type: z.enum([
|
|
958
1239
|
"bar",
|
|
959
1240
|
"line",
|
|
960
1241
|
"area",
|
|
961
|
-
"pie"
|
|
1242
|
+
"pie",
|
|
1243
|
+
"scatter",
|
|
1244
|
+
"stacked-bar",
|
|
1245
|
+
"dual-axis"
|
|
962
1246
|
]),
|
|
963
1247
|
title: z.string().optional(),
|
|
964
1248
|
labels: z.array(z.string()).min(1),
|
|
@@ -1160,6 +1444,160 @@ const uiCatalog = defineUiCatalog({
|
|
|
1160
1444
|
}
|
|
1161
1445
|
}
|
|
1162
1446
|
},
|
|
1447
|
+
{
|
|
1448
|
+
name: "Icon",
|
|
1449
|
+
group: "content",
|
|
1450
|
+
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.",
|
|
1451
|
+
props: z.object({
|
|
1452
|
+
name: iconName,
|
|
1453
|
+
palette: palette.optional()
|
|
1454
|
+
}),
|
|
1455
|
+
example: {
|
|
1456
|
+
component: "Icon",
|
|
1457
|
+
props: {
|
|
1458
|
+
name: "check",
|
|
1459
|
+
palette: "emerald"
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
},
|
|
1463
|
+
{
|
|
1464
|
+
name: "Image",
|
|
1465
|
+
group: "content",
|
|
1466
|
+
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.",
|
|
1467
|
+
props: z.object({
|
|
1468
|
+
src: imageSrc,
|
|
1469
|
+
alt: z.string(),
|
|
1470
|
+
ratio: z.enum([
|
|
1471
|
+
"auto",
|
|
1472
|
+
"1:1",
|
|
1473
|
+
"4:3",
|
|
1474
|
+
"16:9"
|
|
1475
|
+
]).default("auto")
|
|
1476
|
+
}),
|
|
1477
|
+
example: {
|
|
1478
|
+
component: "Image",
|
|
1479
|
+
props: {
|
|
1480
|
+
src: "/chart.png",
|
|
1481
|
+
alt: "Revenue by region",
|
|
1482
|
+
ratio: "16:9"
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
},
|
|
1486
|
+
{
|
|
1487
|
+
name: "Quote",
|
|
1488
|
+
group: "content",
|
|
1489
|
+
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.",
|
|
1490
|
+
props: z.object({
|
|
1491
|
+
text: z.string(),
|
|
1492
|
+
by: z.string().optional()
|
|
1493
|
+
}),
|
|
1494
|
+
example: {
|
|
1495
|
+
component: "Quote",
|
|
1496
|
+
props: {
|
|
1497
|
+
text: "Delivery was two days late.",
|
|
1498
|
+
by: "Order A-1002"
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
},
|
|
1502
|
+
{
|
|
1503
|
+
name: "Callout",
|
|
1504
|
+
group: "content",
|
|
1505
|
+
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.",
|
|
1506
|
+
props: z.object({
|
|
1507
|
+
title: z.string().optional(),
|
|
1508
|
+
text: z.string(),
|
|
1509
|
+
palette: palette.optional()
|
|
1510
|
+
}),
|
|
1511
|
+
example: {
|
|
1512
|
+
component: "Callout",
|
|
1513
|
+
props: {
|
|
1514
|
+
title: "How this is counted",
|
|
1515
|
+
text: "Refunds are excluded."
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
},
|
|
1519
|
+
{
|
|
1520
|
+
name: "KeyValue",
|
|
1521
|
+
group: "data",
|
|
1522
|
+
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`.",
|
|
1523
|
+
props: z.object({ items: z.array(z.object({
|
|
1524
|
+
label: z.string(),
|
|
1525
|
+
value: z.string()
|
|
1526
|
+
})).min(1) }),
|
|
1527
|
+
example: {
|
|
1528
|
+
component: "KeyValue",
|
|
1529
|
+
props: { items: [{
|
|
1530
|
+
label: "Status",
|
|
1531
|
+
value: "Shipped"
|
|
1532
|
+
}, {
|
|
1533
|
+
label: "Carrier",
|
|
1534
|
+
value: "DHL"
|
|
1535
|
+
}] }
|
|
1536
|
+
}
|
|
1537
|
+
},
|
|
1538
|
+
{
|
|
1539
|
+
name: "Gauge",
|
|
1540
|
+
group: "data",
|
|
1541
|
+
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.",
|
|
1542
|
+
props: z.object({
|
|
1543
|
+
label: z.string().optional(),
|
|
1544
|
+
value: z.number(),
|
|
1545
|
+
min: z.number().default(0),
|
|
1546
|
+
max: z.number().default(100),
|
|
1547
|
+
tone: z.enum([
|
|
1548
|
+
"neutral",
|
|
1549
|
+
"success",
|
|
1550
|
+
"warning"
|
|
1551
|
+
]).default("neutral")
|
|
1552
|
+
}),
|
|
1553
|
+
example: {
|
|
1554
|
+
component: "Gauge",
|
|
1555
|
+
props: {
|
|
1556
|
+
label: "Disk used",
|
|
1557
|
+
value: 82,
|
|
1558
|
+
tone: "warning"
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
},
|
|
1562
|
+
{
|
|
1563
|
+
name: "Accordion",
|
|
1564
|
+
group: "layout",
|
|
1565
|
+
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.",
|
|
1566
|
+
props: z.object({ items: z.array(z.object({
|
|
1567
|
+
title: z.string(),
|
|
1568
|
+
text: z.string()
|
|
1569
|
+
})).min(1) }),
|
|
1570
|
+
example: {
|
|
1571
|
+
component: "Accordion",
|
|
1572
|
+
props: { items: [{
|
|
1573
|
+
title: "Method",
|
|
1574
|
+
text: "Sampled hourly."
|
|
1575
|
+
}, {
|
|
1576
|
+
title: "Caveats",
|
|
1577
|
+
text: "Week 12 is partial."
|
|
1578
|
+
}] }
|
|
1579
|
+
}
|
|
1580
|
+
},
|
|
1581
|
+
{
|
|
1582
|
+
name: "Carousel",
|
|
1583
|
+
group: "layout",
|
|
1584
|
+
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.",
|
|
1585
|
+
props: z.object({ items: z.array(z.object({
|
|
1586
|
+
title: z.string(),
|
|
1587
|
+
text: z.string().optional(),
|
|
1588
|
+
src: z.string().optional()
|
|
1589
|
+
})).min(1) }),
|
|
1590
|
+
example: {
|
|
1591
|
+
component: "Carousel",
|
|
1592
|
+
props: { items: [{
|
|
1593
|
+
title: "Day 1",
|
|
1594
|
+
text: "Arrival"
|
|
1595
|
+
}, {
|
|
1596
|
+
title: "Day 2",
|
|
1597
|
+
text: "Old town"
|
|
1598
|
+
}] }
|
|
1599
|
+
}
|
|
1600
|
+
},
|
|
1163
1601
|
{
|
|
1164
1602
|
name: "Alert",
|
|
1165
1603
|
group: "feedback",
|
|
@@ -1230,10 +1668,12 @@ const UI_CATALOG_GROUPS = {
|
|
|
1230
1668
|
};
|
|
1231
1669
|
/**
|
|
1232
1670
|
* catalog 系统提示的体积上限(UTF-8 字节,FR-6.6)。
|
|
1233
|
-
*
|
|
1234
|
-
*
|
|
1671
|
+
*
|
|
1672
|
+
* **0.11.0 起只是 `CATALOG_PROMPT_MAX` 的别名**:旧值 20 480 相对实际 7 788 宽了 2.6 倍,
|
|
1673
|
+
* 拦不住任何东西;且它只量提示词,漏掉了 `render_ui` 入参 Schema 那约两分之一的成本。
|
|
1674
|
+
* @deprecated 用 `CATALOG_PROMPT_MAX` / `CATALOG_SCHEMA_MAX`(`./budget`)。
|
|
1235
1675
|
*/
|
|
1236
|
-
const UI_CATALOG_PROMPT_BUDGET_BYTES =
|
|
1676
|
+
const UI_CATALOG_PROMPT_BUDGET_BYTES = CATALOG_PROMPT_MAX;
|
|
1237
1677
|
/** A2UI 协议公共类型(与 @a2ui/web_core 的 common-types 同一套定义) */
|
|
1238
1678
|
const A2UI_COMMON_TYPES = "https://a2ui.org/specification/v0_9/common_types.json";
|
|
1239
1679
|
const CHILD_LIST_REF = `${A2UI_COMMON_TYPES}#/$defs/ChildList`;
|
|
@@ -1568,7 +2008,8 @@ function renderPie(chart, svg, doc) {
|
|
|
1568
2008
|
});
|
|
1569
2009
|
}
|
|
1570
2010
|
/**
|
|
1571
|
-
* ChartSpec → SVG(bar 等宽柱 + 值标签;line 折线 + 点;pie 扇形 +
|
|
2011
|
+
* ChartSpec → SVG(bar 等宽柱 + 值标签;line 折线 + 点;pie 扇形 + 图例;空数据容错)。
|
|
2012
|
+
* mini 档只有这三种画法,其余类型走最接近的一种并在图内注明是近似呈现。
|
|
1572
2013
|
* @stable
|
|
1573
2014
|
*/
|
|
1574
2015
|
function renderMiniChart(chart, doc) {
|
|
@@ -1587,6 +2028,7 @@ function renderMiniChart(chart, doc) {
|
|
|
1587
2028
|
}));
|
|
1588
2029
|
switch (chart.kind) {
|
|
1589
2030
|
case "bar":
|
|
2031
|
+
case "stacked-bar":
|
|
1590
2032
|
renderBar(chart, svg, doc);
|
|
1591
2033
|
break;
|
|
1592
2034
|
case "line":
|
|
@@ -1595,7 +2037,11 @@ function renderMiniChart(chart, doc) {
|
|
|
1595
2037
|
case "pie":
|
|
1596
2038
|
renderPie(chart, svg, doc);
|
|
1597
2039
|
break;
|
|
2040
|
+
default:
|
|
2041
|
+
renderLine(chart, svg, doc);
|
|
2042
|
+
break;
|
|
1598
2043
|
}
|
|
2044
|
+
if (chart.kind !== "bar" && chart.kind !== "line" && chart.kind !== "pie") svg.appendChild(text(doc, WIDTH / 2, HEIGHT - 4, `Approximate rendering of "${chart.kind}"`, { "text-anchor": "middle" }));
|
|
1599
2045
|
return svg;
|
|
1600
2046
|
}
|
|
1601
2047
|
/** A2UI/OpenUI 降级:chart → table(labels 为首列,series 各为一列) */
|
|
@@ -1606,6 +2052,18 @@ function chartToTable(chart) {
|
|
|
1606
2052
|
rows: chart.labels.map((label, li) => [label, ...chart.series.map((s) => s.data[li] ?? null)])
|
|
1607
2053
|
};
|
|
1608
2054
|
}
|
|
2055
|
+
/**
|
|
2056
|
+
* `Gauge` 的百分比换算(0.11.0 分册 19 FR-19.2)。
|
|
2057
|
+
*
|
|
2058
|
+
* catalog 的 React `SpecGauge` 与文档面的预置组件都读这一份:
|
|
2059
|
+
* 换算里有两个容易各写各的判断——`max === min` 时不能除零、越界值要夹紧——
|
|
2060
|
+
* 两处各写一遍就会在边界值上给出不同的条。
|
|
2061
|
+
*/
|
|
2062
|
+
function gaugePercent(value, min, max) {
|
|
2063
|
+
const span = max - min;
|
|
2064
|
+
if (span <= 0) return 0;
|
|
2065
|
+
return Math.max(0, Math.min(100, (value - min) / span * 100));
|
|
2066
|
+
}
|
|
1609
2067
|
/** RenderBlock → DOM(markdown/json/table/image/file/chart 六种) */
|
|
1610
2068
|
function renderBlocks(container, blocks, doc) {
|
|
1611
2069
|
for (const block of blocks) switch (block.type) {
|
|
@@ -1683,6 +2141,154 @@ function renderRenderResult(request, doc) {
|
|
|
1683
2141
|
renderBlocks(root, request.blocks, doc);
|
|
1684
2142
|
return root;
|
|
1685
2143
|
}
|
|
2144
|
+
/**
|
|
2145
|
+
* viewer 的宿主预置组件(0.11.0 分册 18 FR-18.5 / 分册 19)。
|
|
2146
|
+
*
|
|
2147
|
+
* 技能**不写 JS**:文档 HTML 里只留占位,由这里挂载真实组件。
|
|
2148
|
+
* 交互只落在图表上——表格排序的例子已作废(分册 29 §2 登记 #13)。
|
|
2149
|
+
*
|
|
2150
|
+
* 三条硬约束:
|
|
2151
|
+
* - props 用 **catalog 自己的 zod schema** 解析,同形因此是机制上成立的,
|
|
2152
|
+
* 不是靠一句「记得同步」的注释(FR-19.2);
|
|
2153
|
+
* - 值一律经 `textContent` 落地,**永不** `innerHTML`(FR-19.4 / AC-19.5);
|
|
2154
|
+
* - 白名单外的名字只渲染降级提示,**不触发任何按名 import**(FR-19.3)。
|
|
2155
|
+
*/
|
|
2156
|
+
/** 占位元素的属性名,模板与挂载方共用(FR-19.1 的契约) */
|
|
2157
|
+
const VIEWER_COMPONENT_ATTR = "data-webskill-component";
|
|
2158
|
+
const VIEWER_PROPS_ATTR = "data-webskill-props";
|
|
2159
|
+
/** 降级提示的标记;模板 CSS 与判据都认这个属性,不认某个 class 名 */
|
|
2160
|
+
const VIEWER_FALLBACK_ATTR = "data-webskill-fallback";
|
|
2161
|
+
/** 文档面可用的组件白名单(FR-19.3)。字面量数组——不接受任何按名动态加载 */
|
|
2162
|
+
const DOCUMENT_COMPONENTS = [
|
|
2163
|
+
"Chart",
|
|
2164
|
+
"Table",
|
|
2165
|
+
"Metric",
|
|
2166
|
+
"Gauge",
|
|
2167
|
+
"KeyValue"
|
|
2168
|
+
];
|
|
2169
|
+
const WHITELIST = new Set(DOCUMENT_COMPONENTS);
|
|
2170
|
+
/**
|
|
2171
|
+
* 扫描 `root` 下的占位并挂载预置组件。
|
|
2172
|
+
*
|
|
2173
|
+
* 逐个占位独立处理:一个坏占位只降级它自己,不会让整篇文档变空白。
|
|
2174
|
+
*/
|
|
2175
|
+
function mountViewerComponents(root, options) {
|
|
2176
|
+
const handles = [];
|
|
2177
|
+
for (const node of Array.from(root.querySelectorAll(`[${VIEWER_COMPONENT_ATTR}]`))) {
|
|
2178
|
+
const element = node;
|
|
2179
|
+
const name = element.getAttribute("data-webskill-component") ?? "";
|
|
2180
|
+
if (!WHITELIST.has(name)) {
|
|
2181
|
+
fallback(element, `Unsupported component "${name}"`);
|
|
2182
|
+
continue;
|
|
2183
|
+
}
|
|
2184
|
+
const props = readProps(element, name);
|
|
2185
|
+
if (!props) continue;
|
|
2186
|
+
const handle = render(element, name, props, options?.fontSizes);
|
|
2187
|
+
if (handle) handles.push(handle);
|
|
2188
|
+
}
|
|
2189
|
+
return { dispose: () => {
|
|
2190
|
+
for (const handle of handles) handle.dispose();
|
|
2191
|
+
handles.length = 0;
|
|
2192
|
+
} };
|
|
2193
|
+
}
|
|
2194
|
+
/** 用 catalog 的 schema 解析:多余字段被丢弃,缺省值与 catalog 完全一致 */
|
|
2195
|
+
function readProps(element, name) {
|
|
2196
|
+
const raw = element.getAttribute(VIEWER_PROPS_ATTR);
|
|
2197
|
+
if (raw === null || raw === "") {
|
|
2198
|
+
fallback(element, `Component "${name}" is missing ${VIEWER_PROPS_ATTR}`);
|
|
2199
|
+
return;
|
|
2200
|
+
}
|
|
2201
|
+
let parsed;
|
|
2202
|
+
try {
|
|
2203
|
+
parsed = JSON.parse(raw);
|
|
2204
|
+
} catch {
|
|
2205
|
+
fallback(element, `Component "${name}" has invalid props JSON`);
|
|
2206
|
+
return;
|
|
2207
|
+
}
|
|
2208
|
+
const schema = uiCatalog.component(name)?.props;
|
|
2209
|
+
if (!schema) {
|
|
2210
|
+
fallback(element, `Component "${name}" is not declared in the catalog`);
|
|
2211
|
+
return;
|
|
2212
|
+
}
|
|
2213
|
+
const result = schema.safeParse(parsed);
|
|
2214
|
+
if (!result.success) {
|
|
2215
|
+
fallback(element, `Component "${name}" has props the catalog rejects`);
|
|
2216
|
+
return;
|
|
2217
|
+
}
|
|
2218
|
+
return result.data;
|
|
2219
|
+
}
|
|
2220
|
+
function render(element, name, props, fontSizes) {
|
|
2221
|
+
const doc = element.ownerDocument;
|
|
2222
|
+
element.replaceChildren();
|
|
2223
|
+
switch (name) {
|
|
2224
|
+
case "Chart": {
|
|
2225
|
+
const handle = mountEchart(element);
|
|
2226
|
+
handle.setChart(chartSpecFromProps(props), fontSizes);
|
|
2227
|
+
return handle;
|
|
2228
|
+
}
|
|
2229
|
+
case "Table":
|
|
2230
|
+
renderBlocks(element, [{
|
|
2231
|
+
type: "table",
|
|
2232
|
+
columns: props["columns"],
|
|
2233
|
+
rows: props["rows"]
|
|
2234
|
+
}], doc);
|
|
2235
|
+
return;
|
|
2236
|
+
case "Metric": {
|
|
2237
|
+
append(doc, element, "span", String(props["label"]), { "data-webskill-metric": "label" });
|
|
2238
|
+
append(doc, element, "strong", String(props["value"]), { "data-webskill-metric": "value" });
|
|
2239
|
+
const change = props["change"];
|
|
2240
|
+
if (typeof change === "string") append(doc, element, "span", change, {
|
|
2241
|
+
"data-webskill-metric": "change",
|
|
2242
|
+
"data-trend": String(props["trend"] ?? "neutral")
|
|
2243
|
+
});
|
|
2244
|
+
return;
|
|
2245
|
+
}
|
|
2246
|
+
case "Gauge": {
|
|
2247
|
+
const value = props["value"];
|
|
2248
|
+
const min = props["min"];
|
|
2249
|
+
const max = props["max"];
|
|
2250
|
+
const label = typeof props["label"] === "string" ? props["label"] : "";
|
|
2251
|
+
element.setAttribute("data-tone", String(props["tone"]));
|
|
2252
|
+
if (label !== "") append(doc, element, "span", label, { "data-webskill-gauge": "label" });
|
|
2253
|
+
const fill = append(doc, append(doc, element, "div", "", {
|
|
2254
|
+
role: "meter",
|
|
2255
|
+
"aria-label": label === "" ? "Gauge" : label,
|
|
2256
|
+
"aria-valuenow": String(value),
|
|
2257
|
+
"aria-valuemin": String(min),
|
|
2258
|
+
"aria-valuemax": String(max),
|
|
2259
|
+
"data-webskill-gauge": "track"
|
|
2260
|
+
}), "div", "", { "data-webskill-gauge": "fill" });
|
|
2261
|
+
fill.style.width = `${gaugePercent(value, min, max)}%`;
|
|
2262
|
+
append(doc, element, "span", String(value), { "data-webskill-gauge": "value" });
|
|
2263
|
+
return;
|
|
2264
|
+
}
|
|
2265
|
+
case "KeyValue": {
|
|
2266
|
+
const dl = append(doc, element, "dl", "", {});
|
|
2267
|
+
for (const item of props["items"]) {
|
|
2268
|
+
append(doc, dl, "dt", item.label, {});
|
|
2269
|
+
append(doc, dl, "dd", item.value, {});
|
|
2270
|
+
}
|
|
2271
|
+
return;
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
function append(doc, parent, tag, text, attrs) {
|
|
2276
|
+
const el = doc.createElement(tag);
|
|
2277
|
+
for (const [key, value] of Object.entries(attrs)) el.setAttribute(key, value);
|
|
2278
|
+
if (text !== "") el.textContent = text;
|
|
2279
|
+
parent.appendChild(el);
|
|
2280
|
+
return el;
|
|
2281
|
+
}
|
|
2282
|
+
/**
|
|
2283
|
+
* 可见降级(AC-19.4)。
|
|
2284
|
+
*
|
|
2285
|
+
* 静默留空是最坏的结果:文档看着「渲染成功」,作者却不知道自己写错了组件名。
|
|
2286
|
+
* 原有内容整体替换,避免半截渲染与提示并存。
|
|
2287
|
+
*/
|
|
2288
|
+
function fallback(element, reason) {
|
|
2289
|
+
element.setAttribute(VIEWER_FALLBACK_ATTR, "true");
|
|
2290
|
+
element.textContent = reason;
|
|
2291
|
+
}
|
|
1686
2292
|
/** 最小样式(BEM class,应用可整体覆写) */
|
|
1687
2293
|
const WEBSKILL_STYLES_CSS = `
|
|
1688
2294
|
.webskill-form { display: block; padding: 12px; border: 1px solid #d0d0d0; border-radius: 8px; font-family: system-ui, sans-serif; max-width: 420px; }
|
|
@@ -1886,7 +2492,7 @@ var WebFormBridge = class {
|
|
|
1886
2492
|
input = textarea;
|
|
1887
2493
|
} else {
|
|
1888
2494
|
const textInput = doc.createElement("input");
|
|
1889
|
-
textInput.type = control.control === "number" ? "number" : "text";
|
|
2495
|
+
textInput.type = control.control === "number" ? "number" : control.control === "date" ? "date" : "text";
|
|
1890
2496
|
textInput.className = "webskill-form__input";
|
|
1891
2497
|
if (control.defaultValue !== void 0) textInput.value = String(control.defaultValue);
|
|
1892
2498
|
input = textInput;
|
|
@@ -2433,7 +3039,8 @@ const FIELD_TYPE = {
|
|
|
2433
3039
|
select: "select",
|
|
2434
3040
|
textarea: "textarea",
|
|
2435
3041
|
file: "file",
|
|
2436
|
-
password: "password"
|
|
3042
|
+
password: "password",
|
|
3043
|
+
date: "date"
|
|
2437
3044
|
};
|
|
2438
3045
|
function toOptions(options) {
|
|
2439
3046
|
return (options ?? []).map((option) => ({
|
|
@@ -2591,6 +3198,7 @@ function createUiCatalogToolSource(options = {}) {
|
|
|
2591
3198
|
systemPrompt: () => Promise.resolve(`${catalog.toPrompt()}\n\n${NO_DUPLICATE_RENDER}`),
|
|
2592
3199
|
listToolSpecs: () => Promise.resolve(presets.length > 0 ? [renderTool(), presetTool()] : [renderTool()]),
|
|
2593
3200
|
canHandle: (name) => name === "render_ui" || presets.length > 0 && name === "describe_ui_preset",
|
|
3201
|
+
argCaptureTrust: (name) => name === "render_ui" || name === "describe_ui_preset" ? { tier: "reviewed" } : void 0,
|
|
2594
3202
|
call: (name, args) => {
|
|
2595
3203
|
if (name === "describe_ui_preset") {
|
|
2596
3204
|
const requested = args["preset"];
|
|
@@ -2873,7 +3481,7 @@ function fromA2uiSpecAction(event) {
|
|
|
2873
3481
|
*/
|
|
2874
3482
|
async function loadWebSkillLitCatalog() {
|
|
2875
3483
|
try {
|
|
2876
|
-
const { webskillLitCatalog } = await import("./webskillLitCatalog-
|
|
3484
|
+
const { webskillLitCatalog } = await import("./webskillLitCatalog-D_zCqeQF-C9lrvvMr.js");
|
|
2877
3485
|
return webskillLitCatalog();
|
|
2878
3486
|
} catch (cause) {
|
|
2879
3487
|
throw new WebSkillError("UI_UNAVAILABLE", "The WebSkill A2UI catalog is unavailable; install @a2ui/lit, @a2ui/web_core and lit to render catalog surfaces with A2UI", cause);
|
|
@@ -2898,4 +3506,4 @@ async function loadOpenUiPeers() {
|
|
|
2898
3506
|
}
|
|
2899
3507
|
|
|
2900
3508
|
//#endregion
|
|
2901
|
-
export {
|
|
3509
|
+
export { uiPreset as $, ensureStyles as A, mountViewerComponents as B, applySuggestion as C, resolveSurfaceFormTexts as Ct, collectSpecActions as D, uiCatalog as Dt, collectScopedValues as E, toEchartsOption as Et, fromVercelToolResult as F, renderRenderResult as G, qualifyFieldName as H, gaugePercent as I, toA2uiSurfaceAction as J, resolveColumnWidths as K, interactionToUiSpec as L, fromA2uiSpecAction as M, fromA2uiSurfaceAction as N, collectValues as O, fromUiSurfaceActionDispatch as P, toVercelToolInvocation as Q, loadOpenUiPeers as R, WebFormBridge as S, resolveInteractionTexts as St, collectFormScopes as T, shapeInteractionValue as Tt, renderBlocks as U, normalizeColumnWidths as V, renderMiniChart as W, toOpenUiSpecLang as X, toJsonRenderSpec as Y, toUiSurfaceActionDispatch as Z, VIEWER_FALLBACK_ATTR as _, defineUiCatalog as _t, A2UI_VERSION as a, DEFAULT_CHART_FONT_SIZES as at, WEBSKILL_STYLES_CSS as b, renderMiniMarkdown as bt, DOCUMENT_COMPONENTS as c, DEFAULT_SURFACE_HOST_CONTROL_TEXTS as ct, SPEC_TABLE_MIN_COLUMN_VAR as d, UI_CATALOG_PROMPT_BUDGET_BYTES as dt, A2UI_CHILDREN_PROP as et, SPEC_TABLE_MIN_COLUMN_WIDTH as f, WEBSKILL_A2UI_CATALOG_ID as ft, VIEWER_COMPONENT_ATTR as g, chartSpecFromProps as gt, VERCEL_INTERACTION_TOOL_NAME as h, buildA2uiCatalogDefinition as ht, A2UI_SURFACE_ACTION as i, CATALOG_SCHEMA_MAX as it, evaluateFieldCondition as j, createUiCatalogToolSource as k, MAX_CONDITION_DEPTH as l, PLANNED_INCREMENT as lt, UI_PRESET_NAMES as m, a2uiComponentShapes as mt, A2UI_SPEC_ACTION as n, CATALOG_BUDGET_STAGE as nt, CHART_PALETTE as o, DEFAULT_INTERACTION_TEXTS as ot, UI_PRESETS as p, a2uiComponentSchema as pt, toA2uiSpecMessages as q, A2UI_SPEC_FORM_PATH as r, CATALOG_PROMPT_MAX as rt, DESCRIBE_UI_PRESET_TOOL as s, DEFAULT_SURFACE_FORM_TEXTS as st, A2UI_BASIC_CATALOG_ID as t, A2UI_COMMON_TYPES as tt, RENDER_UI_TOOL as u, UI_CATALOG_GROUPS as ut, VIEWER_PROPS_ATTR as v, interactionToFormModel as vt, chartToTable as w, resolveSurfaceHostControlTexts as wt, WEBSKILL_SURFACE_ACTION as x, resolveChartFontSizes as xt, VercelUiBridge as y, mountEchart as yt, loadWebSkillLitCatalog as z };
|