@webskill/sdk 0.2.5 → 0.2.7
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/browser.d.ts +2 -2
- package/dist/browser.js +1 -1
- package/dist/catalogComponents-C_V39rbF-B94i0fW7.js +129139 -0
- package/dist/{dist-B77plHjw.js → dist-BdOW8N4V.js} +505 -35
- package/dist/{dist-Chk8iB-E.js → dist-CcMIUXeZ.js} +1 -1
- package/dist/{dist-DNSG9FqC.js → dist-CtBLBbEz.js} +1186 -123
- package/dist/dist-DxyxmXPU-DGC-dpXf.js +5621 -0
- package/dist/echarts-DhNm2ene.js +78816 -0
- package/dist/governance.d.ts +94 -93
- package/dist/governance.js +205 -194
- package/dist/{index-CsDJvYGV.d.ts → index-BHL5FWGw.d.ts} +2 -2
- package/dist/index-BNQNSDKg.d.ts +580 -0
- package/dist/{index-DfINBEOy.d.ts → index-BpIK7tJM.d.ts} +35 -5
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/jsonRenderRegistry-9GrWP_hE-CMkryt1H.js +2468 -0
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +1 -1
- package/dist/node.d.ts +3 -3
- package/dist/node.js +2 -2
- package/dist/openUiLibrary-B8-Cvou9-Cu1QZqoT.js +3785 -0
- package/dist/rolldown-runtime-BOF7iYI8.js +55 -0
- package/dist/testing.d.ts +1 -1
- package/dist/{types-CKm5G_eQ-krKWW8WV.d.ts → types-7fnqDVrf-BnRQjVU3.d.ts} +127 -2
- package/dist/ui-react.d.ts +138 -2
- package/dist/ui-react.js +6935 -3
- package/dist/ui-vue.d.ts +1 -1
- package/dist/ui-vue.js +1 -1
- package/dist/ui.d.ts +3 -203
- package/dist/ui.js +3 -3
- package/dist/webskillLitCatalog-CNaUpasU-CfSRvqCZ.js +1601 -0
- package/package.json +2 -1
|
@@ -1,127 +1,7 @@
|
|
|
1
1
|
import { u as WebSkillError } from "./dist-BQzncxXg.js";
|
|
2
|
+
import { toJSONSchema, z } from "zod";
|
|
2
3
|
|
|
3
|
-
//#region ../ui/dist/
|
|
4
|
-
/** 提交值按请求类型归形(WebFormBridge 与框架组件库共享单一来源) */
|
|
5
|
-
function shapeInteractionValue(model, values) {
|
|
6
|
-
switch (model.kind) {
|
|
7
|
-
case "ask": return values["answer"];
|
|
8
|
-
case "confirm": return values["confirmed"] === true;
|
|
9
|
-
case "select": return values["selected"];
|
|
10
|
-
case "authorize": return true;
|
|
11
|
-
case "form": return values;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
/** 五类 InteractionRequest → 统一中间模型(框架无关) */
|
|
15
|
-
function interactionToFormModel(request) {
|
|
16
|
-
switch (request.type) {
|
|
17
|
-
case "ask": return {
|
|
18
|
-
kind: "ask",
|
|
19
|
-
message: request.message,
|
|
20
|
-
controls: [{
|
|
21
|
-
name: "answer",
|
|
22
|
-
label: request.message,
|
|
23
|
-
control: "text",
|
|
24
|
-
required: true
|
|
25
|
-
}],
|
|
26
|
-
submitLabel: "Submit",
|
|
27
|
-
cancelLabel: "Cancel"
|
|
28
|
-
};
|
|
29
|
-
case "confirm": return {
|
|
30
|
-
kind: "confirm",
|
|
31
|
-
message: request.message,
|
|
32
|
-
controls: [{
|
|
33
|
-
name: "confirmed",
|
|
34
|
-
label: request.message,
|
|
35
|
-
control: "boolean",
|
|
36
|
-
defaultValue: request.defaultValue ?? true
|
|
37
|
-
}],
|
|
38
|
-
submitLabel: "Confirm",
|
|
39
|
-
cancelLabel: "Cancel"
|
|
40
|
-
};
|
|
41
|
-
case "form": return {
|
|
42
|
-
kind: "form",
|
|
43
|
-
...request.title ? { title: request.title } : {},
|
|
44
|
-
controls: request.fields.map((f) => ({
|
|
45
|
-
name: f.name,
|
|
46
|
-
label: f.label,
|
|
47
|
-
control: f.type,
|
|
48
|
-
...f.required ? { required: true } : {},
|
|
49
|
-
...f.description ? { description: f.description } : {},
|
|
50
|
-
...f.defaultValue !== void 0 ? { defaultValue: f.defaultValue } : {},
|
|
51
|
-
...f.options ? { options: f.options } : {}
|
|
52
|
-
})),
|
|
53
|
-
submitLabel: "Submit",
|
|
54
|
-
cancelLabel: "Cancel"
|
|
55
|
-
};
|
|
56
|
-
case "select": return {
|
|
57
|
-
kind: "select",
|
|
58
|
-
message: request.message,
|
|
59
|
-
controls: [{
|
|
60
|
-
name: "selected",
|
|
61
|
-
label: request.message,
|
|
62
|
-
control: "select",
|
|
63
|
-
required: true,
|
|
64
|
-
options: request.options
|
|
65
|
-
}],
|
|
66
|
-
submitLabel: "Select",
|
|
67
|
-
cancelLabel: "Cancel"
|
|
68
|
-
};
|
|
69
|
-
case "authorize": return {
|
|
70
|
-
kind: "authorize",
|
|
71
|
-
title: "Authorization required",
|
|
72
|
-
message: request.message,
|
|
73
|
-
controls: [],
|
|
74
|
-
submitLabel: "Allow",
|
|
75
|
-
cancelLabel: "Deny"
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
const isEmpty = (v) => v === void 0 || v === "";
|
|
80
|
-
/**
|
|
81
|
-
* 控件名 → 属性选择器:优先 CSS.escape(标识符形式,免引号转义);
|
|
82
|
-
* 无 CSS.escape 的环境退化为引号包裹 + 转义反斜杠/双引号(防选择器注入崩溃)
|
|
83
|
-
*/
|
|
84
|
-
function controlSelector(name) {
|
|
85
|
-
if (typeof CSS !== "undefined" && typeof CSS.escape === "function") return `[data-webskill-control=${CSS.escape(name)}]`;
|
|
86
|
-
return `[data-webskill-control="${name.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"]`;
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* 严格类型化控件值收集:
|
|
90
|
-
* - number 空串 → undefined(非 0);非数值输入(NaN)按缺失处理(required 时入 missingRequired)
|
|
91
|
-
* - select 用 option 值本身经 JSON 编码比对命中(对象值可正确命中)
|
|
92
|
-
* - required 空值列入 missingRequired(由调用方阻止提交并标记)
|
|
93
|
-
*/
|
|
94
|
-
function collectValues(controls, container) {
|
|
95
|
-
const values = {};
|
|
96
|
-
const missingRequired = [];
|
|
97
|
-
for (const control of controls) {
|
|
98
|
-
const el = container.querySelector(controlSelector(control.name));
|
|
99
|
-
let value;
|
|
100
|
-
if (el === null) value = void 0;
|
|
101
|
-
else if (control.control === "boolean") value = el.checked;
|
|
102
|
-
else if (control.control === "number") {
|
|
103
|
-
const raw = el.value.trim();
|
|
104
|
-
if (raw === "") value = void 0;
|
|
105
|
-
else {
|
|
106
|
-
const n = Number(raw);
|
|
107
|
-
value = Number.isNaN(n) ? void 0 : n;
|
|
108
|
-
}
|
|
109
|
-
} else if (control.control === "select") {
|
|
110
|
-
const attr = el.value;
|
|
111
|
-
const hit = control.options?.find((o) => JSON.stringify(o.value) === attr);
|
|
112
|
-
value = hit ? hit.value : attr === "" ? void 0 : attr;
|
|
113
|
-
} else {
|
|
114
|
-
const raw = el.value;
|
|
115
|
-
value = raw === "" ? void 0 : raw;
|
|
116
|
-
}
|
|
117
|
-
values[control.name] = value;
|
|
118
|
-
if (control.required && isEmpty(value)) missingRequired.push(control.name);
|
|
119
|
-
}
|
|
120
|
-
return {
|
|
121
|
-
values,
|
|
122
|
-
missingRequired
|
|
123
|
-
};
|
|
124
|
-
}
|
|
4
|
+
//#region ../ui/dist/webskillCatalog-B4pZvE74.js
|
|
125
5
|
/**
|
|
126
6
|
* 最小子集 markdown → DOM:标题(#..###)、无序列表、代码块(```)、加粗、链接。
|
|
127
7
|
* 全部内容经 textContent 写入,用户内容天然转义防 HTML 注入。
|
|
@@ -414,6 +294,779 @@ function chartToTable(chart) {
|
|
|
414
294
|
rows: chart.labels.map((label, li) => [label, ...chart.series.map((s) => s.data[li] ?? null)])
|
|
415
295
|
};
|
|
416
296
|
}
|
|
297
|
+
function componentSchema(def) {
|
|
298
|
+
const props = toJSONSchema(def.props, { io: "input" });
|
|
299
|
+
const node = {
|
|
300
|
+
type: "object",
|
|
301
|
+
description: def.description,
|
|
302
|
+
properties: {
|
|
303
|
+
component: {
|
|
304
|
+
type: "string",
|
|
305
|
+
const: def.name
|
|
306
|
+
},
|
|
307
|
+
id: {
|
|
308
|
+
type: "string",
|
|
309
|
+
description: "Stable node id, reused on replay"
|
|
310
|
+
},
|
|
311
|
+
props
|
|
312
|
+
},
|
|
313
|
+
required: ["component"]
|
|
314
|
+
};
|
|
315
|
+
if (def.children !== void 0) node.properties = {
|
|
316
|
+
...node.properties,
|
|
317
|
+
children: {
|
|
318
|
+
type: "array",
|
|
319
|
+
description: def.children === "any" ? "Any catalog component" : `One of: ${[...def.children].join(", ")}`,
|
|
320
|
+
items: { $ref: "#" }
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
return node;
|
|
324
|
+
}
|
|
325
|
+
function isRecord$1(value) {
|
|
326
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
327
|
+
}
|
|
328
|
+
function typeOf(schema) {
|
|
329
|
+
if (Array.isArray(schema.enum)) return schema.enum.map((option) => JSON.stringify(option)).join(" | ");
|
|
330
|
+
if (schema["const"] !== void 0) return JSON.stringify(schema["const"]);
|
|
331
|
+
const union = schema["anyOf"] ?? schema["oneOf"];
|
|
332
|
+
if (Array.isArray(union)) return union.map(typeOf).join(" | ");
|
|
333
|
+
if (schema.type === "array" && schema.items) return `${typeOf(schema.items)}[]`;
|
|
334
|
+
return typeof schema.type === "string" ? schema.type : "unknown";
|
|
335
|
+
}
|
|
336
|
+
function describeProps(def) {
|
|
337
|
+
const schema = toJSONSchema(def.props, { io: "input" });
|
|
338
|
+
const properties = schema.properties ?? {};
|
|
339
|
+
const required = new Set(schema.required ?? []);
|
|
340
|
+
const entries = Object.entries(properties).map(([key, value]) => `${key}${required.has(key) ? "" : "?"}: ${typeOf(value)}`);
|
|
341
|
+
return entries.length === 0 ? "—" : entries.join(", ");
|
|
342
|
+
}
|
|
343
|
+
function promptFor(catalog, options) {
|
|
344
|
+
const lines = [(options.mode === "tool-description" ? "" : `# ${catalog.title} (${catalog.version})\n\n`) + catalog.instructions.trim(), ""];
|
|
345
|
+
const groups = /* @__PURE__ */ new Map();
|
|
346
|
+
for (const def of catalog.components) groups.set(def.group, [...groups.get(def.group) ?? [], def]);
|
|
347
|
+
for (const [group, defs] of groups) {
|
|
348
|
+
lines.push(`## ${group}`, "");
|
|
349
|
+
for (const def of defs) {
|
|
350
|
+
lines.push(`### ${def.name}`, def.description, "", `props: ${describeProps(def)}`);
|
|
351
|
+
if (def.children !== void 0) lines.push(`children: ${def.children === "any" ? "any component" : [...def.children].join(" | ")}`);
|
|
352
|
+
for (const constraint of def.constraints ?? []) lines.push(`- ${constraint}`);
|
|
353
|
+
if (def.example) lines.push("", "```json", JSON.stringify(def.example), "```");
|
|
354
|
+
lines.push("");
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (catalog.actions.length > 0) {
|
|
358
|
+
lines.push("## actions", "");
|
|
359
|
+
for (const action of catalog.actions) lines.push(`- \`${action.name}\` — ${action.description}`);
|
|
360
|
+
lines.push("");
|
|
361
|
+
}
|
|
362
|
+
return lines.join("\n").trimEnd();
|
|
363
|
+
}
|
|
364
|
+
function validateNode(catalog, byName, node, path, issues) {
|
|
365
|
+
if (!isRecord$1(node)) {
|
|
366
|
+
issues.push({
|
|
367
|
+
path,
|
|
368
|
+
message: "Node must be an object"
|
|
369
|
+
});
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const name = node["component"];
|
|
373
|
+
if (typeof name !== "string") {
|
|
374
|
+
issues.push({
|
|
375
|
+
path,
|
|
376
|
+
message: "Node is missing a \"component\" name"
|
|
377
|
+
});
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
const def = byName.get(name);
|
|
381
|
+
if (!def) {
|
|
382
|
+
issues.push({
|
|
383
|
+
path,
|
|
384
|
+
message: `Unknown component "${name}"; the catalog declares: ${catalog.components.map((c) => c.name).join(", ")}`
|
|
385
|
+
});
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const parsed = def.props.safeParse(node["props"] ?? {});
|
|
389
|
+
if (!parsed.success) for (const issue of parsed.error.issues) issues.push({
|
|
390
|
+
path: `${path}.props${issue.path.map((part) => `.${String(part)}`).join("")}`,
|
|
391
|
+
message: issue.message
|
|
392
|
+
});
|
|
393
|
+
const children = node["children"];
|
|
394
|
+
if (children === void 0) return;
|
|
395
|
+
if (!Array.isArray(children)) {
|
|
396
|
+
issues.push({
|
|
397
|
+
path: `${path}.children`,
|
|
398
|
+
message: "children must be an array"
|
|
399
|
+
});
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
if (def.children === void 0) {
|
|
403
|
+
issues.push({
|
|
404
|
+
path: `${path}.children`,
|
|
405
|
+
message: `Component "${name}" is a leaf and cannot have children`
|
|
406
|
+
});
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const allowed = def.children;
|
|
410
|
+
children.forEach((child, index) => {
|
|
411
|
+
const childPath = `${path}.children[${index}]`;
|
|
412
|
+
if (allowed !== "any" && isRecord$1(child) && typeof child["component"] === "string") {
|
|
413
|
+
if (!allowed.includes(child["component"])) {
|
|
414
|
+
issues.push({
|
|
415
|
+
path: childPath,
|
|
416
|
+
message: `Component "${name}" only accepts children: ${[...allowed].join(", ")}`
|
|
417
|
+
});
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
validateNode(catalog, byName, child, childPath, issues);
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* 同一份定义派生出 JSON Schema / prompt / validator 三种投影,
|
|
426
|
+
* 不允许任何一方手写副本(21 号文档 §1)。
|
|
427
|
+
*/
|
|
428
|
+
function defineUiCatalog(input) {
|
|
429
|
+
const byName = new Map(input.components.map((def) => [def.name, def]));
|
|
430
|
+
if (byName.size !== input.components.length) {
|
|
431
|
+
const seen = /* @__PURE__ */ new Set();
|
|
432
|
+
const duplicate = input.components.find((def) => seen.has(def.name) ? true : (seen.add(def.name), false));
|
|
433
|
+
throw new Error(`Duplicate component name "${duplicate?.name}" in UI catalog "${input.id}"`);
|
|
434
|
+
}
|
|
435
|
+
return {
|
|
436
|
+
...input,
|
|
437
|
+
componentNames: input.components.map((def) => def.name),
|
|
438
|
+
actionNames: input.actions.map((action) => action.name),
|
|
439
|
+
component: (name) => byName.get(name),
|
|
440
|
+
toJsonSchema: () => ({
|
|
441
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
442
|
+
$id: input.id,
|
|
443
|
+
title: input.title,
|
|
444
|
+
description: input.instructions,
|
|
445
|
+
oneOf: input.components.map(componentSchema)
|
|
446
|
+
}),
|
|
447
|
+
toPrompt: (options = {}) => promptFor(input, options),
|
|
448
|
+
validate: (spec) => {
|
|
449
|
+
const issues = [];
|
|
450
|
+
validateNode(input, byName, spec, "root", issues);
|
|
451
|
+
return issues.length === 0 ? { ok: true } : {
|
|
452
|
+
ok: false,
|
|
453
|
+
issues
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
const tone = z.enum([
|
|
459
|
+
"neutral",
|
|
460
|
+
"success",
|
|
461
|
+
"warning",
|
|
462
|
+
"info",
|
|
463
|
+
"destructive"
|
|
464
|
+
]);
|
|
465
|
+
const actionRef = z.object({
|
|
466
|
+
/** catalog actions 里声明过的名字 */
|
|
467
|
+
action: z.string().describe("Action name declared by the catalog"),
|
|
468
|
+
label: z.string(),
|
|
469
|
+
disabled: z.boolean().optional()
|
|
470
|
+
});
|
|
471
|
+
const CONTENT = [
|
|
472
|
+
"Stack",
|
|
473
|
+
"Card",
|
|
474
|
+
"Separator",
|
|
475
|
+
"Heading",
|
|
476
|
+
"Text",
|
|
477
|
+
"Markdown",
|
|
478
|
+
"Badge"
|
|
479
|
+
];
|
|
480
|
+
const DATA = [
|
|
481
|
+
"Metric",
|
|
482
|
+
"Table",
|
|
483
|
+
"Chart",
|
|
484
|
+
"FileLink"
|
|
485
|
+
];
|
|
486
|
+
const INPUT = [
|
|
487
|
+
"Form",
|
|
488
|
+
"Field",
|
|
489
|
+
"Button"
|
|
490
|
+
];
|
|
491
|
+
/**
|
|
492
|
+
* v3 首发 catalog:覆盖既有 `UiSurface` 六类的全部表达力,先窄后宽。
|
|
493
|
+
* 只依赖 zod —— 不 import 任何 UI framework(21 号文档 §5.1)。
|
|
494
|
+
*/
|
|
495
|
+
const uiCatalog = defineUiCatalog({
|
|
496
|
+
id: "https://webskill.dev/catalogs/v1",
|
|
497
|
+
version: "1.0.0",
|
|
498
|
+
title: "WebSkill AI Component Catalog",
|
|
499
|
+
instructions: [
|
|
500
|
+
"Compose the answer UI from the components below. Rules:",
|
|
501
|
+
"",
|
|
502
|
+
"- The root node of a surface must be `Stack` or `Card`.",
|
|
503
|
+
"- Ask for user input with `Form` + `Field`; never describe an input in `Text`.",
|
|
504
|
+
"- A `Button`'s `action` must be one of the catalog actions.",
|
|
505
|
+
"- Never emit HTML, scripts or remote images.",
|
|
506
|
+
"- Prefer `Table` for many rows, `Chart` for trends, `Metric` for a single number.",
|
|
507
|
+
"- At most one `Form` per surface."
|
|
508
|
+
].join("\n"),
|
|
509
|
+
components: [
|
|
510
|
+
{
|
|
511
|
+
name: "Stack",
|
|
512
|
+
group: "layout",
|
|
513
|
+
description: "Vertical or horizontal container. Use it as the root of most surfaces.",
|
|
514
|
+
props: z.object({
|
|
515
|
+
direction: z.enum(["vertical", "horizontal"]).default("vertical"),
|
|
516
|
+
gap: z.enum([
|
|
517
|
+
"sm",
|
|
518
|
+
"md",
|
|
519
|
+
"lg"
|
|
520
|
+
]).default("md"),
|
|
521
|
+
align: z.enum([
|
|
522
|
+
"start",
|
|
523
|
+
"center",
|
|
524
|
+
"end",
|
|
525
|
+
"stretch"
|
|
526
|
+
]).optional()
|
|
527
|
+
}),
|
|
528
|
+
children: "any",
|
|
529
|
+
example: {
|
|
530
|
+
component: "Stack",
|
|
531
|
+
props: { direction: "vertical" },
|
|
532
|
+
children: [{
|
|
533
|
+
component: "Heading",
|
|
534
|
+
props: {
|
|
535
|
+
level: 2,
|
|
536
|
+
text: "Q4 results"
|
|
537
|
+
}
|
|
538
|
+
}]
|
|
539
|
+
}
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
name: "Card",
|
|
543
|
+
group: "layout",
|
|
544
|
+
description: "A titled section that groups related content.",
|
|
545
|
+
props: z.object({
|
|
546
|
+
title: z.string(),
|
|
547
|
+
description: z.string().optional()
|
|
548
|
+
}),
|
|
549
|
+
children: "any",
|
|
550
|
+
example: {
|
|
551
|
+
component: "Card",
|
|
552
|
+
props: { title: "Revenue" },
|
|
553
|
+
children: [{
|
|
554
|
+
component: "Metric",
|
|
555
|
+
props: {
|
|
556
|
+
label: "MRR",
|
|
557
|
+
value: "12,400"
|
|
558
|
+
}
|
|
559
|
+
}]
|
|
560
|
+
}
|
|
561
|
+
},
|
|
562
|
+
{
|
|
563
|
+
name: "Separator",
|
|
564
|
+
group: "layout",
|
|
565
|
+
description: "A horizontal rule between sections.",
|
|
566
|
+
props: z.object({}),
|
|
567
|
+
example: { component: "Separator" }
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
name: "Heading",
|
|
571
|
+
group: "content",
|
|
572
|
+
description: "Section heading. Level 2 is the largest allowed inside a message.",
|
|
573
|
+
props: z.object({
|
|
574
|
+
level: z.union([
|
|
575
|
+
z.literal(2),
|
|
576
|
+
z.literal(3),
|
|
577
|
+
z.literal(4)
|
|
578
|
+
]).default(3),
|
|
579
|
+
text: z.string()
|
|
580
|
+
}),
|
|
581
|
+
example: {
|
|
582
|
+
component: "Heading",
|
|
583
|
+
props: {
|
|
584
|
+
level: 3,
|
|
585
|
+
text: "Summary"
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
},
|
|
589
|
+
{
|
|
590
|
+
name: "Text",
|
|
591
|
+
group: "content",
|
|
592
|
+
description: "A paragraph of plain text.",
|
|
593
|
+
props: z.object({
|
|
594
|
+
text: z.string(),
|
|
595
|
+
tone: z.enum(["default", "muted"]).default("default")
|
|
596
|
+
}),
|
|
597
|
+
example: {
|
|
598
|
+
component: "Text",
|
|
599
|
+
props: { text: "Revenue grew 12% quarter over quarter." }
|
|
600
|
+
}
|
|
601
|
+
},
|
|
602
|
+
{
|
|
603
|
+
name: "Markdown",
|
|
604
|
+
group: "content",
|
|
605
|
+
description: "Restricted Markdown (no HTML). Use for lists and emphasis, not for tables.",
|
|
606
|
+
props: z.object({ text: z.string() }),
|
|
607
|
+
constraints: ["HTML tags are rejected; use Table for tabular data"],
|
|
608
|
+
example: {
|
|
609
|
+
component: "Markdown",
|
|
610
|
+
props: { text: "- first\n- second" }
|
|
611
|
+
}
|
|
612
|
+
},
|
|
613
|
+
{
|
|
614
|
+
name: "Badge",
|
|
615
|
+
group: "content",
|
|
616
|
+
description: "A short status label.",
|
|
617
|
+
props: z.object({
|
|
618
|
+
text: z.string(),
|
|
619
|
+
tone: tone.default("neutral")
|
|
620
|
+
}),
|
|
621
|
+
example: {
|
|
622
|
+
component: "Badge",
|
|
623
|
+
props: {
|
|
624
|
+
text: "active",
|
|
625
|
+
tone: "success"
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
},
|
|
629
|
+
{
|
|
630
|
+
name: "Metric",
|
|
631
|
+
group: "data",
|
|
632
|
+
description: "A single headline number with an optional change indicator.",
|
|
633
|
+
props: z.object({
|
|
634
|
+
label: z.string(),
|
|
635
|
+
value: z.union([z.string(), z.number()]),
|
|
636
|
+
change: z.string().optional(),
|
|
637
|
+
trend: z.enum([
|
|
638
|
+
"up",
|
|
639
|
+
"down",
|
|
640
|
+
"neutral"
|
|
641
|
+
]).optional()
|
|
642
|
+
}),
|
|
643
|
+
example: {
|
|
644
|
+
component: "Metric",
|
|
645
|
+
props: {
|
|
646
|
+
label: "MRR",
|
|
647
|
+
value: "12,400",
|
|
648
|
+
trend: "up",
|
|
649
|
+
change: "+12%"
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
},
|
|
653
|
+
{
|
|
654
|
+
name: "Table",
|
|
655
|
+
group: "data",
|
|
656
|
+
description: "Tabular data with explicit columns. Prefer it over Markdown tables.",
|
|
657
|
+
props: z.object({
|
|
658
|
+
columns: z.array(z.string()).min(1),
|
|
659
|
+
rows: z.array(z.array(z.union([
|
|
660
|
+
z.string(),
|
|
661
|
+
z.number(),
|
|
662
|
+
z.boolean(),
|
|
663
|
+
z.null()
|
|
664
|
+
])))
|
|
665
|
+
}),
|
|
666
|
+
example: {
|
|
667
|
+
component: "Table",
|
|
668
|
+
props: {
|
|
669
|
+
columns: ["Quarter", "Revenue"],
|
|
670
|
+
rows: [["Q3", 9800], ["Q4", 12400]]
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
},
|
|
674
|
+
{
|
|
675
|
+
name: "Chart",
|
|
676
|
+
group: "data",
|
|
677
|
+
description: "A bar, line, area or pie chart over labelled series.",
|
|
678
|
+
props: z.object({
|
|
679
|
+
type: z.enum([
|
|
680
|
+
"bar",
|
|
681
|
+
"line",
|
|
682
|
+
"area",
|
|
683
|
+
"pie"
|
|
684
|
+
]),
|
|
685
|
+
title: z.string().optional(),
|
|
686
|
+
labels: z.array(z.string()).min(1),
|
|
687
|
+
series: z.array(z.object({
|
|
688
|
+
name: z.string(),
|
|
689
|
+
values: z.array(z.number())
|
|
690
|
+
})).min(1)
|
|
691
|
+
}),
|
|
692
|
+
example: {
|
|
693
|
+
component: "Chart",
|
|
694
|
+
props: {
|
|
695
|
+
type: "bar",
|
|
696
|
+
labels: ["Q3", "Q4"],
|
|
697
|
+
series: [{
|
|
698
|
+
name: "Revenue",
|
|
699
|
+
values: [9800, 12400]
|
|
700
|
+
}]
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
},
|
|
704
|
+
{
|
|
705
|
+
name: "FileLink",
|
|
706
|
+
group: "data",
|
|
707
|
+
description: "A download entry for a run artifact.",
|
|
708
|
+
props: z.object({
|
|
709
|
+
path: z.string(),
|
|
710
|
+
label: z.string().optional(),
|
|
711
|
+
mimeType: z.string().optional()
|
|
712
|
+
}),
|
|
713
|
+
example: {
|
|
714
|
+
component: "FileLink",
|
|
715
|
+
props: {
|
|
716
|
+
path: "report.csv",
|
|
717
|
+
label: "Download report"
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
},
|
|
721
|
+
{
|
|
722
|
+
name: "Form",
|
|
723
|
+
group: "input",
|
|
724
|
+
description: "Collects user input. Children must be Field nodes; actions go in the buttons prop.",
|
|
725
|
+
props: z.object({
|
|
726
|
+
title: z.string().optional(),
|
|
727
|
+
submitLabel: z.string().default("Submit"),
|
|
728
|
+
cancelLabel: z.string().optional()
|
|
729
|
+
}),
|
|
730
|
+
children: ["Field"],
|
|
731
|
+
constraints: ["At most one Form per surface", "Every Field name must be unique inside the Form"],
|
|
732
|
+
example: {
|
|
733
|
+
component: "Form",
|
|
734
|
+
props: { title: "Schedule report" },
|
|
735
|
+
children: [{
|
|
736
|
+
component: "Field",
|
|
737
|
+
props: {
|
|
738
|
+
name: "email",
|
|
739
|
+
label: "Email",
|
|
740
|
+
type: "text",
|
|
741
|
+
required: true
|
|
742
|
+
}
|
|
743
|
+
}]
|
|
744
|
+
}
|
|
745
|
+
},
|
|
746
|
+
{
|
|
747
|
+
name: "Field",
|
|
748
|
+
group: "input",
|
|
749
|
+
description: "A single form control. Only valid inside a Form.",
|
|
750
|
+
props: z.object({
|
|
751
|
+
name: z.string(),
|
|
752
|
+
label: z.string(),
|
|
753
|
+
type: z.enum([
|
|
754
|
+
"text",
|
|
755
|
+
"number",
|
|
756
|
+
"date",
|
|
757
|
+
"textarea",
|
|
758
|
+
"select",
|
|
759
|
+
"multi-select",
|
|
760
|
+
"toggle",
|
|
761
|
+
"file"
|
|
762
|
+
]),
|
|
763
|
+
required: z.boolean().optional(),
|
|
764
|
+
description: z.string().optional(),
|
|
765
|
+
defaultValue: z.unknown().optional(),
|
|
766
|
+
options: z.array(z.object({
|
|
767
|
+
label: z.string(),
|
|
768
|
+
value: z.union([z.string(), z.number()])
|
|
769
|
+
})).optional()
|
|
770
|
+
}),
|
|
771
|
+
constraints: ["select / multi-select must provide options"],
|
|
772
|
+
example: {
|
|
773
|
+
component: "Field",
|
|
774
|
+
props: {
|
|
775
|
+
name: "range",
|
|
776
|
+
label: "Range",
|
|
777
|
+
type: "select",
|
|
778
|
+
options: []
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
},
|
|
782
|
+
{
|
|
783
|
+
name: "Button",
|
|
784
|
+
group: "input",
|
|
785
|
+
description: "Triggers one of the catalog actions.",
|
|
786
|
+
props: actionRef,
|
|
787
|
+
example: {
|
|
788
|
+
component: "Button",
|
|
789
|
+
props: {
|
|
790
|
+
action: "submit",
|
|
791
|
+
label: "Send"
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
},
|
|
795
|
+
{
|
|
796
|
+
name: "Alert",
|
|
797
|
+
group: "feedback",
|
|
798
|
+
description: "Draws attention to a state the user must know about.",
|
|
799
|
+
props: z.object({
|
|
800
|
+
tone: z.enum([
|
|
801
|
+
"info",
|
|
802
|
+
"warning",
|
|
803
|
+
"destructive"
|
|
804
|
+
]).default("info"),
|
|
805
|
+
title: z.string(),
|
|
806
|
+
description: z.string().optional()
|
|
807
|
+
}),
|
|
808
|
+
example: {
|
|
809
|
+
component: "Alert",
|
|
810
|
+
props: {
|
|
811
|
+
tone: "warning",
|
|
812
|
+
title: "Partial data"
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
},
|
|
816
|
+
{
|
|
817
|
+
name: "Custom",
|
|
818
|
+
group: "layout",
|
|
819
|
+
description: "Host extension point; the name must be registered in the host SurfaceRegistry.",
|
|
820
|
+
props: z.object({
|
|
821
|
+
name: z.string(),
|
|
822
|
+
props: z.record(z.string(), z.unknown()).default({})
|
|
823
|
+
}),
|
|
824
|
+
constraints: ["Rejected when the host has not registered the component name"],
|
|
825
|
+
example: {
|
|
826
|
+
component: "Custom",
|
|
827
|
+
props: {
|
|
828
|
+
name: "sales-map",
|
|
829
|
+
props: {}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
],
|
|
834
|
+
actions: [
|
|
835
|
+
{
|
|
836
|
+
name: "submit",
|
|
837
|
+
description: "Submit the surface form values back to the waiting run."
|
|
838
|
+
},
|
|
839
|
+
{
|
|
840
|
+
name: "cancel",
|
|
841
|
+
description: "Dismiss the surface without submitting."
|
|
842
|
+
},
|
|
843
|
+
{
|
|
844
|
+
name: "select",
|
|
845
|
+
description: "Report a selected row or option."
|
|
846
|
+
},
|
|
847
|
+
{
|
|
848
|
+
name: "download",
|
|
849
|
+
description: "Download the referenced artifact."
|
|
850
|
+
},
|
|
851
|
+
{
|
|
852
|
+
name: "refresh",
|
|
853
|
+
description: "Ask the run to recompute the surface."
|
|
854
|
+
}
|
|
855
|
+
]
|
|
856
|
+
});
|
|
857
|
+
/** prompt / 文档里引用的分组顺序(Console 展示同序) */
|
|
858
|
+
const UI_CATALOG_GROUPS = {
|
|
859
|
+
content: CONTENT,
|
|
860
|
+
data: DATA,
|
|
861
|
+
input: INPUT
|
|
862
|
+
};
|
|
863
|
+
/** A2UI 协议公共类型(与 @a2ui/web_core 的 common-types 同一套定义) */
|
|
864
|
+
const A2UI_COMMON_TYPES = "https://a2ui.org/specification/v0_9/common_types.json";
|
|
865
|
+
const CHILD_LIST_REF = `${A2UI_COMMON_TYPES}#/$defs/ChildList`;
|
|
866
|
+
const ACTION_REF = `${A2UI_COMMON_TYPES}#/$defs/Action`;
|
|
867
|
+
const DYNAMIC_VALUE_REF = `${A2UI_COMMON_TYPES}#/$defs/DynamicValue`;
|
|
868
|
+
/** catalog 里承载 action 名的 props 字段 */
|
|
869
|
+
const ACTION_PROP = "action";
|
|
870
|
+
/** 表单控件的标识字段;有它才谈得上把值写进数据模型 */
|
|
871
|
+
const NAME_PROP = "name";
|
|
872
|
+
const SUBMIT_LABEL_PROP = "submitLabel";
|
|
873
|
+
const CANCEL_LABEL_PROP = "cancelLabel";
|
|
874
|
+
/** A2UI 线上形状里表单控件的取值字段 */
|
|
875
|
+
const A2UI_VALUE_PROP = "value";
|
|
876
|
+
/** A2UI 线上形状里的子节点字段 */
|
|
877
|
+
const A2UI_CHILDREN_PROP = "children";
|
|
878
|
+
/** catalog → 每个组件的 A2UI 线上形状 */
|
|
879
|
+
function a2uiComponentShapes(catalog) {
|
|
880
|
+
const branches = catalog.toJsonSchema()["oneOf"] ?? [];
|
|
881
|
+
const byName = /* @__PURE__ */ new Map();
|
|
882
|
+
for (const branch of branches) {
|
|
883
|
+
const name = (branch.properties?.["component"])?.["const"];
|
|
884
|
+
if (typeof name === "string") byName.set(name, branch);
|
|
885
|
+
}
|
|
886
|
+
return catalog.components.map((def) => {
|
|
887
|
+
const branch = byName.get(def.name);
|
|
888
|
+
const props = branch?.properties?.["props"] ?? { type: "object" };
|
|
889
|
+
const keys = Object.keys(props.properties ?? {});
|
|
890
|
+
const childrenDescription = (branch?.properties?.["children"])?.description;
|
|
891
|
+
return {
|
|
892
|
+
name: def.name,
|
|
893
|
+
description: def.description,
|
|
894
|
+
props,
|
|
895
|
+
staticProps: keys.filter((key) => key !== ACTION_PROP),
|
|
896
|
+
childList: def.children !== void 0,
|
|
897
|
+
...childrenDescription ? { childListDescription: childrenDescription } : {},
|
|
898
|
+
actionProps: keys.filter((key) => key === ACTION_PROP),
|
|
899
|
+
valueProps: def.group === "input" && keys.includes(NAME_PROP) ? [A2UI_VALUE_PROP] : [],
|
|
900
|
+
...keys.includes(SUBMIT_LABEL_PROP) ? { submitLabelProp: SUBMIT_LABEL_PROP } : {},
|
|
901
|
+
...keys.includes(CANCEL_LABEL_PROP) ? { cancelLabelProp: CANCEL_LABEL_PROP } : {}
|
|
902
|
+
};
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
/** 单个组件的 A2UI props JSON Schema(协议要求每个组件是一份 schema) */
|
|
906
|
+
function a2uiComponentSchema(shape) {
|
|
907
|
+
const properties = { ...shape.props.properties ?? {} };
|
|
908
|
+
for (const key of shape.actionProps) properties[key] = {
|
|
909
|
+
$ref: ACTION_REF,
|
|
910
|
+
description: "What happens when the user activates this component."
|
|
911
|
+
};
|
|
912
|
+
for (const key of shape.valueProps) properties[key] = {
|
|
913
|
+
$ref: DYNAMIC_VALUE_REF,
|
|
914
|
+
description: "Two-way binding to the surface data model."
|
|
915
|
+
};
|
|
916
|
+
if (shape.childList) properties[A2UI_CHILDREN_PROP] = {
|
|
917
|
+
$ref: CHILD_LIST_REF,
|
|
918
|
+
description: shape.childListDescription ? `${shape.childListDescription}. Children are referenced by id, never inlined.` : "Child component ids, in render order."
|
|
919
|
+
};
|
|
920
|
+
return {
|
|
921
|
+
...shape.props,
|
|
922
|
+
description: shape.description,
|
|
923
|
+
properties
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
/** 自定义 catalog 的协议 id(对齐 https://a2ui.org/specification/v1_0/catalog_definition.json) */
|
|
927
|
+
const WEBSKILL_A2UI_CATALOG_ID = "https://webskill.dev/a2ui/catalogs/v1/catalog.json";
|
|
928
|
+
/**
|
|
929
|
+
* WebSkill catalog → A2UI `catalog_definition.json`。
|
|
930
|
+
* 组件 schema 由 catalog 的 JSON Schema 投影经 `a2uiComponentShapes` 改写成 A2UI 线上形状
|
|
931
|
+
* (嵌套变 id 引用、action 名变协议信封),不手写第二份词汇表——
|
|
932
|
+
* Lit 元素的 zod API 也由同一份 shape 派生。
|
|
933
|
+
*/
|
|
934
|
+
function buildA2uiCatalogDefinition(catalog) {
|
|
935
|
+
const components = {};
|
|
936
|
+
for (const shape of a2uiComponentShapes(catalog)) components[shape.name] = a2uiComponentSchema(shape);
|
|
937
|
+
return {
|
|
938
|
+
catalogId: WEBSKILL_A2UI_CATALOG_ID,
|
|
939
|
+
title: catalog.title,
|
|
940
|
+
description: `${catalog.title} ${catalog.version}`,
|
|
941
|
+
instructions: catalog.instructions,
|
|
942
|
+
components,
|
|
943
|
+
functions: Object.fromEntries(catalog.actions.map((action) => [action.name, { description: action.description }]))
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
//#endregion
|
|
948
|
+
//#region ../ui/dist/index.js
|
|
949
|
+
/** 提交值按请求类型归形(WebFormBridge 与框架组件库共享单一来源) */
|
|
950
|
+
function shapeInteractionValue(model, values) {
|
|
951
|
+
switch (model.kind) {
|
|
952
|
+
case "ask": return values["answer"];
|
|
953
|
+
case "confirm": return values["confirmed"] === true;
|
|
954
|
+
case "select": return values["selected"];
|
|
955
|
+
case "authorize": return true;
|
|
956
|
+
case "form": return values;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
/** 五类 InteractionRequest → 统一中间模型(框架无关) */
|
|
960
|
+
function interactionToFormModel(request) {
|
|
961
|
+
switch (request.type) {
|
|
962
|
+
case "ask": return {
|
|
963
|
+
kind: "ask",
|
|
964
|
+
message: request.message,
|
|
965
|
+
controls: [{
|
|
966
|
+
name: "answer",
|
|
967
|
+
label: request.message,
|
|
968
|
+
control: "text",
|
|
969
|
+
required: true
|
|
970
|
+
}],
|
|
971
|
+
submitLabel: "Submit",
|
|
972
|
+
cancelLabel: "Cancel"
|
|
973
|
+
};
|
|
974
|
+
case "confirm": return {
|
|
975
|
+
kind: "confirm",
|
|
976
|
+
message: request.message,
|
|
977
|
+
controls: [{
|
|
978
|
+
name: "confirmed",
|
|
979
|
+
label: request.message,
|
|
980
|
+
control: "boolean",
|
|
981
|
+
defaultValue: request.defaultValue ?? true
|
|
982
|
+
}],
|
|
983
|
+
submitLabel: "Confirm",
|
|
984
|
+
cancelLabel: "Cancel"
|
|
985
|
+
};
|
|
986
|
+
case "form": return {
|
|
987
|
+
kind: "form",
|
|
988
|
+
...request.title ? { title: request.title } : {},
|
|
989
|
+
controls: request.fields.map((f) => ({
|
|
990
|
+
name: f.name,
|
|
991
|
+
label: f.label,
|
|
992
|
+
control: f.type,
|
|
993
|
+
...f.required ? { required: true } : {},
|
|
994
|
+
...f.description ? { description: f.description } : {},
|
|
995
|
+
...f.defaultValue !== void 0 ? { defaultValue: f.defaultValue } : {},
|
|
996
|
+
...f.options ? { options: f.options } : {}
|
|
997
|
+
})),
|
|
998
|
+
submitLabel: "Submit",
|
|
999
|
+
cancelLabel: "Cancel"
|
|
1000
|
+
};
|
|
1001
|
+
case "select": return {
|
|
1002
|
+
kind: "select",
|
|
1003
|
+
message: request.message,
|
|
1004
|
+
controls: [{
|
|
1005
|
+
name: "selected",
|
|
1006
|
+
label: request.message,
|
|
1007
|
+
control: "select",
|
|
1008
|
+
required: true,
|
|
1009
|
+
options: request.options
|
|
1010
|
+
}],
|
|
1011
|
+
submitLabel: "Select",
|
|
1012
|
+
cancelLabel: "Cancel"
|
|
1013
|
+
};
|
|
1014
|
+
case "authorize": return {
|
|
1015
|
+
kind: "authorize",
|
|
1016
|
+
title: "Authorization required",
|
|
1017
|
+
message: request.message,
|
|
1018
|
+
controls: [],
|
|
1019
|
+
submitLabel: "Allow",
|
|
1020
|
+
cancelLabel: "Deny"
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
const isEmpty = (v) => v === void 0 || v === "";
|
|
1025
|
+
/**
|
|
1026
|
+
* 控件名 → 属性选择器:优先 CSS.escape(标识符形式,免引号转义);
|
|
1027
|
+
* 无 CSS.escape 的环境退化为引号包裹 + 转义反斜杠/双引号(防选择器注入崩溃)
|
|
1028
|
+
*/
|
|
1029
|
+
function controlSelector(name) {
|
|
1030
|
+
if (typeof CSS !== "undefined" && typeof CSS.escape === "function") return `[data-webskill-control=${CSS.escape(name)}]`;
|
|
1031
|
+
return `[data-webskill-control="${name.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"]`;
|
|
1032
|
+
}
|
|
1033
|
+
/**
|
|
1034
|
+
* 严格类型化控件值收集:
|
|
1035
|
+
* - number 空串 → undefined(非 0);非数值输入(NaN)按缺失处理(required 时入 missingRequired)
|
|
1036
|
+
* - select 用 option 值本身经 JSON 编码比对命中(对象值可正确命中)
|
|
1037
|
+
* - required 空值列入 missingRequired(由调用方阻止提交并标记)
|
|
1038
|
+
*/
|
|
1039
|
+
function collectValues(controls, container) {
|
|
1040
|
+
const values = {};
|
|
1041
|
+
const missingRequired = [];
|
|
1042
|
+
for (const control of controls) {
|
|
1043
|
+
const el = container.querySelector(controlSelector(control.name));
|
|
1044
|
+
let value;
|
|
1045
|
+
if (el === null) value = void 0;
|
|
1046
|
+
else if (control.control === "boolean") value = el.checked;
|
|
1047
|
+
else if (control.control === "number") {
|
|
1048
|
+
const raw = el.value.trim();
|
|
1049
|
+
if (raw === "") value = void 0;
|
|
1050
|
+
else {
|
|
1051
|
+
const n = Number(raw);
|
|
1052
|
+
value = Number.isNaN(n) ? void 0 : n;
|
|
1053
|
+
}
|
|
1054
|
+
} else if (control.control === "select") {
|
|
1055
|
+
const attr = el.value;
|
|
1056
|
+
const hit = control.options?.find((o) => JSON.stringify(o.value) === attr);
|
|
1057
|
+
value = hit ? hit.value : attr === "" ? void 0 : attr;
|
|
1058
|
+
} else {
|
|
1059
|
+
const raw = el.value;
|
|
1060
|
+
value = raw === "" ? void 0 : raw;
|
|
1061
|
+
}
|
|
1062
|
+
values[control.name] = value;
|
|
1063
|
+
if (control.required && isEmpty(value)) missingRequired.push(control.name);
|
|
1064
|
+
}
|
|
1065
|
+
return {
|
|
1066
|
+
values,
|
|
1067
|
+
missingRequired
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
417
1070
|
/** RenderBlock → DOM(markdown/json/table/image/file/chart 六种) */
|
|
418
1071
|
function renderBlocks(container, blocks, doc) {
|
|
419
1072
|
for (const block of blocks) switch (block.type) {
|
|
@@ -706,7 +1359,174 @@ var WebFormBridge = class {
|
|
|
706
1359
|
return wrapper;
|
|
707
1360
|
}
|
|
708
1361
|
};
|
|
1362
|
+
/**
|
|
1363
|
+
* 保留的 custom 组件名:catalog 声明树整棵挂在这个 surface 上。
|
|
1364
|
+
* `UiSurface` 的六类联合表达不了通用节点树,改造它属破坏性变更(21 号文档 §7 已备案),
|
|
1365
|
+
* 因此本轮用一个保留 component 名承载,四档 registry 各自识别它。
|
|
1366
|
+
*/
|
|
1367
|
+
const UI_SPEC_COMPONENT = "webskill.ui-spec";
|
|
1368
|
+
function collectActions(node, out) {
|
|
1369
|
+
if (node.component === "Button" && typeof node.props?.["action"] === "string") out.add(node.props["action"]);
|
|
1370
|
+
if (node.component === "Form") {
|
|
1371
|
+
out.add("submit");
|
|
1372
|
+
if (typeof node.props?.["cancelLabel"] === "string") out.add("cancel");
|
|
1373
|
+
}
|
|
1374
|
+
for (const child of node.children ?? []) collectActions(child, out);
|
|
1375
|
+
}
|
|
1376
|
+
const INTENTS = /* @__PURE__ */ new Set([
|
|
1377
|
+
"submit",
|
|
1378
|
+
"cancel",
|
|
1379
|
+
"select",
|
|
1380
|
+
"download",
|
|
1381
|
+
"refresh"
|
|
1382
|
+
]);
|
|
1383
|
+
/** 声明树 → `UiSurface`。id 缺省时由调用方给(工具源用 runtime 生成的 id)。 */
|
|
1384
|
+
function toSurface(catalog, spec, id) {
|
|
1385
|
+
const names = /* @__PURE__ */ new Set();
|
|
1386
|
+
collectActions(spec, names);
|
|
1387
|
+
const actions = [...names].filter((name) => INTENTS.has(name)).map((intent) => ({
|
|
1388
|
+
id: intent,
|
|
1389
|
+
label: catalog.actions.find((action) => action.name === intent)?.name ?? intent,
|
|
1390
|
+
intent,
|
|
1391
|
+
...intent === "submit" ? { awaitResponse: true } : {}
|
|
1392
|
+
}));
|
|
1393
|
+
const props = {
|
|
1394
|
+
catalogId: catalog.id,
|
|
1395
|
+
catalogVersion: catalog.version,
|
|
1396
|
+
spec,
|
|
1397
|
+
actions: [...names]
|
|
1398
|
+
};
|
|
1399
|
+
return {
|
|
1400
|
+
kind: "custom",
|
|
1401
|
+
id,
|
|
1402
|
+
component: UI_SPEC_COMPONENT,
|
|
1403
|
+
props,
|
|
1404
|
+
...actions.length > 0 ? { actions } : {}
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
/** surface 是否承载 catalog 声明树(四档 renderer 的分派入口) */
|
|
1408
|
+
function isUiSpecSurface(surface) {
|
|
1409
|
+
return surface.kind === "custom" && surface.component === "webskill.ui-spec";
|
|
1410
|
+
}
|
|
1411
|
+
/**
|
|
1412
|
+
* catalog 声明树 → json-render 扁平 spec。纯数据变换,不 import json-render
|
|
1413
|
+
* (`@webskill/ui` 保持环境无关)。
|
|
1414
|
+
*/
|
|
1415
|
+
function toJsonRenderSpec(spec) {
|
|
1416
|
+
const elements = {};
|
|
1417
|
+
let counter = 0;
|
|
1418
|
+
const walk = (node) => {
|
|
1419
|
+
const key = node.id ?? `${node.component.toLowerCase()}-${counter++}`;
|
|
1420
|
+
const children = (node.children ?? []).map(walk);
|
|
1421
|
+
elements[key] = {
|
|
1422
|
+
type: node.component,
|
|
1423
|
+
props: node.props ?? {},
|
|
1424
|
+
...children.length > 0 ? { children } : {}
|
|
1425
|
+
};
|
|
1426
|
+
return key;
|
|
1427
|
+
};
|
|
1428
|
+
return {
|
|
1429
|
+
root: walk(spec),
|
|
1430
|
+
elements
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
const RENDER_UI_TOOL = "render_ui";
|
|
1434
|
+
const defaultId = () => `ui-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1435
|
+
/**
|
|
1436
|
+
* 把 catalog 变成一个工具的入参 Schema,让模型直接生成声明式 UI(21 号文档 §3):
|
|
1437
|
+
* agentLoop / UiBridge / extractUiSurfaceEvents 全部零改动,装配处只多一个 externalTools 条目。
|
|
1438
|
+
*/
|
|
1439
|
+
function createUiCatalogToolSource(options = {}) {
|
|
1440
|
+
const catalog = options.catalog ?? uiCatalog;
|
|
1441
|
+
const newSurfaceId = options.newSurfaceId ?? defaultId;
|
|
1442
|
+
return {
|
|
1443
|
+
kind: "ui-catalog",
|
|
1444
|
+
listToolSpecs: () => Promise.resolve([{
|
|
1445
|
+
name: RENDER_UI_TOOL,
|
|
1446
|
+
description: [
|
|
1447
|
+
"Render a user interface for the answer instead of describing it in prose.",
|
|
1448
|
+
"Call it when the user asks for a form, a table, a chart, a metric or a file download.",
|
|
1449
|
+
"",
|
|
1450
|
+
catalog.toPrompt({ mode: "tool-description" })
|
|
1451
|
+
].join("\n"),
|
|
1452
|
+
inputSchema: {
|
|
1453
|
+
type: "object",
|
|
1454
|
+
properties: { spec: catalog.toJsonSchema() },
|
|
1455
|
+
required: ["spec"],
|
|
1456
|
+
additionalProperties: false
|
|
1457
|
+
}
|
|
1458
|
+
}]),
|
|
1459
|
+
canHandle: (name) => name === RENDER_UI_TOOL,
|
|
1460
|
+
call: (_name, args) => {
|
|
1461
|
+
const spec = args["spec"];
|
|
1462
|
+
const result = catalog.validate(spec);
|
|
1463
|
+
if (!result.ok) return Promise.resolve({
|
|
1464
|
+
ok: false,
|
|
1465
|
+
content: [],
|
|
1466
|
+
error: {
|
|
1467
|
+
code: "UI_SPEC_INVALID",
|
|
1468
|
+
message: `The UI spec does not match the catalog: ${result.issues.map((issue) => `${issue.path}: ${issue.message}`).join("; ")}`
|
|
1469
|
+
}
|
|
1470
|
+
});
|
|
1471
|
+
const surface = toSurface(catalog, spec, newSurfaceId());
|
|
1472
|
+
return Promise.resolve({
|
|
1473
|
+
ok: true,
|
|
1474
|
+
content: [{
|
|
1475
|
+
type: "json",
|
|
1476
|
+
data: { $surface: {
|
|
1477
|
+
type: "open",
|
|
1478
|
+
surface
|
|
1479
|
+
} }
|
|
1480
|
+
}, {
|
|
1481
|
+
type: "text",
|
|
1482
|
+
text: `Rendered surface ${surface.id}.`
|
|
1483
|
+
}]
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
const WEBSKILL_SURFACE_ACTION = "webskill:surface-action";
|
|
1489
|
+
function toUiSurfaceDescriptor(snapshot, provenance) {
|
|
1490
|
+
return {
|
|
1491
|
+
type: "webskill-surface",
|
|
1492
|
+
snapshot,
|
|
1493
|
+
provenance
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
function toUiSurfaceActionDispatch(snapshot, action, value) {
|
|
1497
|
+
return {
|
|
1498
|
+
...snapshot.runId ? { runId: snapshot.runId } : {},
|
|
1499
|
+
surfaceId: snapshot.surface.id,
|
|
1500
|
+
actionId: action.id,
|
|
1501
|
+
intent: action.intent,
|
|
1502
|
+
...action.nonce ? { nonce: action.nonce } : {},
|
|
1503
|
+
...value ? { value } : {}
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
/** Returns undefined for malformed or non-WebSkill renderer events. @experimental */
|
|
1507
|
+
function fromUiSurfaceActionDispatch(value) {
|
|
1508
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
1509
|
+
const event = value;
|
|
1510
|
+
if (typeof event["surfaceId"] !== "string" || typeof event["actionId"] !== "string" || !isSurfaceActionIntent(event["intent"])) return;
|
|
1511
|
+
return {
|
|
1512
|
+
...typeof event["runId"] === "string" ? { runId: event["runId"] } : {},
|
|
1513
|
+
surfaceId: event["surfaceId"],
|
|
1514
|
+
actionId: event["actionId"],
|
|
1515
|
+
intent: event["intent"],
|
|
1516
|
+
...typeof event["nonce"] === "string" ? { nonce: event["nonce"] } : {},
|
|
1517
|
+
...isRecord(event["value"]) ? { value: event["value"] } : {},
|
|
1518
|
+
...event["cancelled"] === true ? { cancelled: true } : {}
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
function isSurfaceActionIntent(value) {
|
|
1522
|
+
return value === "submit" || value === "cancel" || value === "select" || value === "download" || value === "refresh";
|
|
1523
|
+
}
|
|
1524
|
+
function isRecord(value) {
|
|
1525
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1526
|
+
}
|
|
709
1527
|
const VERCEL_INTERACTION_TOOL_NAME = "webskill_interaction";
|
|
1528
|
+
const VERCEL_SURFACE_DATA_PART_TYPE = "data-webskill-surface";
|
|
1529
|
+
const VERCEL_SURFACE_ACTION_DATA_PART_TYPE = WEBSKILL_SURFACE_ACTION;
|
|
710
1530
|
/** InteractionRequest → SDK tool UI 结构(应用聊天 UI 直接消费) */
|
|
711
1531
|
function toVercelToolInvocation(request) {
|
|
712
1532
|
return {
|
|
@@ -729,6 +1549,20 @@ function fromVercelToolResult(part) {
|
|
|
729
1549
|
value: p["output"] ?? p["result"] ?? p["value"]
|
|
730
1550
|
};
|
|
731
1551
|
}
|
|
1552
|
+
/** UiSurfaceSnapshot → AI SDK data part; the SurfaceHost owns renderer selection. @experimental */
|
|
1553
|
+
function toVercelSurfaceDataPart(snapshot) {
|
|
1554
|
+
return {
|
|
1555
|
+
type: VERCEL_SURFACE_DATA_PART_TYPE,
|
|
1556
|
+
id: `webskill-surface-${snapshot.surface.id}`,
|
|
1557
|
+
data: toUiSurfaceDescriptor(snapshot, "vercel-surface-host")
|
|
1558
|
+
};
|
|
1559
|
+
}
|
|
1560
|
+
/** Decodes a nonce-bound action data part emitted by a Vercel SurfaceHost. @experimental */
|
|
1561
|
+
function fromVercelSurfaceAction(part) {
|
|
1562
|
+
const event = part;
|
|
1563
|
+
if (event?.type !== "webskill:surface-action") return void 0;
|
|
1564
|
+
return fromUiSurfaceActionDispatch(event.data);
|
|
1565
|
+
}
|
|
732
1566
|
/**
|
|
733
1567
|
* 薄桥接类:request → SDK tool UI 结构 → 应用的聊天 UI(sendInteraction 回调实现)
|
|
734
1568
|
* → SDK 回传 → InteractionResponse。
|
|
@@ -757,6 +1591,7 @@ const OPENUI_SUBMIT_ACTION = "webskill:submit";
|
|
|
757
1591
|
const OPENUI_CANCEL_ACTION = "webskill:cancel";
|
|
758
1592
|
/** authorize 的 Allow 专用动作(提交即批准 → value:true;Deny 走 cancel → cancelled:true) */
|
|
759
1593
|
const OPENUI_AUTHORIZE_ACTION = "webskill:authorize";
|
|
1594
|
+
const OPENUI_SURFACE_ACTION = WEBSKILL_SURFACE_ACTION;
|
|
760
1595
|
const str = (s) => JSON.stringify(s);
|
|
761
1596
|
const val = (v) => JSON.stringify(v ?? null);
|
|
762
1597
|
function fieldStatements(fields) {
|
|
@@ -844,6 +1679,21 @@ function toOpenUiLang(request) {
|
|
|
844
1679
|
return lines.join("\n");
|
|
845
1680
|
}
|
|
846
1681
|
/**
|
|
1682
|
+
* UiSurfaceSnapshot → OpenUI library program. OpenUI applications provide WebSkillSurfaceHost;
|
|
1683
|
+
* its descriptor tells the host whether it is a native extension rather than an OpenUI-native component.
|
|
1684
|
+
* @experimental
|
|
1685
|
+
*/
|
|
1686
|
+
function toOpenUiSurfaceLang(snapshot) {
|
|
1687
|
+
const descriptor = toUiSurfaceDescriptor(snapshot, snapshot.surface.kind === "chart" || snapshot.surface.kind === "table" || snapshot.surface.kind === "custom" ? "openui-extension" : "openui-library");
|
|
1688
|
+
return `root = WebSkillSurfaceHost(${val(descriptor)})`;
|
|
1689
|
+
}
|
|
1690
|
+
/** Decodes a nonce-bound action emitted by an OpenUI WebSkillSurfaceHost. @experimental */
|
|
1691
|
+
function fromOpenUiSurfaceAction(action) {
|
|
1692
|
+
const event = action;
|
|
1693
|
+
if (event?.type !== "webskill:surface-action") return void 0;
|
|
1694
|
+
return fromUiSurfaceActionDispatch(event.params);
|
|
1695
|
+
}
|
|
1696
|
+
/**
|
|
847
1697
|
* OpenUI ActionEvent → InteractionResponse(formState 优先,取消单独分支)
|
|
848
1698
|
* @experimental
|
|
849
1699
|
*/
|
|
@@ -874,6 +1724,38 @@ function fromOpenUiAction(action) {
|
|
|
874
1724
|
value: a.params
|
|
875
1725
|
};
|
|
876
1726
|
}
|
|
1727
|
+
const literal = (value) => JSON.stringify(value ?? null);
|
|
1728
|
+
/** 组件的位置参数顺序 = catalog props 的 schema key 顺序(OpenUI 的 param map 同源) */
|
|
1729
|
+
function propOrder(catalog) {
|
|
1730
|
+
const branches = catalog.toJsonSchema()["oneOf"] ?? [];
|
|
1731
|
+
const order = /* @__PURE__ */ new Map();
|
|
1732
|
+
for (const branch of branches) {
|
|
1733
|
+
const name = (branch.properties?.["component"])?.["const"];
|
|
1734
|
+
const props = branch.properties?.["props"];
|
|
1735
|
+
if (typeof name === "string") order.set(name, Object.keys(props?.properties ?? {}));
|
|
1736
|
+
}
|
|
1737
|
+
return order;
|
|
1738
|
+
}
|
|
1739
|
+
/**
|
|
1740
|
+
* catalog 声明树 → openui-lang 文本。语法为每行一条 `id = Component(位置参数…)`,
|
|
1741
|
+
* `root = …` 为入口;容器组件的子节点作为最后一个参数传 id 数组。
|
|
1742
|
+
*/
|
|
1743
|
+
function toOpenUiSpecLang(spec, catalog = uiCatalog) {
|
|
1744
|
+
const order = propOrder(catalog);
|
|
1745
|
+
const lines = [];
|
|
1746
|
+
let counter = 0;
|
|
1747
|
+
const walk = (node) => {
|
|
1748
|
+
const id = node.id ?? `n${counter++}`;
|
|
1749
|
+
const childIds = (node.children ?? []).map(walk);
|
|
1750
|
+
const def = catalog.component(node.component);
|
|
1751
|
+
const args = (order.get(node.component) ?? []).map((key) => literal(node.props?.[key]));
|
|
1752
|
+
if (def?.children !== void 0) args.push(`[${childIds.join(", ")}]`);
|
|
1753
|
+
lines.push(`${id} = ${node.component}(${args.join(", ")})`);
|
|
1754
|
+
return id;
|
|
1755
|
+
};
|
|
1756
|
+
const rootId = walk(spec);
|
|
1757
|
+
return [...lines.filter((line) => !line.startsWith(`${rootId} =`)), `root = ${lines.find((line) => line.startsWith(`${rootId} =`)).slice(rootId.length + 3)}`].join("\n");
|
|
1758
|
+
}
|
|
877
1759
|
/**
|
|
878
1760
|
* InteractionRequest → A2UI 协议消息(v0.9.1,Basic Catalog)。
|
|
879
1761
|
* 依据 https://a2ui.org/specification/v0.9-a2ui/:
|
|
@@ -889,6 +1771,7 @@ const A2UI_VERSION = "v0.9.1";
|
|
|
889
1771
|
const A2UI_BASIC_CATALOG_ID = "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json";
|
|
890
1772
|
const A2UI_SUBMIT_ACTION = "webskill:submit";
|
|
891
1773
|
const A2UI_CANCEL_ACTION = "webskill:cancel";
|
|
1774
|
+
const A2UI_SURFACE_ACTION = WEBSKILL_SURFACE_ACTION;
|
|
892
1775
|
const text = (id, content, variant) => ({
|
|
893
1776
|
id,
|
|
894
1777
|
component: "Text",
|
|
@@ -1022,6 +1905,59 @@ function toA2uiMessages(request) {
|
|
|
1022
1905
|
return messages;
|
|
1023
1906
|
}
|
|
1024
1907
|
/**
|
|
1908
|
+
* UiSurfaceSnapshot → A2UI extension-host messages.
|
|
1909
|
+
* Basic Catalog has no chart/table vocabulary, so a labelled WebSkill host owns these controlled renderers.
|
|
1910
|
+
* @experimental
|
|
1911
|
+
*/
|
|
1912
|
+
function toA2uiSurfaceMessages(snapshot) {
|
|
1913
|
+
const surfaceId = `webskill-surface-${snapshot.surface.id}`;
|
|
1914
|
+
const standard = snapshot.surface.kind === "metric" || snapshot.surface.kind === "file";
|
|
1915
|
+
const descriptor = toUiSurfaceDescriptor(snapshot, standard ? "a2ui-standard" : "a2ui-extension");
|
|
1916
|
+
const component = standard ? {
|
|
1917
|
+
id: "content",
|
|
1918
|
+
component: "Text",
|
|
1919
|
+
text: snapshot.surface.kind === "metric" ? `${snapshot.surface.label}: ${snapshot.surface.value}` : snapshot.surface.kind === "file" ? snapshot.surface.path : ""
|
|
1920
|
+
} : {
|
|
1921
|
+
id: "content",
|
|
1922
|
+
component: "WebSkillSurfaceHost",
|
|
1923
|
+
descriptor,
|
|
1924
|
+
provenance: "WebSkill extension host"
|
|
1925
|
+
};
|
|
1926
|
+
return [{
|
|
1927
|
+
version: A2UI_VERSION,
|
|
1928
|
+
createSurface: {
|
|
1929
|
+
surfaceId,
|
|
1930
|
+
catalogId: A2UI_BASIC_CATALOG_ID,
|
|
1931
|
+
sendDataModel: true
|
|
1932
|
+
}
|
|
1933
|
+
}, {
|
|
1934
|
+
version: A2UI_VERSION,
|
|
1935
|
+
updateComponents: {
|
|
1936
|
+
surfaceId,
|
|
1937
|
+
components: [{
|
|
1938
|
+
id: "root",
|
|
1939
|
+
component: "Column",
|
|
1940
|
+
children: ["content"],
|
|
1941
|
+
justify: "start",
|
|
1942
|
+
align: "stretch"
|
|
1943
|
+
}, component]
|
|
1944
|
+
}
|
|
1945
|
+
}];
|
|
1946
|
+
}
|
|
1947
|
+
/** Decodes a nonce-bound action emitted by a WebSkill A2UI extension host. @experimental */
|
|
1948
|
+
function fromA2uiSurfaceAction(event) {
|
|
1949
|
+
const action = event;
|
|
1950
|
+
if (action?.name !== "webskill:surface-action") return void 0;
|
|
1951
|
+
return fromUiSurfaceActionDispatch(action.context);
|
|
1952
|
+
}
|
|
1953
|
+
/** Builds the A2UI event payload an extension host sends after a user action. @experimental */
|
|
1954
|
+
function toA2uiSurfaceAction(snapshot, action, value) {
|
|
1955
|
+
return {
|
|
1956
|
+
name: A2UI_SURFACE_ACTION,
|
|
1957
|
+
context: toUiSurfaceActionDispatch(snapshot, action, value)
|
|
1958
|
+
};
|
|
1959
|
+
}
|
|
1960
|
+
/**
|
|
1025
1961
|
* 按 InteractionRequest.type 解码提交值(绑定模型原样回传的是表单对象):
|
|
1026
1962
|
* confirm 仅 confirmed===true 批准;select 还原原始 option 值(非字符串值经 JSON 编码比对);
|
|
1027
1963
|
* form 的 number 字段转 number(NaN 保留原值);ask 取 answer;authorize 提交即批准。
|
|
@@ -1091,6 +2027,101 @@ function fromA2uiAction(event, request) {
|
|
|
1091
2027
|
value: e.context
|
|
1092
2028
|
};
|
|
1093
2029
|
}
|
|
2030
|
+
/** 声明树里按钮触发的事件名(宿主据 context.actionId 回传 surface action) */
|
|
2031
|
+
const A2UI_SPEC_ACTION = "webskill:spec-action";
|
|
2032
|
+
/** 表单值在 A2UI 数据模型里的根路径 */
|
|
2033
|
+
const A2UI_SPEC_FORM_PATH = "/form";
|
|
2034
|
+
/**
|
|
2035
|
+
* catalog 声明树 → A2UI 消息序列。
|
|
2036
|
+
*
|
|
2037
|
+
* 协议要求组件是扁平邻接表且根节点 id 为 `root`,因此这里做树 → 邻接表的展开。
|
|
2038
|
+
* 三处改写全部由 `a2uiComponentShapes` 决定,本函数不认识任何具体组件名:
|
|
2039
|
+
* - 子节点变成 props.children 里的 id 引用
|
|
2040
|
+
* - 表单控件的值经 JSON Pointer 与数据模型双向绑定
|
|
2041
|
+
* - 自带提交/取消标签的组件(A2UI 没有表单提交事件)把按钮物化成 Button 子节点,
|
|
2042
|
+
* 与 `toA2uiMessages` 对交互请求的处理同一手法
|
|
2043
|
+
*/
|
|
2044
|
+
function toA2uiSpecMessages(catalog, spec, options) {
|
|
2045
|
+
const shapes = new Map(a2uiComponentShapes(catalog).map((shape) => [shape.name, shape]));
|
|
2046
|
+
const components = [];
|
|
2047
|
+
const defaults = {};
|
|
2048
|
+
let counter = 0;
|
|
2049
|
+
const event = (actionId) => ({ event: {
|
|
2050
|
+
name: A2UI_SPEC_ACTION,
|
|
2051
|
+
context: {
|
|
2052
|
+
...options.context,
|
|
2053
|
+
actionId
|
|
2054
|
+
}
|
|
2055
|
+
} });
|
|
2056
|
+
const button = (id, text, actionId) => {
|
|
2057
|
+
components.push({
|
|
2058
|
+
id,
|
|
2059
|
+
component: "Button",
|
|
2060
|
+
label: text,
|
|
2061
|
+
action: event(actionId)
|
|
2062
|
+
});
|
|
2063
|
+
return id;
|
|
2064
|
+
};
|
|
2065
|
+
const walk = (node, id) => {
|
|
2066
|
+
const props = { ...node.props };
|
|
2067
|
+
const shape = shapes.get(node.component);
|
|
2068
|
+
const children = (node.children ?? []).map((child) => walk(child, child.id ?? `n${counter++}`));
|
|
2069
|
+
for (const key of shape?.valueProps ?? []) {
|
|
2070
|
+
const name = typeof props["name"] === "string" ? props["name"] : id;
|
|
2071
|
+
if (props["defaultValue"] !== void 0) defaults[name] = props["defaultValue"];
|
|
2072
|
+
props[key] = { path: `${A2UI_SPEC_FORM_PATH}/${name}` };
|
|
2073
|
+
}
|
|
2074
|
+
for (const key of shape?.actionProps ?? []) props[key] = event(typeof props[key] === "string" ? props[key] : "select");
|
|
2075
|
+
if (shape?.submitLabelProp) {
|
|
2076
|
+
const text = props[shape.submitLabelProp];
|
|
2077
|
+
children.push(button(`${id}-submit`, typeof text === "string" ? text : "Submit", "submit"));
|
|
2078
|
+
}
|
|
2079
|
+
if (shape?.cancelLabelProp && typeof props[shape.cancelLabelProp] === "string") children.push(button(`${id}-cancel`, props[shape.cancelLabelProp], "cancel"));
|
|
2080
|
+
if (children.length > 0) props[A2UI_CHILDREN_PROP] = children;
|
|
2081
|
+
components.push({
|
|
2082
|
+
...props,
|
|
2083
|
+
id,
|
|
2084
|
+
component: node.component
|
|
2085
|
+
});
|
|
2086
|
+
return id;
|
|
2087
|
+
};
|
|
2088
|
+
walk(spec, "root");
|
|
2089
|
+
const messages = [{
|
|
2090
|
+
version: A2UI_VERSION,
|
|
2091
|
+
createSurface: {
|
|
2092
|
+
surfaceId: options.surfaceId,
|
|
2093
|
+
catalogId: options.catalogId ?? "https://webskill.dev/a2ui/catalogs/v1/catalog.json",
|
|
2094
|
+
sendDataModel: true
|
|
2095
|
+
}
|
|
2096
|
+
}, {
|
|
2097
|
+
version: A2UI_VERSION,
|
|
2098
|
+
updateComponents: {
|
|
2099
|
+
surfaceId: options.surfaceId,
|
|
2100
|
+
components
|
|
2101
|
+
}
|
|
2102
|
+
}];
|
|
2103
|
+
if (Object.keys(defaults).length > 0) messages.push({
|
|
2104
|
+
version: A2UI_VERSION,
|
|
2105
|
+
updateDataModel: {
|
|
2106
|
+
surfaceId: options.surfaceId,
|
|
2107
|
+
path: A2UI_SPEC_FORM_PATH,
|
|
2108
|
+
value: defaults
|
|
2109
|
+
}
|
|
2110
|
+
});
|
|
2111
|
+
return messages;
|
|
2112
|
+
}
|
|
2113
|
+
/** A2UI action 事件 → 声明树 action(非本 catalog 的事件返回 undefined) */
|
|
2114
|
+
function fromA2uiSpecAction(event) {
|
|
2115
|
+
const action = event;
|
|
2116
|
+
if (action?.name !== "webskill:spec-action") return void 0;
|
|
2117
|
+
const context = action.context ?? {};
|
|
2118
|
+
const actionId = context["actionId"];
|
|
2119
|
+
if (typeof actionId !== "string") return void 0;
|
|
2120
|
+
return {
|
|
2121
|
+
actionId,
|
|
2122
|
+
context
|
|
2123
|
+
};
|
|
2124
|
+
}
|
|
1094
2125
|
let loading;
|
|
1095
2126
|
function loadA2uiRuntime() {
|
|
1096
2127
|
loading ??= (async () => {
|
|
@@ -1158,6 +2189,38 @@ var LitRendererBridge = class {
|
|
|
1158
2189
|
});
|
|
1159
2190
|
}
|
|
1160
2191
|
};
|
|
2192
|
+
/**
|
|
2193
|
+
* BYOC Lit 元素的加载入口。
|
|
2194
|
+
*
|
|
2195
|
+
* 元素模块静态依赖 `lit` 与 `@a2ui/*`(三者都是 optional peer),
|
|
2196
|
+
* 因此只能经动态 import 进来:未用 A2UI 档的应用不为这段体积买单,
|
|
2197
|
+
* 缺依赖时给出带安装提示的 `UI_UNAVAILABLE`,而不是一个空白 surface。
|
|
2198
|
+
*/
|
|
2199
|
+
async function loadWebSkillLitCatalog() {
|
|
2200
|
+
try {
|
|
2201
|
+
const { webskillLitCatalog } = await import("./webskillLitCatalog-CNaUpasU-CfSRvqCZ.js");
|
|
2202
|
+
return webskillLitCatalog();
|
|
2203
|
+
} catch (cause) {
|
|
2204
|
+
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);
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
/**
|
|
2208
|
+
* Loads the optional OpenUI peers (@openuidev/react-lang and zod) for the OpenUI
|
|
2209
|
+
* interaction/surface paths. Throws a WebSkillError with code UI_UNAVAILABLE and an
|
|
2210
|
+
* install hint when either peer is missing, so callers can fall back to native
|
|
2211
|
+
* rendering instead of leaving a blank surface.
|
|
2212
|
+
*/
|
|
2213
|
+
async function loadOpenUiPeers() {
|
|
2214
|
+
try {
|
|
2215
|
+
const [openuiModule, zodModule] = await Promise.all([import("./dist-DxyxmXPU-DGC-dpXf.js"), import("zod")]);
|
|
2216
|
+
return {
|
|
2217
|
+
openui: openuiModule,
|
|
2218
|
+
z: zodModule.z
|
|
2219
|
+
};
|
|
2220
|
+
} catch (cause) {
|
|
2221
|
+
throw new WebSkillError("UI_UNAVAILABLE", "The OpenUI renderer is unavailable; install @openuidev/react-lang and zod to render surfaces and interactions with the OpenUI track", cause);
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
1161
2224
|
|
|
1162
2225
|
//#endregion
|
|
1163
|
-
export {
|
|
2226
|
+
export { toVercelToolInvocation as $, fromOpenUiSurfaceAction as A, shapeInteractionValue as B, createUiCatalogToolSource as C, fromA2uiSpecAction as D, fromA2uiAction as E, isUiSpecSurface as F, toJsonRenderSpec as G, toA2uiSpecMessages as H, loadOpenUiPeers as I, toOpenUiSurfaceLang as J, toOpenUiLang as K, loadWebSkillLitCatalog as L, fromVercelSurfaceAction as M, fromVercelToolResult as N, fromA2uiSurfaceAction as O, interactionToFormModel as P, toVercelSurfaceDataPart as Q, renderBlocks as R, collectValues as S, ensureStyles as T, toA2uiSurfaceAction as U, toA2uiMessages as V, toA2uiSurfaceMessages as W, toUiSurfaceActionDispatch as X, toSurface as Y, toUiSurfaceDescriptor as Z, VERCEL_SURFACE_DATA_PART_TYPE as _, A2UI_SUBMIT_ACTION as a, a2uiComponentSchema as at, WEBSKILL_SURFACE_ACTION as b, LitRendererBridge as c, chartToTable as ct, OPENUI_SUBMIT_ACTION as d, renderMiniMarkdown as dt, A2UI_CHILDREN_PROP as et, OPENUI_SURFACE_ACTION as f, uiCatalog as ft, VERCEL_SURFACE_ACTION_DATA_PART_TYPE as g, VERCEL_INTERACTION_TOOL_NAME as h, A2UI_SPEC_FORM_PATH as i, WEBSKILL_A2UI_CATALOG_ID as it, fromUiSurfaceActionDispatch as j, fromOpenUiAction as k, OPENUI_AUTHORIZE_ACTION as l, defineUiCatalog as lt, UI_SPEC_COMPONENT as m, A2UI_CANCEL_ACTION as n, CHART_PALETTE as nt, A2UI_SURFACE_ACTION as o, a2uiComponentShapes as ot, RENDER_UI_TOOL as p, toOpenUiSpecLang as q, A2UI_SPEC_ACTION as r, UI_CATALOG_GROUPS as rt, A2UI_VERSION as s, buildA2uiCatalogDefinition as st, A2UI_BASIC_CATALOG_ID as t, A2UI_COMMON_TYPES as tt, OPENUI_CANCEL_ACTION as u, renderMiniChart as ut, VercelUiBridge as v, decodeInteractionResponse as w, WebFormBridge as x, WEBSKILL_STYLES_CSS as y, renderRenderResult as z };
|