@webskill/sdk 0.2.8 → 0.4.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/README.md +42 -0
- package/dist/browser.d.ts +2 -2
- package/dist/browser.js +4 -9
- package/dist/{catalogComponents-C_V39rbF-BOHveMWa.js → catalogComponents-KsujmL4b-Clx1kCnU.js} +278 -122
- package/dist/client-BCM6Z3yq-qUQNkKrK.js +7787 -0
- package/dist/{dist-BQzncxXg.js → dist-8oQRa8Xz.js} +212 -13
- package/dist/{dist-B9VLwOME.js → dist-C-Sh0MDU.js} +1019 -317
- package/dist/{dist-CtBLBbEz.js → dist-D9Lcn5Pp.js} +528 -838
- package/dist/governance.d.ts +46 -11
- package/dist/governance.js +152 -25
- package/dist/{index-7DVaZJU7.d.ts → index-CHXxDccV.d.ts} +62 -144
- package/dist/{index-QrHtAudz.d.ts → index-DLfR2Y6I.d.ts} +412 -21
- package/dist/index.d.ts +3 -3
- package/dist/index.js +4 -3
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +2 -2
- package/dist/memoryArtifactStore-BtOeB_hm-tj3fC5ip.js +78 -0
- package/dist/node.d.ts +297 -4
- package/dist/node.js +281 -7
- package/dist/{openUiLibrary-B8-Cvou9-D3RsU2EB.js → openUiLibrary-YLS-cxyT-C96jWDQq.js} +6 -5
- package/dist/skillVersionStore-uyefLPR1-DXOzbksv.d.ts +158 -0
- package/dist/stdio-CFMoANJJ-BxrTeXh7.js +31 -0
- package/dist/{testing-BUoXvm1u.js → testing-DDCJWvgA.js} +8 -6
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +2 -2
- package/dist/{types-AmKCKJn_-BogJPQHU.d.ts → types-7Wcg--Vh-1YlQ4jF9.d.ts} +219 -70
- package/dist/types-WovEf4ED-CZSDiiBU.js +6215 -0
- package/dist/ui-react.d.ts +329 -18
- package/dist/ui-react.js +3715 -3464
- package/dist/ui-vue.d.ts +1 -1
- package/dist/ui-vue.js +1 -1
- package/dist/ui.d.ts +4 -3
- package/dist/ui.js +3 -3
- package/dist/{webskillLitCatalog-CNaUpasU-CfSRvqCZ.js → webskillLitCatalog-CSTbhBe_-CYIs5BX8.js} +312 -122
- package/package.json +4 -7
- package/dist/jsonRenderRegistry-9GrWP_hE-CQY8bT9w.js +0 -2468
- package/dist/memoryArtifactStore-C9lFVqPF-yFz6yJj0.js +0 -48
- package/dist/skillVersionStore-B7rGjtMi-BgnQho9v.d.ts +0 -365
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { m as WebSkillError } from "./dist-8oQRa8Xz.js";
|
|
2
2
|
import { toJSONSchema, z } from "zod";
|
|
3
3
|
|
|
4
|
-
//#region ../ui/dist/webskillCatalog-
|
|
4
|
+
//#region ../ui/dist/webskillCatalog-CpIg33vd.js
|
|
5
5
|
/**
|
|
6
6
|
* 最小子集 markdown → DOM:标题(#..###)、无序列表、代码块(```)、加粗、链接。
|
|
7
7
|
* 全部内容经 textContent 写入,用户内容天然转义防 HTML 注入。
|
|
@@ -84,214 +84,112 @@ function renderMiniMarkdown(text, doc) {
|
|
|
84
84
|
}
|
|
85
85
|
return root;
|
|
86
86
|
}
|
|
87
|
+
const KINDS = /* @__PURE__ */ new Set([
|
|
88
|
+
"bar",
|
|
89
|
+
"line",
|
|
90
|
+
"pie"
|
|
91
|
+
]);
|
|
92
|
+
const list = (value) => Array.isArray(value) ? value : [];
|
|
87
93
|
/**
|
|
88
|
-
*
|
|
89
|
-
*
|
|
94
|
+
* catalog `Chart` 节点 props → `ChartSpec`。四档共用这一份转换:
|
|
95
|
+
* 此前 native / OpenUI / json-render / A2UI 各抄了一遍,改一处就会漂移。
|
|
96
|
+
* catalog 的 `area` 在 `ChartSpec` 里没有对应枚举,按折线渲染——
|
|
97
|
+
* 原先的裸 cast 会把非法枚举一路带进渲染器,画出空图。
|
|
90
98
|
*/
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
];
|
|
103
|
-
const WIDTH = 320;
|
|
104
|
-
const HEIGHT = 200;
|
|
105
|
-
function el(doc, name, attrs) {
|
|
106
|
-
const node = doc.createElementNS(SVG_NS, name);
|
|
107
|
-
for (const [key, value] of Object.entries(attrs)) node.setAttribute(key, value);
|
|
108
|
-
return node;
|
|
109
|
-
}
|
|
110
|
-
function text$1(doc, x, y, content, attrs = {}) {
|
|
111
|
-
const node = el(doc, "text", {
|
|
112
|
-
x: String(x),
|
|
113
|
-
y: String(y),
|
|
114
|
-
"font-size": "9",
|
|
115
|
-
fill: "#555",
|
|
116
|
-
...attrs
|
|
117
|
-
});
|
|
118
|
-
node.textContent = content;
|
|
119
|
-
return node;
|
|
120
|
-
}
|
|
121
|
-
const fmt = (n) => Number.isInteger(n) ? String(n) : n.toFixed(1);
|
|
122
|
-
function maxValue(chart) {
|
|
123
|
-
let max = 0;
|
|
124
|
-
for (const s of chart.series) for (const v of s.data) if (v > max) max = v;
|
|
125
|
-
return max;
|
|
126
|
-
}
|
|
127
|
-
function renderBar(chart, svg, doc) {
|
|
128
|
-
const plot = {
|
|
129
|
-
x0: 30,
|
|
130
|
-
y0: 24,
|
|
131
|
-
x1: 312,
|
|
132
|
-
y1: 164
|
|
99
|
+
function chartSpecFromProps(props) {
|
|
100
|
+
const type = typeof props["type"] === "string" ? props["type"] : "bar";
|
|
101
|
+
const series = list(props["series"]);
|
|
102
|
+
return {
|
|
103
|
+
kind: KINDS.has(type) ? type : "line",
|
|
104
|
+
labels: list(props["labels"]),
|
|
105
|
+
series: series.map((item) => ({
|
|
106
|
+
...typeof item.name === "string" ? { name: item.name } : {},
|
|
107
|
+
data: list(item.values)
|
|
108
|
+
})),
|
|
109
|
+
...typeof props["title"] === "string" ? { title: props["title"] } : {}
|
|
133
110
|
};
|
|
134
|
-
const max = maxValue(chart);
|
|
135
|
-
svg.appendChild(el(doc, "line", {
|
|
136
|
-
x1: String(plot.x0),
|
|
137
|
-
y1: String(plot.y0),
|
|
138
|
-
x2: String(plot.x0),
|
|
139
|
-
y2: String(plot.y1),
|
|
140
|
-
stroke: "#999"
|
|
141
|
-
}));
|
|
142
|
-
svg.appendChild(el(doc, "line", {
|
|
143
|
-
x1: String(plot.x0),
|
|
144
|
-
y1: String(plot.y1),
|
|
145
|
-
x2: String(plot.x1),
|
|
146
|
-
y2: String(plot.y1),
|
|
147
|
-
stroke: "#999"
|
|
148
|
-
}));
|
|
149
|
-
if (max <= 0 || chart.labels.length === 0 || chart.series.length === 0) return;
|
|
150
|
-
const groupWidth = (plot.x1 - plot.x0) / chart.labels.length;
|
|
151
|
-
const barWidth = Math.max(1, groupWidth / chart.series.length * .7);
|
|
152
|
-
chart.series.forEach((series, si) => {
|
|
153
|
-
const color = CHART_PALETTE[si % CHART_PALETTE.length];
|
|
154
|
-
series.data.forEach((value, li) => {
|
|
155
|
-
if (li >= chart.labels.length) return;
|
|
156
|
-
const height = (plot.y1 - plot.y0) * value / max;
|
|
157
|
-
const x = plot.x0 + li * groupWidth + (groupWidth - barWidth * chart.series.length) / 2 + si * barWidth;
|
|
158
|
-
const y = plot.y1 - height;
|
|
159
|
-
svg.appendChild(el(doc, "rect", {
|
|
160
|
-
x: String(x),
|
|
161
|
-
y: String(y),
|
|
162
|
-
width: String(barWidth),
|
|
163
|
-
height: String(height),
|
|
164
|
-
fill: color,
|
|
165
|
-
"data-series": series.name ?? String(si)
|
|
166
|
-
}));
|
|
167
|
-
if (chart.series.length === 1) svg.appendChild(text$1(doc, x + barWidth / 2, y - 2, fmt(value), { "text-anchor": "middle" }));
|
|
168
|
-
});
|
|
169
|
-
});
|
|
170
|
-
chart.labels.forEach((label, li) => {
|
|
171
|
-
svg.appendChild(text$1(doc, plot.x0 + li * groupWidth + groupWidth / 2, plot.y1 + 12, label, { "text-anchor": "middle" }));
|
|
172
|
-
});
|
|
173
111
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
112
|
+
const seriesNames = (chart) => chart.series.map((series) => series.name ?? "");
|
|
113
|
+
/** echarts 的图例条目来源:饼图按标签分项,其余按系列名 */
|
|
114
|
+
const legendEntries = (chart) => chart.kind === "pie" ? chart.labels : seriesNames(chart);
|
|
115
|
+
/** `ChartSpec` → ECharts option:图表类型、数据系列、图例的唯一定义处 */
|
|
116
|
+
function toEchartsOption(chart) {
|
|
117
|
+
return {
|
|
118
|
+
animation: false,
|
|
119
|
+
title: chart.title ? {
|
|
120
|
+
text: chart.title,
|
|
121
|
+
left: "center",
|
|
122
|
+
textStyle: { fontSize: 14 }
|
|
123
|
+
} : void 0,
|
|
124
|
+
tooltip: { trigger: chart.kind === "pie" ? "item" : "axis" },
|
|
125
|
+
legend: { bottom: 0 },
|
|
126
|
+
xAxis: chart.kind === "pie" ? void 0 : {
|
|
127
|
+
type: "category",
|
|
128
|
+
data: chart.labels
|
|
129
|
+
},
|
|
130
|
+
yAxis: chart.kind === "pie" ? void 0 : { type: "value" },
|
|
131
|
+
series: chart.kind === "pie" ? chart.series.map((series) => ({
|
|
132
|
+
type: "pie",
|
|
133
|
+
name: series.name,
|
|
134
|
+
data: chart.labels.map((label, index) => ({
|
|
135
|
+
name: label,
|
|
136
|
+
value: series.data[index] ?? 0
|
|
137
|
+
}))
|
|
138
|
+
})) : chart.series.map((series) => ({
|
|
139
|
+
type: chart.kind,
|
|
140
|
+
name: series.name,
|
|
141
|
+
data: series.data
|
|
142
|
+
}))
|
|
180
143
|
};
|
|
181
|
-
const max = maxValue(chart);
|
|
182
|
-
svg.appendChild(el(doc, "line", {
|
|
183
|
-
x1: String(plot.x0),
|
|
184
|
-
y1: String(plot.y0),
|
|
185
|
-
x2: String(plot.x0),
|
|
186
|
-
y2: String(plot.y1),
|
|
187
|
-
stroke: "#999"
|
|
188
|
-
}));
|
|
189
|
-
svg.appendChild(el(doc, "line", {
|
|
190
|
-
x1: String(plot.x0),
|
|
191
|
-
y1: String(plot.y1),
|
|
192
|
-
x2: String(plot.x1),
|
|
193
|
-
y2: String(plot.y1),
|
|
194
|
-
stroke: "#999"
|
|
195
|
-
}));
|
|
196
|
-
if (max <= 0 || chart.labels.length === 0 || chart.series.length === 0) return;
|
|
197
|
-
const step = chart.labels.length > 1 ? (plot.x1 - plot.x0) / (chart.labels.length - 1) : 0;
|
|
198
|
-
const px = (li) => chart.labels.length > 1 ? plot.x0 + li * step : (plot.x0 + plot.x1) / 2;
|
|
199
|
-
const py = (v) => plot.y1 - (plot.y1 - plot.y0) * v / max;
|
|
200
|
-
chart.series.forEach((series, si) => {
|
|
201
|
-
const color = CHART_PALETTE[si % CHART_PALETTE.length];
|
|
202
|
-
const points = series.data.slice(0, chart.labels.length).map((v, li) => `${px(li)},${py(v)}`).join(" ");
|
|
203
|
-
if (points !== "") svg.appendChild(el(doc, "polyline", {
|
|
204
|
-
points,
|
|
205
|
-
fill: "none",
|
|
206
|
-
stroke: color,
|
|
207
|
-
"stroke-width": "1.5"
|
|
208
|
-
}));
|
|
209
|
-
series.data.slice(0, chart.labels.length).forEach((v, li) => {
|
|
210
|
-
svg.appendChild(el(doc, "circle", {
|
|
211
|
-
cx: String(px(li)),
|
|
212
|
-
cy: String(py(v)),
|
|
213
|
-
r: "2",
|
|
214
|
-
fill: color
|
|
215
|
-
}));
|
|
216
|
-
});
|
|
217
|
-
});
|
|
218
|
-
chart.labels.forEach((label, li) => {
|
|
219
|
-
svg.appendChild(text$1(doc, px(li), plot.y1 + 12, label, { "text-anchor": "middle" }));
|
|
220
|
-
});
|
|
221
144
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
let angle = -Math.PI / 2;
|
|
233
|
-
data.forEach((value, i) => {
|
|
234
|
-
const label = chart.labels[i] ?? String(i);
|
|
235
|
-
const color = CHART_PALETTE[i % CHART_PALETTE.length];
|
|
236
|
-
const slice = Math.max(value, 0) / total * Math.PI * 2;
|
|
237
|
-
const x0 = cx + r * Math.cos(angle);
|
|
238
|
-
const y0 = cy + r * Math.sin(angle);
|
|
239
|
-
const x1 = cx + r * Math.cos(angle + slice);
|
|
240
|
-
const y1 = cy + r * Math.sin(angle + slice);
|
|
241
|
-
const path = el(doc, "path", {
|
|
242
|
-
d: `M ${cx} ${cy} L ${x0} ${y0} A ${r} ${r} 0 ${slice > Math.PI ? "1" : "0"} 1 ${x1} ${y1} Z`,
|
|
243
|
-
fill: color,
|
|
244
|
-
"data-label": label
|
|
245
|
-
});
|
|
246
|
-
svg.appendChild(path);
|
|
247
|
-
angle += slice;
|
|
248
|
-
const ly = 40 + i * 14;
|
|
249
|
-
svg.appendChild(el(doc, "rect", {
|
|
250
|
-
x: "190",
|
|
251
|
-
y: String(ly - 7),
|
|
252
|
-
width: "8",
|
|
253
|
-
height: "8",
|
|
254
|
-
fill: color
|
|
255
|
-
}));
|
|
256
|
-
svg.appendChild(text$1(doc, 202, ly, `${label} (${fmt(value)})`));
|
|
257
|
-
});
|
|
145
|
+
/**
|
|
146
|
+
* 图表的 DOM 契约。echarts 画在 canvas 上,DOM 里读不到图表类型/系列/图例,
|
|
147
|
+
* 「同一份 ChartSpec 在各档下一致」只能靠宿主元素上的这三个属性对拍。
|
|
148
|
+
*/
|
|
149
|
+
function applyChartDomContract(container, chart) {
|
|
150
|
+
container.setAttribute("data-webskill-chart", chart.kind);
|
|
151
|
+
container.setAttribute("data-webskill-chart-series", seriesNames(chart).join("|"));
|
|
152
|
+
container.setAttribute("data-webskill-chart-legend", legendEntries(chart).join("|"));
|
|
153
|
+
if (chart.title) container.setAttribute("aria-label", chart.title);
|
|
154
|
+
else container.removeAttribute("aria-label");
|
|
258
155
|
}
|
|
259
156
|
/**
|
|
260
|
-
*
|
|
261
|
-
*
|
|
157
|
+
* 框架无关的 echarts 挂载点:三档的 React 组件与 A2UI 档的 Lit 元素共用同一实现,
|
|
158
|
+
* 图表不再因档位而异。echarts 走动态 import,不打进首屏。
|
|
262
159
|
*/
|
|
263
|
-
function
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
160
|
+
function mountEchart(container) {
|
|
161
|
+
let disposed = false;
|
|
162
|
+
let instance;
|
|
163
|
+
let queued;
|
|
164
|
+
let observer;
|
|
165
|
+
const apply = (chart) => {
|
|
166
|
+
instance?.setOption(toEchartsOption(chart), {
|
|
167
|
+
notMerge: true,
|
|
168
|
+
lazyUpdate: true
|
|
169
|
+
});
|
|
170
|
+
};
|
|
171
|
+
import("./echarts-DhNm2ene.js").then((echarts) => {
|
|
172
|
+
if (disposed) return;
|
|
173
|
+
instance = echarts.init(container);
|
|
174
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
175
|
+
observer = new ResizeObserver(() => instance?.resize());
|
|
176
|
+
observer.observe(container);
|
|
177
|
+
}
|
|
178
|
+
if (queued) apply(queued);
|
|
270
179
|
});
|
|
271
|
-
if (chart.title) svg.appendChild(text$1(doc, WIDTH / 2, 14, chart.title, {
|
|
272
|
-
"text-anchor": "middle",
|
|
273
|
-
"font-size": "11",
|
|
274
|
-
fill: "#333"
|
|
275
|
-
}));
|
|
276
|
-
switch (chart.kind) {
|
|
277
|
-
case "bar":
|
|
278
|
-
renderBar(chart, svg, doc);
|
|
279
|
-
break;
|
|
280
|
-
case "line":
|
|
281
|
-
renderLine(chart, svg, doc);
|
|
282
|
-
break;
|
|
283
|
-
case "pie":
|
|
284
|
-
renderPie(chart, svg, doc);
|
|
285
|
-
break;
|
|
286
|
-
}
|
|
287
|
-
return svg;
|
|
288
|
-
}
|
|
289
|
-
/** A2UI/OpenUI 降级:chart → table(labels 为首列,series 各为一列) */
|
|
290
|
-
function chartToTable(chart) {
|
|
291
180
|
return {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
181
|
+
setChart(chart) {
|
|
182
|
+
applyChartDomContract(container, chart);
|
|
183
|
+
queued = chart;
|
|
184
|
+
apply(chart);
|
|
185
|
+
},
|
|
186
|
+
dispose() {
|
|
187
|
+
disposed = true;
|
|
188
|
+
observer?.disconnect();
|
|
189
|
+
observer = void 0;
|
|
190
|
+
instance?.dispose();
|
|
191
|
+
instance = void 0;
|
|
192
|
+
}
|
|
295
193
|
};
|
|
296
194
|
}
|
|
297
195
|
function componentSchema(def) {
|
|
@@ -489,7 +387,7 @@ const INPUT = [
|
|
|
489
387
|
"Button"
|
|
490
388
|
];
|
|
491
389
|
/**
|
|
492
|
-
* v3 首发 catalog
|
|
390
|
+
* v3 首发 catalog:覆盖旧六类 surface(metric / chart / table / form / file / custom)的全部表达力,先窄后宽。
|
|
493
391
|
* 只依赖 zod —— 不 import 任何 UI framework(21 号文档 §5.1)。
|
|
494
392
|
*/
|
|
495
393
|
const uiCatalog = defineUiCatalog({
|
|
@@ -708,13 +606,18 @@ const uiCatalog = defineUiCatalog({
|
|
|
708
606
|
props: z.object({
|
|
709
607
|
path: z.string(),
|
|
710
608
|
label: z.string().optional(),
|
|
711
|
-
mimeType: z.string().optional()
|
|
609
|
+
mimeType: z.string().optional(),
|
|
610
|
+
/** 字节数;人类可读格式化是渲染层的事,不进 props */
|
|
611
|
+
size: z.number().int().nonnegative().optional(),
|
|
612
|
+
actions: z.array(actionRef).optional()
|
|
712
613
|
}),
|
|
614
|
+
constraints: ["size is a byte count, not a formatted string"],
|
|
713
615
|
example: {
|
|
714
616
|
component: "FileLink",
|
|
715
617
|
props: {
|
|
716
618
|
path: "report.csv",
|
|
717
|
-
label: "Download report"
|
|
619
|
+
label: "Download report",
|
|
620
|
+
size: 2411724
|
|
718
621
|
}
|
|
719
622
|
}
|
|
720
623
|
},
|
|
@@ -1067,61 +970,271 @@ function collectValues(controls, container) {
|
|
|
1067
970
|
missingRequired
|
|
1068
971
|
};
|
|
1069
972
|
}
|
|
1070
|
-
/**
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
973
|
+
/**
|
|
974
|
+
* 轻量 SVG 图表自绘(bar/line/pie,零依赖,三端复用单一来源)。
|
|
975
|
+
* 全部经 doc.createElementNS 构建(内容来自技能输出,不拼 innerHTML)。
|
|
976
|
+
*/
|
|
977
|
+
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
978
|
+
/** 固定 8 色循环调色板 */
|
|
979
|
+
const CHART_PALETTE = [
|
|
980
|
+
"#4e79a7",
|
|
981
|
+
"#f28e2b",
|
|
982
|
+
"#e15759",
|
|
983
|
+
"#76b7b2",
|
|
984
|
+
"#59a14f",
|
|
985
|
+
"#edc948",
|
|
986
|
+
"#b07aa1",
|
|
987
|
+
"#ff9da7"
|
|
988
|
+
];
|
|
989
|
+
const WIDTH = 320;
|
|
990
|
+
const HEIGHT = 200;
|
|
991
|
+
function el(doc, name, attrs) {
|
|
992
|
+
const node = doc.createElementNS(SVG_NS, name);
|
|
993
|
+
for (const [key, value] of Object.entries(attrs)) node.setAttribute(key, value);
|
|
994
|
+
return node;
|
|
995
|
+
}
|
|
996
|
+
function text(doc, x, y, content, attrs = {}) {
|
|
997
|
+
const node = el(doc, "text", {
|
|
998
|
+
x: String(x),
|
|
999
|
+
y: String(y),
|
|
1000
|
+
"font-size": "9",
|
|
1001
|
+
fill: "#555",
|
|
1002
|
+
...attrs
|
|
1003
|
+
});
|
|
1004
|
+
node.textContent = content;
|
|
1005
|
+
return node;
|
|
1006
|
+
}
|
|
1007
|
+
const fmt = (n) => Number.isInteger(n) ? String(n) : n.toFixed(1);
|
|
1008
|
+
function maxValue(chart) {
|
|
1009
|
+
let max = 0;
|
|
1010
|
+
for (const s of chart.series) for (const v of s.data) if (v > max) max = v;
|
|
1011
|
+
return max;
|
|
1012
|
+
}
|
|
1013
|
+
function renderBar(chart, svg, doc) {
|
|
1014
|
+
const plot = {
|
|
1015
|
+
x0: 30,
|
|
1016
|
+
y0: 24,
|
|
1017
|
+
x1: 312,
|
|
1018
|
+
y1: 164
|
|
1019
|
+
};
|
|
1020
|
+
const max = maxValue(chart);
|
|
1021
|
+
svg.appendChild(el(doc, "line", {
|
|
1022
|
+
x1: String(plot.x0),
|
|
1023
|
+
y1: String(plot.y0),
|
|
1024
|
+
x2: String(plot.x0),
|
|
1025
|
+
y2: String(plot.y1),
|
|
1026
|
+
stroke: "#999"
|
|
1027
|
+
}));
|
|
1028
|
+
svg.appendChild(el(doc, "line", {
|
|
1029
|
+
x1: String(plot.x0),
|
|
1030
|
+
y1: String(plot.y1),
|
|
1031
|
+
x2: String(plot.x1),
|
|
1032
|
+
y2: String(plot.y1),
|
|
1033
|
+
stroke: "#999"
|
|
1034
|
+
}));
|
|
1035
|
+
if (max <= 0 || chart.labels.length === 0 || chart.series.length === 0) return;
|
|
1036
|
+
const groupWidth = (plot.x1 - plot.x0) / chart.labels.length;
|
|
1037
|
+
const barWidth = Math.max(1, groupWidth / chart.series.length * .7);
|
|
1038
|
+
chart.series.forEach((series, si) => {
|
|
1039
|
+
const color = CHART_PALETTE[si % CHART_PALETTE.length];
|
|
1040
|
+
series.data.forEach((value, li) => {
|
|
1041
|
+
if (li >= chart.labels.length) return;
|
|
1042
|
+
const height = (plot.y1 - plot.y0) * value / max;
|
|
1043
|
+
const x = plot.x0 + li * groupWidth + (groupWidth - barWidth * chart.series.length) / 2 + si * barWidth;
|
|
1044
|
+
const y = plot.y1 - height;
|
|
1045
|
+
svg.appendChild(el(doc, "rect", {
|
|
1046
|
+
x: String(x),
|
|
1047
|
+
y: String(y),
|
|
1048
|
+
width: String(barWidth),
|
|
1049
|
+
height: String(height),
|
|
1050
|
+
fill: color,
|
|
1051
|
+
"data-series": series.name ?? String(si)
|
|
1052
|
+
}));
|
|
1053
|
+
if (chart.series.length === 1) svg.appendChild(text(doc, x + barWidth / 2, y - 2, fmt(value), { "text-anchor": "middle" }));
|
|
1054
|
+
});
|
|
1055
|
+
});
|
|
1056
|
+
chart.labels.forEach((label, li) => {
|
|
1057
|
+
svg.appendChild(text(doc, plot.x0 + li * groupWidth + groupWidth / 2, plot.y1 + 12, label, { "text-anchor": "middle" }));
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
function renderLine(chart, svg, doc) {
|
|
1061
|
+
const plot = {
|
|
1062
|
+
x0: 30,
|
|
1063
|
+
y0: 24,
|
|
1064
|
+
x1: 312,
|
|
1065
|
+
y1: 164
|
|
1066
|
+
};
|
|
1067
|
+
const max = maxValue(chart);
|
|
1068
|
+
svg.appendChild(el(doc, "line", {
|
|
1069
|
+
x1: String(plot.x0),
|
|
1070
|
+
y1: String(plot.y0),
|
|
1071
|
+
x2: String(plot.x0),
|
|
1072
|
+
y2: String(plot.y1),
|
|
1073
|
+
stroke: "#999"
|
|
1074
|
+
}));
|
|
1075
|
+
svg.appendChild(el(doc, "line", {
|
|
1076
|
+
x1: String(plot.x0),
|
|
1077
|
+
y1: String(plot.y1),
|
|
1078
|
+
x2: String(plot.x1),
|
|
1079
|
+
y2: String(plot.y1),
|
|
1080
|
+
stroke: "#999"
|
|
1081
|
+
}));
|
|
1082
|
+
if (max <= 0 || chart.labels.length === 0 || chart.series.length === 0) return;
|
|
1083
|
+
const step = chart.labels.length > 1 ? (plot.x1 - plot.x0) / (chart.labels.length - 1) : 0;
|
|
1084
|
+
const px = (li) => chart.labels.length > 1 ? plot.x0 + li * step : (plot.x0 + plot.x1) / 2;
|
|
1085
|
+
const py = (v) => plot.y1 - (plot.y1 - plot.y0) * v / max;
|
|
1086
|
+
chart.series.forEach((series, si) => {
|
|
1087
|
+
const color = CHART_PALETTE[si % CHART_PALETTE.length];
|
|
1088
|
+
const points = series.data.slice(0, chart.labels.length).map((v, li) => `${px(li)},${py(v)}`).join(" ");
|
|
1089
|
+
if (points !== "") svg.appendChild(el(doc, "polyline", {
|
|
1090
|
+
points,
|
|
1091
|
+
fill: "none",
|
|
1092
|
+
stroke: color,
|
|
1093
|
+
"stroke-width": "1.5"
|
|
1094
|
+
}));
|
|
1095
|
+
series.data.slice(0, chart.labels.length).forEach((v, li) => {
|
|
1096
|
+
svg.appendChild(el(doc, "circle", {
|
|
1097
|
+
cx: String(px(li)),
|
|
1098
|
+
cy: String(py(v)),
|
|
1099
|
+
r: "2",
|
|
1100
|
+
fill: color
|
|
1101
|
+
}));
|
|
1102
|
+
});
|
|
1103
|
+
});
|
|
1104
|
+
chart.labels.forEach((label, li) => {
|
|
1105
|
+
svg.appendChild(text(doc, px(li), plot.y1 + 12, label, { "text-anchor": "middle" }));
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
function renderPie(chart, svg, doc) {
|
|
1109
|
+
const data = (chart.series[0]?.data ?? []).slice(0, chart.labels.length);
|
|
1110
|
+
const total = data.reduce((sum, v) => sum + (v > 0 ? v : 0), 0);
|
|
1111
|
+
const cx = 90;
|
|
1112
|
+
const cy = 104;
|
|
1113
|
+
const r = 72;
|
|
1114
|
+
if (total <= 0) {
|
|
1115
|
+
svg.appendChild(text(doc, cx, cy, "No data", { "text-anchor": "middle" }));
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
let angle = -Math.PI / 2;
|
|
1119
|
+
data.forEach((value, i) => {
|
|
1120
|
+
const label = chart.labels[i] ?? String(i);
|
|
1121
|
+
const color = CHART_PALETTE[i % CHART_PALETTE.length];
|
|
1122
|
+
const slice = Math.max(value, 0) / total * Math.PI * 2;
|
|
1123
|
+
const x0 = cx + r * Math.cos(angle);
|
|
1124
|
+
const y0 = cy + r * Math.sin(angle);
|
|
1125
|
+
const x1 = cx + r * Math.cos(angle + slice);
|
|
1126
|
+
const y1 = cy + r * Math.sin(angle + slice);
|
|
1127
|
+
const path = el(doc, "path", {
|
|
1128
|
+
d: `M ${cx} ${cy} L ${x0} ${y0} A ${r} ${r} 0 ${slice > Math.PI ? "1" : "0"} 1 ${x1} ${y1} Z`,
|
|
1129
|
+
fill: color,
|
|
1130
|
+
"data-label": label
|
|
1131
|
+
});
|
|
1132
|
+
svg.appendChild(path);
|
|
1133
|
+
angle += slice;
|
|
1134
|
+
const ly = 40 + i * 14;
|
|
1135
|
+
svg.appendChild(el(doc, "rect", {
|
|
1136
|
+
x: "190",
|
|
1137
|
+
y: String(ly - 7),
|
|
1138
|
+
width: "8",
|
|
1139
|
+
height: "8",
|
|
1140
|
+
fill: color
|
|
1141
|
+
}));
|
|
1142
|
+
svg.appendChild(text(doc, 202, ly, `${label} (${fmt(value)})`));
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
/**
|
|
1146
|
+
* ChartSpec → SVG(bar 等宽柱 + 值标签;line 折线 + 点;pie 扇形 + 图例;空数据容错)
|
|
1147
|
+
* @stable
|
|
1148
|
+
*/
|
|
1149
|
+
function renderMiniChart(chart, doc) {
|
|
1150
|
+
const svg = el(doc, "svg", {
|
|
1151
|
+
viewBox: `0 0 ${WIDTH} ${HEIGHT}`,
|
|
1152
|
+
width: String(WIDTH),
|
|
1153
|
+
height: String(HEIGHT),
|
|
1154
|
+
class: `webskill-chart webskill-chart--${chart.kind}`,
|
|
1155
|
+
role: "img"
|
|
1156
|
+
});
|
|
1157
|
+
if (chart.title) svg.appendChild(text(doc, WIDTH / 2, 14, chart.title, {
|
|
1158
|
+
"text-anchor": "middle",
|
|
1159
|
+
"font-size": "11",
|
|
1160
|
+
fill: "#333"
|
|
1161
|
+
}));
|
|
1162
|
+
switch (chart.kind) {
|
|
1163
|
+
case "bar":
|
|
1164
|
+
renderBar(chart, svg, doc);
|
|
1165
|
+
break;
|
|
1166
|
+
case "line":
|
|
1167
|
+
renderLine(chart, svg, doc);
|
|
1168
|
+
break;
|
|
1169
|
+
case "pie":
|
|
1170
|
+
renderPie(chart, svg, doc);
|
|
1171
|
+
break;
|
|
1172
|
+
}
|
|
1173
|
+
return svg;
|
|
1174
|
+
}
|
|
1175
|
+
/** A2UI/OpenUI 降级:chart → table(labels 为首列,series 各为一列) */
|
|
1176
|
+
function chartToTable(chart) {
|
|
1177
|
+
return {
|
|
1178
|
+
type: "table",
|
|
1179
|
+
columns: ["label", ...chart.series.map((s, i) => s.name ?? `series ${i + 1}`)],
|
|
1180
|
+
rows: chart.labels.map((label, li) => [label, ...chart.series.map((s) => s.data[li] ?? null)])
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
/** RenderBlock → DOM(markdown/json/table/image/file/chart 六种) */
|
|
1184
|
+
function renderBlocks(container, blocks, doc) {
|
|
1185
|
+
for (const block of blocks) switch (block.type) {
|
|
1186
|
+
case "chart":
|
|
1187
|
+
container.appendChild(renderMiniChart(block.chart, doc));
|
|
1188
|
+
break;
|
|
1189
|
+
case "markdown":
|
|
1190
|
+
container.appendChild(renderMiniMarkdown(block.text, doc));
|
|
1191
|
+
break;
|
|
1192
|
+
case "json": {
|
|
1193
|
+
const pre = doc.createElement("pre");
|
|
1194
|
+
pre.className = "webskill-result__json";
|
|
1195
|
+
pre.textContent = JSON.stringify(block.data, null, 2);
|
|
1196
|
+
container.appendChild(pre);
|
|
1197
|
+
break;
|
|
1198
|
+
}
|
|
1199
|
+
case "table": {
|
|
1200
|
+
const table = doc.createElement("table");
|
|
1201
|
+
const thead = doc.createElement("thead");
|
|
1202
|
+
const headRow = doc.createElement("tr");
|
|
1203
|
+
for (const column of block.columns) {
|
|
1204
|
+
const th = doc.createElement("th");
|
|
1205
|
+
th.textContent = column;
|
|
1206
|
+
headRow.appendChild(th);
|
|
1207
|
+
}
|
|
1208
|
+
thead.appendChild(headRow);
|
|
1209
|
+
table.appendChild(thead);
|
|
1210
|
+
const tbody = doc.createElement("tbody");
|
|
1211
|
+
for (const row of block.rows) {
|
|
1212
|
+
const tr = doc.createElement("tr");
|
|
1213
|
+
for (const cell of row) {
|
|
1214
|
+
const td = doc.createElement("td");
|
|
1215
|
+
td.textContent = typeof cell === "object" ? JSON.stringify(cell) : String(cell ?? "");
|
|
1216
|
+
tr.appendChild(td);
|
|
1217
|
+
}
|
|
1218
|
+
tbody.appendChild(tr);
|
|
1219
|
+
}
|
|
1220
|
+
table.appendChild(tbody);
|
|
1221
|
+
container.appendChild(table);
|
|
1222
|
+
break;
|
|
1223
|
+
}
|
|
1224
|
+
case "image": {
|
|
1225
|
+
const img = doc.createElement("img");
|
|
1226
|
+
img.src = block.url;
|
|
1227
|
+
img.alt = block.alt ?? "";
|
|
1228
|
+
container.appendChild(img);
|
|
1229
|
+
break;
|
|
1230
|
+
}
|
|
1231
|
+
case "file": {
|
|
1232
|
+
const div = doc.createElement("div");
|
|
1233
|
+
div.className = "webskill-result__file";
|
|
1234
|
+
const meta = [block.mimeType, block.size !== void 0 ? `${block.size} bytes` : void 0].filter(Boolean).join(", ");
|
|
1235
|
+
div.textContent = meta ? `${block.path} (${meta})` : block.path;
|
|
1236
|
+
container.appendChild(div);
|
|
1237
|
+
break;
|
|
1125
1238
|
}
|
|
1126
1239
|
}
|
|
1127
1240
|
}
|
|
@@ -1360,55 +1473,6 @@ var WebFormBridge = class {
|
|
|
1360
1473
|
}
|
|
1361
1474
|
};
|
|
1362
1475
|
/**
|
|
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
1476
|
* catalog 声明树 → json-render 扁平 spec。纯数据变换,不 import json-render
|
|
1413
1477
|
* (`@webskill/ui` 保持环境无关)。
|
|
1414
1478
|
*/
|
|
@@ -1426,29 +1490,135 @@ function toJsonRenderSpec(spec) {
|
|
|
1426
1490
|
return key;
|
|
1427
1491
|
};
|
|
1428
1492
|
return {
|
|
1429
|
-
root: walk(spec),
|
|
1430
|
-
elements
|
|
1493
|
+
root: walk(spec),
|
|
1494
|
+
elements
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
/** ControlModel 的控件词汇 → catalog `Field.type`(catalog 的 8 种是超集) */
|
|
1498
|
+
const FIELD_TYPE = {
|
|
1499
|
+
text: "text",
|
|
1500
|
+
number: "number",
|
|
1501
|
+
boolean: "toggle",
|
|
1502
|
+
select: "select",
|
|
1503
|
+
textarea: "textarea"
|
|
1504
|
+
};
|
|
1505
|
+
function toOptions(options) {
|
|
1506
|
+
return (options ?? []).map((option) => ({
|
|
1507
|
+
label: option.label,
|
|
1508
|
+
value: typeof option.value === "number" ? option.value : String(option.value)
|
|
1509
|
+
}));
|
|
1510
|
+
}
|
|
1511
|
+
function toField(control) {
|
|
1512
|
+
return {
|
|
1513
|
+
component: "Field",
|
|
1514
|
+
props: {
|
|
1515
|
+
name: control.name,
|
|
1516
|
+
label: control.label,
|
|
1517
|
+
type: FIELD_TYPE[control.control],
|
|
1518
|
+
...control.required ? { required: true } : {},
|
|
1519
|
+
...control.description ? { description: control.description } : {},
|
|
1520
|
+
...control.defaultValue !== void 0 ? { defaultValue: control.defaultValue } : {},
|
|
1521
|
+
...control.options ? { options: toOptions(control.options) } : {}
|
|
1522
|
+
}
|
|
1523
|
+
};
|
|
1524
|
+
}
|
|
1525
|
+
/**
|
|
1526
|
+
* 五类 `InteractionRequest` → catalog 声明树。
|
|
1527
|
+
*
|
|
1528
|
+
* 路径 A(runtime 发起的交互请求)与路径 B(模型产出的 surface)的差别只在「谁产出这棵树」;
|
|
1529
|
+
* 产出之后四档走的是同一条 spec 渲染路径,因此这份生成器全仓只写一份。
|
|
1530
|
+
*/
|
|
1531
|
+
function interactionToUiSpec(request, labels = {}) {
|
|
1532
|
+
const model = interactionToFormModel(request);
|
|
1533
|
+
const title = labels.title ?? model.title;
|
|
1534
|
+
const cancelLabel = labels.cancelLabel ?? model.cancelLabel;
|
|
1535
|
+
const form = {
|
|
1536
|
+
component: "Form",
|
|
1537
|
+
props: {
|
|
1538
|
+
...title ? { title } : {},
|
|
1539
|
+
submitLabel: labels.submitLabel ?? model.submitLabel,
|
|
1540
|
+
...cancelLabel ? { cancelLabel } : {}
|
|
1541
|
+
},
|
|
1542
|
+
children: model.controls.map(toField)
|
|
1543
|
+
};
|
|
1544
|
+
if (model.controls.length > 0 || !model.message) return form;
|
|
1545
|
+
return {
|
|
1546
|
+
component: "Stack",
|
|
1547
|
+
children: [{
|
|
1548
|
+
component: "Text",
|
|
1549
|
+
props: { text: model.message }
|
|
1550
|
+
}, form]
|
|
1431
1551
|
};
|
|
1432
1552
|
}
|
|
1433
1553
|
const RENDER_UI_TOOL = "render_ui";
|
|
1434
1554
|
const defaultId = () => `ui-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1435
1555
|
/**
|
|
1556
|
+
* 工具描述只留一句指针:catalog 本体走 system 消息(C4)。
|
|
1557
|
+
* 工具 `description` 的长度上限与截断在各 provider 上不一致且是静默的,
|
|
1558
|
+
* 近 5000 字符的 catalog 放在那里,被截断时模型拿到的是半份规格而请求照样成功。
|
|
1559
|
+
*/
|
|
1560
|
+
const RENDER_UI_DESCRIPTION = [
|
|
1561
|
+
"Render a UI for the answer instead of prose.",
|
|
1562
|
+
"Use it for forms, tables, charts, metrics or file downloads.",
|
|
1563
|
+
"The component catalog is in the system prompt."
|
|
1564
|
+
].join(" ");
|
|
1565
|
+
const INTENTS = /* @__PURE__ */ new Set([
|
|
1566
|
+
"submit",
|
|
1567
|
+
"cancel",
|
|
1568
|
+
"select",
|
|
1569
|
+
"download",
|
|
1570
|
+
"refresh"
|
|
1571
|
+
]);
|
|
1572
|
+
function isIntent(value) {
|
|
1573
|
+
return typeof value === "string" && INTENTS.has(value);
|
|
1574
|
+
}
|
|
1575
|
+
/**
|
|
1576
|
+
* 从节点树抽出 action 能力表。能力表走事件外壳而非树内:
|
|
1577
|
+
* 树是模型可写的,nonce 放进去就等于让模型自己签发能力。
|
|
1578
|
+
* action id 取节点 id;Button 未声明 id 时退回 intent 名(同 intent 只留一条)。
|
|
1579
|
+
*/
|
|
1580
|
+
function collectActions(node, out) {
|
|
1581
|
+
if (node.component === "Button" && isIntent(node.props?.["action"])) {
|
|
1582
|
+
const intent = node.props["action"];
|
|
1583
|
+
const id = node.id ?? intent;
|
|
1584
|
+
if (!out.has(id)) out.set(id, {
|
|
1585
|
+
id,
|
|
1586
|
+
intent,
|
|
1587
|
+
...intent === "submit" ? { awaitResponse: true } : {}
|
|
1588
|
+
});
|
|
1589
|
+
}
|
|
1590
|
+
if (node.component === "Form") {
|
|
1591
|
+
if (!out.has("submit")) out.set("submit", {
|
|
1592
|
+
id: "submit",
|
|
1593
|
+
intent: "submit",
|
|
1594
|
+
awaitResponse: true
|
|
1595
|
+
});
|
|
1596
|
+
if (typeof node.props?.["cancelLabel"] === "string" && !out.has("cancel")) out.set("cancel", {
|
|
1597
|
+
id: "cancel",
|
|
1598
|
+
intent: "cancel"
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
for (const child of node.children ?? []) collectActions(child, out);
|
|
1602
|
+
}
|
|
1603
|
+
/** 节点树声明的 action 能力表(renderer 据此生成按钮,runtime 据此签发 nonce)。@experimental */
|
|
1604
|
+
function collectSpecActions(spec) {
|
|
1605
|
+
const actions = /* @__PURE__ */ new Map();
|
|
1606
|
+
collectActions(spec, actions);
|
|
1607
|
+
return [...actions.values()];
|
|
1608
|
+
}
|
|
1609
|
+
/**
|
|
1436
1610
|
* 把 catalog 变成一个工具的入参 Schema,让模型直接生成声明式 UI(21 号文档 §3):
|
|
1437
|
-
* agentLoop / UiBridge /
|
|
1611
|
+
* agentLoop / UiBridge / extractUiSpecEvents 全部零改动,装配处只多一个 externalTools 条目。
|
|
1438
1612
|
*/
|
|
1439
1613
|
function createUiCatalogToolSource(options = {}) {
|
|
1440
1614
|
const catalog = options.catalog ?? uiCatalog;
|
|
1441
1615
|
const newSurfaceId = options.newSurfaceId ?? defaultId;
|
|
1442
1616
|
return {
|
|
1443
1617
|
kind: "ui-catalog",
|
|
1618
|
+
systemPrompt: () => Promise.resolve(catalog.toPrompt()),
|
|
1444
1619
|
listToolSpecs: () => Promise.resolve([{
|
|
1445
1620
|
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"),
|
|
1621
|
+
description: RENDER_UI_DESCRIPTION,
|
|
1452
1622
|
inputSchema: {
|
|
1453
1623
|
type: "object",
|
|
1454
1624
|
properties: { spec: catalog.toJsonSchema() },
|
|
@@ -1468,65 +1638,28 @@ function createUiCatalogToolSource(options = {}) {
|
|
|
1468
1638
|
message: `The UI spec does not match the catalog: ${result.issues.map((issue) => `${issue.path}: ${issue.message}`).join("; ")}`
|
|
1469
1639
|
}
|
|
1470
1640
|
});
|
|
1471
|
-
const
|
|
1641
|
+
const node = spec;
|
|
1642
|
+
const surfaceId = newSurfaceId();
|
|
1643
|
+
const actions = collectSpecActions(node);
|
|
1472
1644
|
return Promise.resolve({
|
|
1473
1645
|
ok: true,
|
|
1474
1646
|
content: [{
|
|
1475
1647
|
type: "json",
|
|
1476
1648
|
data: { $surface: {
|
|
1477
1649
|
type: "open",
|
|
1478
|
-
|
|
1650
|
+
id: surfaceId,
|
|
1651
|
+
node,
|
|
1652
|
+
...actions.length > 0 ? { actions } : {}
|
|
1479
1653
|
} }
|
|
1480
1654
|
}, {
|
|
1481
1655
|
type: "text",
|
|
1482
|
-
text: `Rendered surface ${
|
|
1656
|
+
text: `Rendered surface ${surfaceId}.`
|
|
1483
1657
|
}]
|
|
1484
1658
|
});
|
|
1485
1659
|
}
|
|
1486
1660
|
};
|
|
1487
1661
|
}
|
|
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
|
-
}
|
|
1527
1662
|
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;
|
|
1530
1663
|
/** InteractionRequest → SDK tool UI 结构(应用聊天 UI 直接消费) */
|
|
1531
1664
|
function toVercelToolInvocation(request) {
|
|
1532
1665
|
return {
|
|
@@ -1549,20 +1682,6 @@ function fromVercelToolResult(part) {
|
|
|
1549
1682
|
value: p["output"] ?? p["result"] ?? p["value"]
|
|
1550
1683
|
};
|
|
1551
1684
|
}
|
|
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
|
-
}
|
|
1566
1685
|
/**
|
|
1567
1686
|
* 薄桥接类:request → SDK tool UI 结构 → 应用的聊天 UI(sendInteraction 回调实现)
|
|
1568
1687
|
* → SDK 回传 → InteractionResponse。
|
|
@@ -1576,154 +1695,6 @@ var VercelUiBridge = class {
|
|
|
1576
1695
|
return fromVercelToolResult(await this.#sendInteraction(toVercelToolInvocation(input)));
|
|
1577
1696
|
}
|
|
1578
1697
|
};
|
|
1579
|
-
/**
|
|
1580
|
-
* InteractionRequest ↔ openui-lang 双向转换。
|
|
1581
|
-
* openui-lang 语法(@openuidev/react-lang 实测):
|
|
1582
|
-
* - 每行一条语句 `identifier = Expression`,`root = Component(...)` 为入口
|
|
1583
|
-
* - 组件调用参数为位置参数;字符串双引号反斜杠转义
|
|
1584
|
-
* - 组件词汇(与应用 Library 约定):Form(title, children)、
|
|
1585
|
-
* Text(content)(纯展示文本,authorize 的能力描述用)、
|
|
1586
|
-
* TextField(name, label, required?, defaultValue?)、NumberField、BooleanField(name, label, default?)、
|
|
1587
|
-
* SelectField(name, label, options)、SubmitButton(label, actionType, requestId)、
|
|
1588
|
-
* CancelButton(label, actionType, requestId)
|
|
1589
|
-
*/
|
|
1590
|
-
const OPENUI_SUBMIT_ACTION = "webskill:submit";
|
|
1591
|
-
const OPENUI_CANCEL_ACTION = "webskill:cancel";
|
|
1592
|
-
/** authorize 的 Allow 专用动作(提交即批准 → value:true;Deny 走 cancel → cancelled:true) */
|
|
1593
|
-
const OPENUI_AUTHORIZE_ACTION = "webskill:authorize";
|
|
1594
|
-
const OPENUI_SURFACE_ACTION = WEBSKILL_SURFACE_ACTION;
|
|
1595
|
-
const str = (s) => JSON.stringify(s);
|
|
1596
|
-
const val = (v) => JSON.stringify(v ?? null);
|
|
1597
|
-
function fieldStatements(fields) {
|
|
1598
|
-
return fields.map((f, i) => {
|
|
1599
|
-
const id = `f${i}`;
|
|
1600
|
-
const required = f.required ? ", true" : "";
|
|
1601
|
-
switch (f.control) {
|
|
1602
|
-
case "number": return `${id} = NumberField(${str(f.name)}, ${str(f.label)}${required})`;
|
|
1603
|
-
case "boolean": return `${id} = BooleanField(${str(f.name)}, ${str(f.label)}${f.defaultValue !== void 0 ? `, ${val(f.defaultValue)}` : ""})`;
|
|
1604
|
-
case "select": return `${id} = SelectField(${str(f.name)}, ${str(f.label)}, ${val(f.options ?? [])})`;
|
|
1605
|
-
case "textarea": return `${id} = TextField(${str(f.name)}, ${str(f.label)}${required})`;
|
|
1606
|
-
default: {
|
|
1607
|
-
const args = [str(f.name), str(f.label)];
|
|
1608
|
-
if (f.required || f.defaultValue !== void 0) args.push(f.required ? "true" : "null");
|
|
1609
|
-
if (f.defaultValue !== void 0) args.push(val(f.defaultValue));
|
|
1610
|
-
return `${id} = TextField(${args.join(", ")})`;
|
|
1611
|
-
}
|
|
1612
|
-
}
|
|
1613
|
-
});
|
|
1614
|
-
}
|
|
1615
|
-
/** @experimental */
|
|
1616
|
-
function toOpenUiLang(request) {
|
|
1617
|
-
const lines = [];
|
|
1618
|
-
let fields = [];
|
|
1619
|
-
let title = null;
|
|
1620
|
-
switch (request.type) {
|
|
1621
|
-
case "ask":
|
|
1622
|
-
title = "Question";
|
|
1623
|
-
fields = [{
|
|
1624
|
-
name: "answer",
|
|
1625
|
-
label: request.message,
|
|
1626
|
-
control: "text",
|
|
1627
|
-
required: true
|
|
1628
|
-
}];
|
|
1629
|
-
break;
|
|
1630
|
-
case "confirm":
|
|
1631
|
-
title = "Confirm";
|
|
1632
|
-
fields = [{
|
|
1633
|
-
name: "confirmed",
|
|
1634
|
-
label: request.message,
|
|
1635
|
-
control: "boolean",
|
|
1636
|
-
defaultValue: request.defaultValue ?? true
|
|
1637
|
-
}];
|
|
1638
|
-
break;
|
|
1639
|
-
case "form":
|
|
1640
|
-
title = request.title ?? "Form";
|
|
1641
|
-
fields = request.fields.map((f) => ({
|
|
1642
|
-
name: f.name,
|
|
1643
|
-
label: f.label,
|
|
1644
|
-
control: f.type,
|
|
1645
|
-
...f.required ? { required: true } : {},
|
|
1646
|
-
...f.defaultValue !== void 0 ? { defaultValue: f.defaultValue } : {},
|
|
1647
|
-
...f.options ? { options: f.options } : {}
|
|
1648
|
-
}));
|
|
1649
|
-
break;
|
|
1650
|
-
case "select":
|
|
1651
|
-
title = "Select";
|
|
1652
|
-
fields = [{
|
|
1653
|
-
name: "selected",
|
|
1654
|
-
label: request.message,
|
|
1655
|
-
control: "select",
|
|
1656
|
-
options: request.options
|
|
1657
|
-
}];
|
|
1658
|
-
break;
|
|
1659
|
-
case "authorize":
|
|
1660
|
-
title = "Authorization required";
|
|
1661
|
-
fields = [];
|
|
1662
|
-
break;
|
|
1663
|
-
}
|
|
1664
|
-
lines.push(...fieldStatements(fields));
|
|
1665
|
-
const children = fields.map((_, i) => `f${i}`);
|
|
1666
|
-
const isAuthorize = request.type === "authorize";
|
|
1667
|
-
if (isAuthorize) {
|
|
1668
|
-
lines.push(`message = Text(${str(request.message)})`);
|
|
1669
|
-
children.push("message");
|
|
1670
|
-
}
|
|
1671
|
-
const submitAction = isAuthorize ? OPENUI_AUTHORIZE_ACTION : OPENUI_SUBMIT_ACTION;
|
|
1672
|
-
lines.push(`submit = SubmitButton(${str(isAuthorize ? "Allow" : "Submit")}, ${str(submitAction)}, ${str(request.id)})`);
|
|
1673
|
-
lines.push(`cancel = CancelButton(${str(isAuthorize ? "Deny" : "Cancel")}, ${str(OPENUI_CANCEL_ACTION)}, ${str(request.id)})`);
|
|
1674
|
-
lines.push(`root = Form(${val(title)}, [${[
|
|
1675
|
-
...children,
|
|
1676
|
-
"submit",
|
|
1677
|
-
"cancel"
|
|
1678
|
-
].join(", ")}])`);
|
|
1679
|
-
return lines.join("\n");
|
|
1680
|
-
}
|
|
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
|
-
/**
|
|
1697
|
-
* OpenUI ActionEvent → InteractionResponse(formState 优先,取消单独分支)
|
|
1698
|
-
* @experimental
|
|
1699
|
-
*/
|
|
1700
|
-
function fromOpenUiAction(action) {
|
|
1701
|
-
const a = action ?? {};
|
|
1702
|
-
const id = String(a.params?.["id"] ?? "");
|
|
1703
|
-
if (a.type === "webskill:cancel") return {
|
|
1704
|
-
id,
|
|
1705
|
-
cancelled: true
|
|
1706
|
-
};
|
|
1707
|
-
if (a.type === "webskill:authorize") return {
|
|
1708
|
-
id,
|
|
1709
|
-
value: true
|
|
1710
|
-
};
|
|
1711
|
-
if (a.type === "webskill:submit") {
|
|
1712
|
-
const formState = a.formState;
|
|
1713
|
-
if (formState) return {
|
|
1714
|
-
id,
|
|
1715
|
-
value: Object.fromEntries(Object.entries(formState).map(([key, v]) => [key, typeof v === "object" && v !== null && "value" in v ? v.value : v]))
|
|
1716
|
-
};
|
|
1717
|
-
return {
|
|
1718
|
-
id,
|
|
1719
|
-
value: a.params?.["values"]
|
|
1720
|
-
};
|
|
1721
|
-
}
|
|
1722
|
-
return {
|
|
1723
|
-
id,
|
|
1724
|
-
value: a.params
|
|
1725
|
-
};
|
|
1726
|
-
}
|
|
1727
1698
|
const literal = (value) => JSON.stringify(value ?? null);
|
|
1728
1699
|
/** 组件的位置参数顺序 = catalog props 的 schema key 顺序(OpenUI 的 param map 同源) */
|
|
1729
1700
|
function propOrder(catalog) {
|
|
@@ -1756,194 +1727,50 @@ function toOpenUiSpecLang(spec, catalog = uiCatalog) {
|
|
|
1756
1727
|
const rootId = walk(spec);
|
|
1757
1728
|
return [...lines.filter((line) => !line.startsWith(`${rootId} =`)), `root = ${lines.find((line) => line.startsWith(`${rootId} =`)).slice(rootId.length + 3)}`].join("\n");
|
|
1758
1729
|
}
|
|
1730
|
+
const WEBSKILL_SURFACE_ACTION = "webskill:surface-action";
|
|
1731
|
+
function toUiSurfaceActionDispatch(snapshot, action, value) {
|
|
1732
|
+
return {
|
|
1733
|
+
...snapshot.runId ? { runId: snapshot.runId } : {},
|
|
1734
|
+
surfaceId: snapshot.id,
|
|
1735
|
+
actionId: action.id,
|
|
1736
|
+
intent: action.intent,
|
|
1737
|
+
...action.nonce ? { nonce: action.nonce } : {},
|
|
1738
|
+
...value ? { value } : {}
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
/** Returns undefined for malformed or non-WebSkill renderer events. @experimental */
|
|
1742
|
+
function fromUiSurfaceActionDispatch(value) {
|
|
1743
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
1744
|
+
const event = value;
|
|
1745
|
+
if (typeof event["surfaceId"] !== "string" || typeof event["actionId"] !== "string" || !isSurfaceActionIntent(event["intent"])) return;
|
|
1746
|
+
return {
|
|
1747
|
+
...typeof event["runId"] === "string" ? { runId: event["runId"] } : {},
|
|
1748
|
+
surfaceId: event["surfaceId"],
|
|
1749
|
+
actionId: event["actionId"],
|
|
1750
|
+
intent: event["intent"],
|
|
1751
|
+
...typeof event["nonce"] === "string" ? { nonce: event["nonce"] } : {},
|
|
1752
|
+
...isRecord(event["value"]) ? { value: event["value"] } : {},
|
|
1753
|
+
...event["cancelled"] === true ? { cancelled: true } : {}
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
1756
|
+
function isSurfaceActionIntent(value) {
|
|
1757
|
+
return value === "submit" || value === "cancel" || value === "select" || value === "download" || value === "refresh";
|
|
1758
|
+
}
|
|
1759
|
+
function isRecord(value) {
|
|
1760
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1761
|
+
}
|
|
1759
1762
|
/**
|
|
1760
|
-
*
|
|
1763
|
+
* A2UI 协议常量与 surface 动作编解码(v0.9.1)。
|
|
1761
1764
|
* 依据 https://a2ui.org/specification/v0.9-a2ui/:
|
|
1762
1765
|
* - 信封 {version, createSurface|updateComponents|updateDataModel|deleteSurface}
|
|
1763
1766
|
* - 组件为扁平邻接表(id 引用),root 必需
|
|
1764
1767
|
* - 输入组件与数据模型经 JSON Pointer 双向绑定;action.context 回传
|
|
1765
|
-
* - createSurface.sendDataModel: true → 客户端动作时携带完整数据模型
|
|
1766
1768
|
*
|
|
1767
|
-
*
|
|
1768
|
-
* cancel → {name: 'webskill:cancel', context: {requestId}}。
|
|
1769
|
+
* 声明树 → A2UI 消息的转换在 `specToA2ui.ts`(四档共用的单一渲染路径)。
|
|
1769
1770
|
*/
|
|
1770
1771
|
const A2UI_VERSION = "v0.9.1";
|
|
1771
1772
|
const A2UI_BASIC_CATALOG_ID = "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json";
|
|
1772
|
-
const A2UI_SUBMIT_ACTION = "webskill:submit";
|
|
1773
|
-
const A2UI_CANCEL_ACTION = "webskill:cancel";
|
|
1774
1773
|
const A2UI_SURFACE_ACTION = WEBSKILL_SURFACE_ACTION;
|
|
1775
|
-
const text = (id, content, variant) => ({
|
|
1776
|
-
id,
|
|
1777
|
-
component: "Text",
|
|
1778
|
-
text: content,
|
|
1779
|
-
...variant ? { variant } : {}
|
|
1780
|
-
});
|
|
1781
|
-
/** @experimental */
|
|
1782
|
-
function toA2uiMessages(request) {
|
|
1783
|
-
const surfaceId = `webskill-${request.id}`;
|
|
1784
|
-
const components = [];
|
|
1785
|
-
const children = [];
|
|
1786
|
-
const defaults = {};
|
|
1787
|
-
if (request.type === "form" && request.title) {
|
|
1788
|
-
components.push(text("title", request.title, "h3"));
|
|
1789
|
-
children.push("title");
|
|
1790
|
-
}
|
|
1791
|
-
if (request.type === "authorize") {
|
|
1792
|
-
components.push(text("title", "Authorization required", "h3"));
|
|
1793
|
-
children.push("title");
|
|
1794
|
-
}
|
|
1795
|
-
const message = request.type === "ask" || request.type === "confirm" || request.type === "select" || request.type === "authorize" ? request.message : void 0;
|
|
1796
|
-
if (message) {
|
|
1797
|
-
components.push(text("message", message));
|
|
1798
|
-
children.push("message");
|
|
1799
|
-
}
|
|
1800
|
-
const fields = request.type === "form" ? request.fields.map((f) => ({ ...f })) : request.type === "ask" ? [{
|
|
1801
|
-
name: "answer",
|
|
1802
|
-
label: request.message,
|
|
1803
|
-
type: "text",
|
|
1804
|
-
required: true
|
|
1805
|
-
}] : request.type === "confirm" ? [{
|
|
1806
|
-
name: "confirmed",
|
|
1807
|
-
label: request.message,
|
|
1808
|
-
type: "boolean",
|
|
1809
|
-
defaultValue: request.defaultValue ?? true
|
|
1810
|
-
}] : request.type === "authorize" ? [] : [{
|
|
1811
|
-
name: "selected",
|
|
1812
|
-
label: request.message,
|
|
1813
|
-
type: "select",
|
|
1814
|
-
options: request.options
|
|
1815
|
-
}];
|
|
1816
|
-
for (const field of fields) {
|
|
1817
|
-
const path = `/form/${field.name}`;
|
|
1818
|
-
if (field.defaultValue !== void 0) defaults[field.name] = field.defaultValue;
|
|
1819
|
-
const checks = field.required ? [{
|
|
1820
|
-
call: "required",
|
|
1821
|
-
args: { value: { path } },
|
|
1822
|
-
message: `${field.label} is required`
|
|
1823
|
-
}] : void 0;
|
|
1824
|
-
if (field.type === "boolean") components.push({
|
|
1825
|
-
id: `field_${field.name}`,
|
|
1826
|
-
component: "CheckBox",
|
|
1827
|
-
label: field.label,
|
|
1828
|
-
value: { path },
|
|
1829
|
-
...checks ? { checks } : {}
|
|
1830
|
-
});
|
|
1831
|
-
else if (field.type === "select") components.push({
|
|
1832
|
-
id: `field_${field.name}`,
|
|
1833
|
-
component: "ChoicePicker",
|
|
1834
|
-
variant: "mutuallyExclusive",
|
|
1835
|
-
label: field.label,
|
|
1836
|
-
options: (field.options ?? []).map((o) => ({
|
|
1837
|
-
label: o.label,
|
|
1838
|
-
value: typeof o.value === "string" ? o.value : JSON.stringify(o.value)
|
|
1839
|
-
})),
|
|
1840
|
-
value: { path },
|
|
1841
|
-
...checks ? { checks } : {}
|
|
1842
|
-
});
|
|
1843
|
-
else components.push({
|
|
1844
|
-
id: `field_${field.name}`,
|
|
1845
|
-
component: "TextField",
|
|
1846
|
-
label: field.label,
|
|
1847
|
-
value: { path },
|
|
1848
|
-
variant: field.type === "textarea" ? "longText" : "shortText",
|
|
1849
|
-
...checks ? { checks } : {}
|
|
1850
|
-
});
|
|
1851
|
-
children.push(`field_${field.name}`);
|
|
1852
|
-
}
|
|
1853
|
-
components.push(text("submit_label", request.type === "authorize" ? "Allow" : "Submit"));
|
|
1854
|
-
components.push({
|
|
1855
|
-
id: "submit",
|
|
1856
|
-
component: "Button",
|
|
1857
|
-
child: "submit_label",
|
|
1858
|
-
variant: "primary",
|
|
1859
|
-
action: { event: {
|
|
1860
|
-
name: A2UI_SUBMIT_ACTION,
|
|
1861
|
-
context: { requestId: request.id }
|
|
1862
|
-
} }
|
|
1863
|
-
});
|
|
1864
|
-
components.push(text("cancel_label", request.type === "authorize" ? "Deny" : "Cancel"));
|
|
1865
|
-
components.push({
|
|
1866
|
-
id: "cancel",
|
|
1867
|
-
component: "Button",
|
|
1868
|
-
child: "cancel_label",
|
|
1869
|
-
variant: "borderless",
|
|
1870
|
-
action: { event: {
|
|
1871
|
-
name: A2UI_CANCEL_ACTION,
|
|
1872
|
-
context: { requestId: request.id }
|
|
1873
|
-
} }
|
|
1874
|
-
});
|
|
1875
|
-
children.push("submit", "cancel");
|
|
1876
|
-
components.unshift({
|
|
1877
|
-
id: "root",
|
|
1878
|
-
component: "Column",
|
|
1879
|
-
children,
|
|
1880
|
-
justify: "start",
|
|
1881
|
-
align: "stretch"
|
|
1882
|
-
});
|
|
1883
|
-
const messages = [{
|
|
1884
|
-
version: A2UI_VERSION,
|
|
1885
|
-
createSurface: {
|
|
1886
|
-
surfaceId,
|
|
1887
|
-
catalogId: A2UI_BASIC_CATALOG_ID,
|
|
1888
|
-
sendDataModel: true
|
|
1889
|
-
}
|
|
1890
|
-
}, {
|
|
1891
|
-
version: A2UI_VERSION,
|
|
1892
|
-
updateComponents: {
|
|
1893
|
-
surfaceId,
|
|
1894
|
-
components
|
|
1895
|
-
}
|
|
1896
|
-
}];
|
|
1897
|
-
if (Object.keys(defaults).length > 0) messages.push({
|
|
1898
|
-
version: A2UI_VERSION,
|
|
1899
|
-
updateDataModel: {
|
|
1900
|
-
surfaceId,
|
|
1901
|
-
path: "/form",
|
|
1902
|
-
value: defaults
|
|
1903
|
-
}
|
|
1904
|
-
});
|
|
1905
|
-
return messages;
|
|
1906
|
-
}
|
|
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
1774
|
/** Decodes a nonce-bound action emitted by a WebSkill A2UI extension host. @experimental */
|
|
1948
1775
|
function fromA2uiSurfaceAction(event) {
|
|
1949
1776
|
const action = event;
|
|
@@ -1957,76 +1784,6 @@ function toA2uiSurfaceAction(snapshot, action, value) {
|
|
|
1957
1784
|
context: toUiSurfaceActionDispatch(snapshot, action, value)
|
|
1958
1785
|
};
|
|
1959
1786
|
}
|
|
1960
|
-
/**
|
|
1961
|
-
* 按 InteractionRequest.type 解码提交值(绑定模型原样回传的是表单对象):
|
|
1962
|
-
* confirm 仅 confirmed===true 批准;select 还原原始 option 值(非字符串值经 JSON 编码比对);
|
|
1963
|
-
* form 的 number 字段转 number(NaN 保留原值);ask 取 answer;authorize 提交即批准。
|
|
1964
|
-
* 纯转换器:任何直接使用 fromA2uiAction 的消费者都应经此拿到正确类型。
|
|
1965
|
-
* @experimental
|
|
1966
|
-
*/
|
|
1967
|
-
function decodeInteractionResponse(request, response) {
|
|
1968
|
-
if (response.cancelled) return response;
|
|
1969
|
-
const values = response.value;
|
|
1970
|
-
switch (request.type) {
|
|
1971
|
-
case "confirm": return {
|
|
1972
|
-
...response,
|
|
1973
|
-
value: values?.confirmed === true
|
|
1974
|
-
};
|
|
1975
|
-
case "ask": return {
|
|
1976
|
-
...response,
|
|
1977
|
-
value: values?.answer
|
|
1978
|
-
};
|
|
1979
|
-
case "select": {
|
|
1980
|
-
const raw = values?.selected;
|
|
1981
|
-
const option = request.options.find((o) => o.value === raw || JSON.stringify(o.value) === raw);
|
|
1982
|
-
return {
|
|
1983
|
-
...response,
|
|
1984
|
-
value: option ? option.value : raw
|
|
1985
|
-
};
|
|
1986
|
-
}
|
|
1987
|
-
case "form": {
|
|
1988
|
-
if (typeof values !== "object" || values === null) return response;
|
|
1989
|
-
const out = { ...values };
|
|
1990
|
-
for (const field of request.fields) if (field.type === "number" && out[field.name] !== void 0) {
|
|
1991
|
-
const n = Number(out[field.name]);
|
|
1992
|
-
if (!Number.isNaN(n)) out[field.name] = n;
|
|
1993
|
-
}
|
|
1994
|
-
return {
|
|
1995
|
-
...response,
|
|
1996
|
-
value: out
|
|
1997
|
-
};
|
|
1998
|
-
}
|
|
1999
|
-
case "authorize": return {
|
|
2000
|
-
...response,
|
|
2001
|
-
value: true
|
|
2002
|
-
};
|
|
2003
|
-
default: return response;
|
|
2004
|
-
}
|
|
2005
|
-
}
|
|
2006
|
-
/**
|
|
2007
|
-
* A2UI client→server action 事件 → InteractionResponse。
|
|
2008
|
-
* 传入 request 时按类型解码提交值(见 decodeInteractionResponse)。
|
|
2009
|
-
* @experimental
|
|
2010
|
-
*/
|
|
2011
|
-
function fromA2uiAction(event, request) {
|
|
2012
|
-
const e = event ?? {};
|
|
2013
|
-
const id = String(e.context?.["requestId"] ?? "");
|
|
2014
|
-
if (e.name === "webskill:cancel") return {
|
|
2015
|
-
id,
|
|
2016
|
-
cancelled: true
|
|
2017
|
-
};
|
|
2018
|
-
if (e.name === "webskill:submit") {
|
|
2019
|
-
const response = {
|
|
2020
|
-
id,
|
|
2021
|
-
value: e.context?.["values"] ?? e.context?.["form"] ?? e.context
|
|
2022
|
-
};
|
|
2023
|
-
return request ? decodeInteractionResponse(request, response) : response;
|
|
2024
|
-
}
|
|
2025
|
-
return {
|
|
2026
|
-
id,
|
|
2027
|
-
value: e.context
|
|
2028
|
-
};
|
|
2029
|
-
}
|
|
2030
1787
|
/** 声明树里按钮触发的事件名(宿主据 context.actionId 回传 surface action) */
|
|
2031
1788
|
const A2UI_SPEC_ACTION = "webskill:spec-action";
|
|
2032
1789
|
/** 表单值在 A2UI 数据模型里的根路径 */
|
|
@@ -2122,73 +1879,6 @@ function fromA2uiSpecAction(event) {
|
|
|
2122
1879
|
context
|
|
2123
1880
|
};
|
|
2124
1881
|
}
|
|
2125
|
-
let loading;
|
|
2126
|
-
function loadA2uiRuntime() {
|
|
2127
|
-
loading ??= (async () => {
|
|
2128
|
-
try {
|
|
2129
|
-
const [core, lit] = await Promise.all([import("@a2ui/web_core/v0_9"), import("@a2ui/lit/v0_9")]);
|
|
2130
|
-
lit.A2uiSurface;
|
|
2131
|
-
return {
|
|
2132
|
-
MessageProcessor: core.MessageProcessor,
|
|
2133
|
-
basicCatalog: lit.basicCatalog
|
|
2134
|
-
};
|
|
2135
|
-
} catch (e) {
|
|
2136
|
-
throw new WebSkillError("UI_UNAVAILABLE", "The A2UI Lit runtime is unavailable; install @a2ui/lit and @a2ui/web_core to use LitRendererBridge", e);
|
|
2137
|
-
}
|
|
2138
|
-
})();
|
|
2139
|
-
return loading;
|
|
2140
|
-
}
|
|
2141
|
-
/** @experimental */
|
|
2142
|
-
var LitRendererBridge = class {
|
|
2143
|
-
#mount;
|
|
2144
|
-
#doc;
|
|
2145
|
-
/** 进行中的交互:id → resolve 与 surface 元素(cancel 时清理并以 cancelled resolve) */
|
|
2146
|
-
#pending = /* @__PURE__ */ new Map();
|
|
2147
|
-
constructor(deps) {
|
|
2148
|
-
this.#mount = deps.mount;
|
|
2149
|
-
this.#doc = deps.document ?? deps.mount.ownerDocument;
|
|
2150
|
-
}
|
|
2151
|
-
async request(input) {
|
|
2152
|
-
const { MessageProcessor, basicCatalog } = await loadA2uiRuntime();
|
|
2153
|
-
const messages = toA2uiMessages(input);
|
|
2154
|
-
const surfaceId = `webskill-${input.id}`;
|
|
2155
|
-
return new Promise((resolve) => {
|
|
2156
|
-
const entry = { resolve };
|
|
2157
|
-
this.#pending.set(input.id, entry);
|
|
2158
|
-
const processor = new MessageProcessor([basicCatalog], (action) => {
|
|
2159
|
-
const surfaces = processor.getClientDataModel("v0.9.1")?.surfaces;
|
|
2160
|
-
const formValues = typeof surfaces?.[surfaceId] === "object" && surfaces[surfaceId] !== null ? surfaces[surfaceId]["form"] ?? action.context : action.context;
|
|
2161
|
-
this.#pending.delete(input.id);
|
|
2162
|
-
entry.surfaceEl?.remove();
|
|
2163
|
-
resolve(fromA2uiAction({
|
|
2164
|
-
...action,
|
|
2165
|
-
context: {
|
|
2166
|
-
...action.context,
|
|
2167
|
-
values: formValues
|
|
2168
|
-
}
|
|
2169
|
-
}, input));
|
|
2170
|
-
}, { version: "v0.9.1" });
|
|
2171
|
-
processor.onSurfaceCreated((surface) => {
|
|
2172
|
-
const el = this.#doc.createElement("a2ui-surface");
|
|
2173
|
-
el.surface = surface;
|
|
2174
|
-
entry.surfaceEl = el;
|
|
2175
|
-
this.#mount.appendChild(el);
|
|
2176
|
-
});
|
|
2177
|
-
processor.processMessages(messages);
|
|
2178
|
-
});
|
|
2179
|
-
}
|
|
2180
|
-
/** runtime 交互超时/取消:移除 surface 并以 cancelled resolve pending Promise(防悬挂 + DOM 残留) */
|
|
2181
|
-
cancel(id) {
|
|
2182
|
-
const entry = this.#pending.get(id);
|
|
2183
|
-
if (!entry) return;
|
|
2184
|
-
this.#pending.delete(id);
|
|
2185
|
-
entry.surfaceEl?.remove();
|
|
2186
|
-
entry.resolve({
|
|
2187
|
-
id,
|
|
2188
|
-
cancelled: true
|
|
2189
|
-
});
|
|
2190
|
-
}
|
|
2191
|
-
};
|
|
2192
1882
|
/**
|
|
2193
1883
|
* BYOC Lit 元素的加载入口。
|
|
2194
1884
|
*
|
|
@@ -2198,7 +1888,7 @@ var LitRendererBridge = class {
|
|
|
2198
1888
|
*/
|
|
2199
1889
|
async function loadWebSkillLitCatalog() {
|
|
2200
1890
|
try {
|
|
2201
|
-
const { webskillLitCatalog } = await import("./webskillLitCatalog-
|
|
1891
|
+
const { webskillLitCatalog } = await import("./webskillLitCatalog-CSTbhBe_-CYIs5BX8.js");
|
|
2202
1892
|
return webskillLitCatalog();
|
|
2203
1893
|
} catch (cause) {
|
|
2204
1894
|
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);
|
|
@@ -2223,4 +1913,4 @@ async function loadOpenUiPeers() {
|
|
|
2223
1913
|
}
|
|
2224
1914
|
|
|
2225
1915
|
//#endregion
|
|
2226
|
-
export {
|
|
1916
|
+
export { toA2uiSpecMessages as A, a2uiComponentSchema as B, interactionToUiSpec as C, renderMiniChart as D, renderBlocks as E, toVercelToolInvocation as F, mountEchart as G, buildA2uiCatalogDefinition as H, A2UI_CHILDREN_PROP as I, renderMiniMarkdown as K, A2UI_COMMON_TYPES as L, toJsonRenderSpec as M, toOpenUiSpecLang as N, renderRenderResult as O, toUiSurfaceActionDispatch as P, UI_CATALOG_GROUPS as R, interactionToFormModel as S, loadWebSkillLitCatalog as T, chartSpecFromProps as U, a2uiComponentShapes as V, defineUiCatalog as W, ensureStyles as _, A2UI_VERSION as a, fromUiSurfaceActionDispatch as b, VERCEL_INTERACTION_TOOL_NAME as c, WEBSKILL_SURFACE_ACTION as d, WebFormBridge as f, createUiCatalogToolSource as g, collectValues as h, A2UI_SURFACE_ACTION as i, toA2uiSurfaceAction as j, shapeInteractionValue as k, VercelUiBridge as l, collectSpecActions as m, A2UI_SPEC_ACTION as n, CHART_PALETTE as o, chartToTable as p, uiCatalog as q, A2UI_SPEC_FORM_PATH as r, RENDER_UI_TOOL as s, A2UI_BASIC_CATALOG_ID as t, WEBSKILL_STYLES_CSS as u, fromA2uiSpecAction as v, loadOpenUiPeers as w, fromVercelToolResult as x, fromA2uiSurfaceAction as y, WEBSKILL_A2UI_CATALOG_ID as z };
|