@webskill/sdk 0.1.1 → 0.1.2
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 +1 -1
- package/dist/browser.js +3 -3
- package/dist/{dist-D5fsiETF.js → dist-DRsnVw6A.js} +44 -20
- package/dist/{memoryArtifactStore-C9lFVqPF-U8nXvqL9.js → dist-DS1sfgHa.js} +1 -48
- package/dist/dist-DxGyAznR.js +1058 -0
- package/dist/{dist-Cc3Mqi3_.js → dist-SarUS8EP.js} +2 -2
- package/dist/governance.d.ts +2 -2
- package/dist/governance.js +2 -2
- package/dist/{index-Dc4X2N54.d.ts → index-DkPK44Ji.d.ts} +1 -1
- package/dist/{index-oG5Nzgb9.d.ts → index-fjRF4H5o.d.ts} +2 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/mcp.d.ts +6 -1
- package/dist/mcp.js +5 -3
- package/dist/memoryArtifactStore-C9lFVqPF-yFz6yJj0.js +48 -0
- package/dist/node.d.ts +2 -2
- package/dist/node.js +3 -3
- package/dist/{testing-pn3NhXSV.js → testing-CbM6rJ-E.js} +1 -1
- package/dist/testing.js +2 -2
- package/dist/ui-react.js +1 -1
- package/dist/ui-vue.js +1 -1
- package/dist/ui.d.ts +2 -11
- package/dist/ui.js +2 -2
- package/package.json +1 -1
- package/dist/dist-Dv4a1Jkm.js +0 -15628
|
@@ -0,0 +1,1058 @@
|
|
|
1
|
+
import { l as WebSkillError } from "./dist-DS1sfgHa.js";
|
|
2
|
+
|
|
3
|
+
//#region ../ui/dist/index.js
|
|
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
|
+
* 严格类型化控件值收集:
|
|
82
|
+
* - number 空串 → undefined(非 0)
|
|
83
|
+
* - select 用 option 值本身经 JSON 编码比对命中(对象值可正确命中)
|
|
84
|
+
* - required 空值列入 missingRequired(由调用方阻止提交并标记)
|
|
85
|
+
*/
|
|
86
|
+
function collectValues(controls, container) {
|
|
87
|
+
const values = {};
|
|
88
|
+
const missingRequired = [];
|
|
89
|
+
for (const control of controls) {
|
|
90
|
+
const el = container.querySelector(`[data-webskill-control="${control.name}"]`);
|
|
91
|
+
let value;
|
|
92
|
+
if (el === null) value = void 0;
|
|
93
|
+
else if (control.control === "boolean") value = el.checked;
|
|
94
|
+
else if (control.control === "number") {
|
|
95
|
+
const raw = el.value.trim();
|
|
96
|
+
value = raw === "" ? void 0 : Number(raw);
|
|
97
|
+
} else if (control.control === "select") {
|
|
98
|
+
const attr = el.value;
|
|
99
|
+
const hit = control.options?.find((o) => JSON.stringify(o.value) === attr);
|
|
100
|
+
value = hit ? hit.value : attr === "" ? void 0 : attr;
|
|
101
|
+
} else {
|
|
102
|
+
const raw = el.value;
|
|
103
|
+
value = raw === "" ? void 0 : raw;
|
|
104
|
+
}
|
|
105
|
+
values[control.name] = value;
|
|
106
|
+
if (control.required && isEmpty(value)) missingRequired.push(control.name);
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
values,
|
|
110
|
+
missingRequired
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* 最小子集 markdown → DOM:标题(#..###)、无序列表、代码块(```)、加粗、链接。
|
|
115
|
+
* 全部内容经 textContent 写入,用户内容天然转义防 HTML 注入。
|
|
116
|
+
*/
|
|
117
|
+
const INLINE_RE = /\*\*([^*]+)\*\*|\[([^\]]+)\]\(([^)\s]+)\)/g;
|
|
118
|
+
function appendInline(container, text, doc) {
|
|
119
|
+
let last = 0;
|
|
120
|
+
for (const match of text.matchAll(INLINE_RE)) {
|
|
121
|
+
const index = match.index ?? 0;
|
|
122
|
+
if (index > last) container.appendChild(doc.createTextNode(text.slice(last, index)));
|
|
123
|
+
if (match[1] !== void 0) {
|
|
124
|
+
const strong = doc.createElement("strong");
|
|
125
|
+
strong.textContent = match[1];
|
|
126
|
+
container.appendChild(strong);
|
|
127
|
+
} else {
|
|
128
|
+
const label = match[2] ?? "";
|
|
129
|
+
const url = match[3] ?? "";
|
|
130
|
+
if (/^https?:\/\//.test(url)) {
|
|
131
|
+
const a = doc.createElement("a");
|
|
132
|
+
a.href = url;
|
|
133
|
+
a.textContent = label;
|
|
134
|
+
a.rel = "noopener noreferrer";
|
|
135
|
+
container.appendChild(a);
|
|
136
|
+
} else container.appendChild(doc.createTextNode(match[0]));
|
|
137
|
+
}
|
|
138
|
+
last = index + match[0].length;
|
|
139
|
+
}
|
|
140
|
+
if (last < text.length) container.appendChild(doc.createTextNode(text.slice(last)));
|
|
141
|
+
}
|
|
142
|
+
function renderMiniMarkdown(text, doc) {
|
|
143
|
+
const root = doc.createElement("div");
|
|
144
|
+
root.className = "webskill-md";
|
|
145
|
+
const lines = text.split(/\r?\n/);
|
|
146
|
+
let i = 0;
|
|
147
|
+
while (i < lines.length) {
|
|
148
|
+
const line = lines[i] ?? "";
|
|
149
|
+
if (line.trimStart().startsWith("```")) {
|
|
150
|
+
const codeLines = [];
|
|
151
|
+
i++;
|
|
152
|
+
while (i < lines.length && !(lines[i] ?? "").trimStart().startsWith("```")) {
|
|
153
|
+
codeLines.push(lines[i] ?? "");
|
|
154
|
+
i++;
|
|
155
|
+
}
|
|
156
|
+
i++;
|
|
157
|
+
const pre = doc.createElement("pre");
|
|
158
|
+
const code = doc.createElement("code");
|
|
159
|
+
code.textContent = codeLines.join("\n");
|
|
160
|
+
pre.appendChild(code);
|
|
161
|
+
root.appendChild(pre);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const heading = /^(#{1,3})\s+(.*)$/.exec(line);
|
|
165
|
+
if (heading) {
|
|
166
|
+
const level = heading[1].length;
|
|
167
|
+
const h = doc.createElement(`h${level}`);
|
|
168
|
+
appendInline(h, heading[2] ?? "", doc);
|
|
169
|
+
root.appendChild(h);
|
|
170
|
+
i++;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (/^\s*[-*]\s+/.test(line)) {
|
|
174
|
+
const ul = doc.createElement("ul");
|
|
175
|
+
while (i < lines.length && /^\s*[-*]\s+/.test(lines[i] ?? "")) {
|
|
176
|
+
const li = doc.createElement("li");
|
|
177
|
+
appendInline(li, (lines[i] ?? "").replace(/^\s*[-*]\s+/, ""), doc);
|
|
178
|
+
ul.appendChild(li);
|
|
179
|
+
i++;
|
|
180
|
+
}
|
|
181
|
+
root.appendChild(ul);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (line.trim() === "") {
|
|
185
|
+
i++;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const p = doc.createElement("p");
|
|
189
|
+
appendInline(p, line, doc);
|
|
190
|
+
root.appendChild(p);
|
|
191
|
+
i++;
|
|
192
|
+
}
|
|
193
|
+
return root;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* 轻量 SVG 图表自绘(bar/line/pie,零依赖,三端复用单一来源)。
|
|
197
|
+
* 全部经 doc.createElementNS 构建(内容来自技能输出,不拼 innerHTML)。
|
|
198
|
+
*/
|
|
199
|
+
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
200
|
+
/** 固定 8 色循环调色板 */
|
|
201
|
+
const CHART_PALETTE = [
|
|
202
|
+
"#4e79a7",
|
|
203
|
+
"#f28e2b",
|
|
204
|
+
"#e15759",
|
|
205
|
+
"#76b7b2",
|
|
206
|
+
"#59a14f",
|
|
207
|
+
"#edc948",
|
|
208
|
+
"#b07aa1",
|
|
209
|
+
"#ff9da7"
|
|
210
|
+
];
|
|
211
|
+
const WIDTH = 320;
|
|
212
|
+
const HEIGHT = 200;
|
|
213
|
+
function el(doc, name, attrs) {
|
|
214
|
+
const node = doc.createElementNS(SVG_NS, name);
|
|
215
|
+
for (const [key, value] of Object.entries(attrs)) node.setAttribute(key, value);
|
|
216
|
+
return node;
|
|
217
|
+
}
|
|
218
|
+
function text$1(doc, x, y, content, attrs = {}) {
|
|
219
|
+
const node = el(doc, "text", {
|
|
220
|
+
x: String(x),
|
|
221
|
+
y: String(y),
|
|
222
|
+
"font-size": "9",
|
|
223
|
+
fill: "#555",
|
|
224
|
+
...attrs
|
|
225
|
+
});
|
|
226
|
+
node.textContent = content;
|
|
227
|
+
return node;
|
|
228
|
+
}
|
|
229
|
+
const fmt = (n) => Number.isInteger(n) ? String(n) : n.toFixed(1);
|
|
230
|
+
function maxValue(chart) {
|
|
231
|
+
let max = 0;
|
|
232
|
+
for (const s of chart.series) for (const v of s.data) if (v > max) max = v;
|
|
233
|
+
return max;
|
|
234
|
+
}
|
|
235
|
+
function renderBar(chart, svg, doc) {
|
|
236
|
+
const plot = {
|
|
237
|
+
x0: 30,
|
|
238
|
+
y0: 24,
|
|
239
|
+
x1: 312,
|
|
240
|
+
y1: 164
|
|
241
|
+
};
|
|
242
|
+
const max = maxValue(chart);
|
|
243
|
+
svg.appendChild(el(doc, "line", {
|
|
244
|
+
x1: String(plot.x0),
|
|
245
|
+
y1: String(plot.y0),
|
|
246
|
+
x2: String(plot.x0),
|
|
247
|
+
y2: String(plot.y1),
|
|
248
|
+
stroke: "#999"
|
|
249
|
+
}));
|
|
250
|
+
svg.appendChild(el(doc, "line", {
|
|
251
|
+
x1: String(plot.x0),
|
|
252
|
+
y1: String(plot.y1),
|
|
253
|
+
x2: String(plot.x1),
|
|
254
|
+
y2: String(plot.y1),
|
|
255
|
+
stroke: "#999"
|
|
256
|
+
}));
|
|
257
|
+
if (max <= 0 || chart.labels.length === 0 || chart.series.length === 0) return;
|
|
258
|
+
const groupWidth = (plot.x1 - plot.x0) / chart.labels.length;
|
|
259
|
+
const barWidth = Math.max(1, groupWidth / chart.series.length * .7);
|
|
260
|
+
chart.series.forEach((series, si) => {
|
|
261
|
+
const color = CHART_PALETTE[si % CHART_PALETTE.length];
|
|
262
|
+
series.data.forEach((value, li) => {
|
|
263
|
+
if (li >= chart.labels.length) return;
|
|
264
|
+
const height = (plot.y1 - plot.y0) * value / max;
|
|
265
|
+
const x = plot.x0 + li * groupWidth + (groupWidth - barWidth * chart.series.length) / 2 + si * barWidth;
|
|
266
|
+
const y = plot.y1 - height;
|
|
267
|
+
svg.appendChild(el(doc, "rect", {
|
|
268
|
+
x: String(x),
|
|
269
|
+
y: String(y),
|
|
270
|
+
width: String(barWidth),
|
|
271
|
+
height: String(height),
|
|
272
|
+
fill: color,
|
|
273
|
+
"data-series": series.name ?? String(si)
|
|
274
|
+
}));
|
|
275
|
+
if (chart.series.length === 1) svg.appendChild(text$1(doc, x + barWidth / 2, y - 2, fmt(value), { "text-anchor": "middle" }));
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
chart.labels.forEach((label, li) => {
|
|
279
|
+
svg.appendChild(text$1(doc, plot.x0 + li * groupWidth + groupWidth / 2, plot.y1 + 12, label, { "text-anchor": "middle" }));
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
function renderLine(chart, svg, doc) {
|
|
283
|
+
const plot = {
|
|
284
|
+
x0: 30,
|
|
285
|
+
y0: 24,
|
|
286
|
+
x1: 312,
|
|
287
|
+
y1: 164
|
|
288
|
+
};
|
|
289
|
+
const max = maxValue(chart);
|
|
290
|
+
svg.appendChild(el(doc, "line", {
|
|
291
|
+
x1: String(plot.x0),
|
|
292
|
+
y1: String(plot.y0),
|
|
293
|
+
x2: String(plot.x0),
|
|
294
|
+
y2: String(plot.y1),
|
|
295
|
+
stroke: "#999"
|
|
296
|
+
}));
|
|
297
|
+
svg.appendChild(el(doc, "line", {
|
|
298
|
+
x1: String(plot.x0),
|
|
299
|
+
y1: String(plot.y1),
|
|
300
|
+
x2: String(plot.x1),
|
|
301
|
+
y2: String(plot.y1),
|
|
302
|
+
stroke: "#999"
|
|
303
|
+
}));
|
|
304
|
+
if (max <= 0 || chart.labels.length === 0 || chart.series.length === 0) return;
|
|
305
|
+
const step = chart.labels.length > 1 ? (plot.x1 - plot.x0) / (chart.labels.length - 1) : 0;
|
|
306
|
+
const px = (li) => chart.labels.length > 1 ? plot.x0 + li * step : (plot.x0 + plot.x1) / 2;
|
|
307
|
+
const py = (v) => plot.y1 - (plot.y1 - plot.y0) * v / max;
|
|
308
|
+
chart.series.forEach((series, si) => {
|
|
309
|
+
const color = CHART_PALETTE[si % CHART_PALETTE.length];
|
|
310
|
+
const points = series.data.slice(0, chart.labels.length).map((v, li) => `${px(li)},${py(v)}`).join(" ");
|
|
311
|
+
if (points !== "") svg.appendChild(el(doc, "polyline", {
|
|
312
|
+
points,
|
|
313
|
+
fill: "none",
|
|
314
|
+
stroke: color,
|
|
315
|
+
"stroke-width": "1.5"
|
|
316
|
+
}));
|
|
317
|
+
series.data.slice(0, chart.labels.length).forEach((v, li) => {
|
|
318
|
+
svg.appendChild(el(doc, "circle", {
|
|
319
|
+
cx: String(px(li)),
|
|
320
|
+
cy: String(py(v)),
|
|
321
|
+
r: "2",
|
|
322
|
+
fill: color
|
|
323
|
+
}));
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
chart.labels.forEach((label, li) => {
|
|
327
|
+
svg.appendChild(text$1(doc, px(li), plot.y1 + 12, label, { "text-anchor": "middle" }));
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
function renderPie(chart, svg, doc) {
|
|
331
|
+
const data = (chart.series[0]?.data ?? []).slice(0, chart.labels.length);
|
|
332
|
+
const total = data.reduce((sum, v) => sum + (v > 0 ? v : 0), 0);
|
|
333
|
+
const cx = 90;
|
|
334
|
+
const cy = 104;
|
|
335
|
+
const r = 72;
|
|
336
|
+
if (total <= 0) {
|
|
337
|
+
svg.appendChild(text$1(doc, cx, cy, "No data", { "text-anchor": "middle" }));
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
let angle = -Math.PI / 2;
|
|
341
|
+
data.forEach((value, i) => {
|
|
342
|
+
const label = chart.labels[i] ?? String(i);
|
|
343
|
+
const color = CHART_PALETTE[i % CHART_PALETTE.length];
|
|
344
|
+
const slice = Math.max(value, 0) / total * Math.PI * 2;
|
|
345
|
+
const x0 = cx + r * Math.cos(angle);
|
|
346
|
+
const y0 = cy + r * Math.sin(angle);
|
|
347
|
+
const x1 = cx + r * Math.cos(angle + slice);
|
|
348
|
+
const y1 = cy + r * Math.sin(angle + slice);
|
|
349
|
+
const path = el(doc, "path", {
|
|
350
|
+
d: `M ${cx} ${cy} L ${x0} ${y0} A ${r} ${r} 0 ${slice > Math.PI ? "1" : "0"} 1 ${x1} ${y1} Z`,
|
|
351
|
+
fill: color,
|
|
352
|
+
"data-label": label
|
|
353
|
+
});
|
|
354
|
+
svg.appendChild(path);
|
|
355
|
+
angle += slice;
|
|
356
|
+
const ly = 40 + i * 14;
|
|
357
|
+
svg.appendChild(el(doc, "rect", {
|
|
358
|
+
x: "190",
|
|
359
|
+
y: String(ly - 7),
|
|
360
|
+
width: "8",
|
|
361
|
+
height: "8",
|
|
362
|
+
fill: color
|
|
363
|
+
}));
|
|
364
|
+
svg.appendChild(text$1(doc, 202, ly, `${label} (${fmt(value)})`));
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* ChartSpec → SVG(bar 等宽柱 + 值标签;line 折线 + 点;pie 扇形 + 图例;空数据容错)
|
|
369
|
+
* @stable
|
|
370
|
+
*/
|
|
371
|
+
function renderMiniChart(chart, doc) {
|
|
372
|
+
const svg = el(doc, "svg", {
|
|
373
|
+
viewBox: `0 0 ${WIDTH} ${HEIGHT}`,
|
|
374
|
+
width: String(WIDTH),
|
|
375
|
+
height: String(HEIGHT),
|
|
376
|
+
class: `webskill-chart webskill-chart--${chart.kind}`,
|
|
377
|
+
role: "img"
|
|
378
|
+
});
|
|
379
|
+
if (chart.title) svg.appendChild(text$1(doc, WIDTH / 2, 14, chart.title, {
|
|
380
|
+
"text-anchor": "middle",
|
|
381
|
+
"font-size": "11",
|
|
382
|
+
fill: "#333"
|
|
383
|
+
}));
|
|
384
|
+
switch (chart.kind) {
|
|
385
|
+
case "bar":
|
|
386
|
+
renderBar(chart, svg, doc);
|
|
387
|
+
break;
|
|
388
|
+
case "line":
|
|
389
|
+
renderLine(chart, svg, doc);
|
|
390
|
+
break;
|
|
391
|
+
case "pie":
|
|
392
|
+
renderPie(chart, svg, doc);
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
return svg;
|
|
396
|
+
}
|
|
397
|
+
/** A2UI/OpenUI 降级:chart → table(labels 为首列,series 各为一列) */
|
|
398
|
+
function chartToTable(chart) {
|
|
399
|
+
return {
|
|
400
|
+
type: "table",
|
|
401
|
+
columns: ["label", ...chart.series.map((s, i) => s.name ?? `series ${i + 1}`)],
|
|
402
|
+
rows: chart.labels.map((label, li) => [label, ...chart.series.map((s) => s.data[li] ?? null)])
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
/** RenderBlock → DOM(markdown/json/table/image/file/chart 六种) */
|
|
406
|
+
function renderBlocks(container, blocks, doc) {
|
|
407
|
+
for (const block of blocks) switch (block.type) {
|
|
408
|
+
case "chart":
|
|
409
|
+
container.appendChild(renderMiniChart(block.chart, doc));
|
|
410
|
+
break;
|
|
411
|
+
case "markdown":
|
|
412
|
+
container.appendChild(renderMiniMarkdown(block.text, doc));
|
|
413
|
+
break;
|
|
414
|
+
case "json": {
|
|
415
|
+
const pre = doc.createElement("pre");
|
|
416
|
+
pre.className = "webskill-result__json";
|
|
417
|
+
pre.textContent = JSON.stringify(block.data, null, 2);
|
|
418
|
+
container.appendChild(pre);
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
421
|
+
case "table": {
|
|
422
|
+
const table = doc.createElement("table");
|
|
423
|
+
const thead = doc.createElement("thead");
|
|
424
|
+
const headRow = doc.createElement("tr");
|
|
425
|
+
for (const column of block.columns) {
|
|
426
|
+
const th = doc.createElement("th");
|
|
427
|
+
th.textContent = column;
|
|
428
|
+
headRow.appendChild(th);
|
|
429
|
+
}
|
|
430
|
+
thead.appendChild(headRow);
|
|
431
|
+
table.appendChild(thead);
|
|
432
|
+
const tbody = doc.createElement("tbody");
|
|
433
|
+
for (const row of block.rows) {
|
|
434
|
+
const tr = doc.createElement("tr");
|
|
435
|
+
for (const cell of row) {
|
|
436
|
+
const td = doc.createElement("td");
|
|
437
|
+
td.textContent = typeof cell === "object" ? JSON.stringify(cell) : String(cell ?? "");
|
|
438
|
+
tr.appendChild(td);
|
|
439
|
+
}
|
|
440
|
+
tbody.appendChild(tr);
|
|
441
|
+
}
|
|
442
|
+
table.appendChild(tbody);
|
|
443
|
+
container.appendChild(table);
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
446
|
+
case "image": {
|
|
447
|
+
const img = doc.createElement("img");
|
|
448
|
+
img.src = block.url;
|
|
449
|
+
img.alt = block.alt ?? "";
|
|
450
|
+
container.appendChild(img);
|
|
451
|
+
break;
|
|
452
|
+
}
|
|
453
|
+
case "file": {
|
|
454
|
+
const div = doc.createElement("div");
|
|
455
|
+
div.className = "webskill-result__file";
|
|
456
|
+
const meta = [block.mimeType, block.size !== void 0 ? `${block.size} bytes` : void 0].filter(Boolean).join(", ");
|
|
457
|
+
div.textContent = meta ? `${block.path} (${meta})` : block.path;
|
|
458
|
+
container.appendChild(div);
|
|
459
|
+
break;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
/** 完整 RenderResultRequest → DOM(summary 头 + blocks) */
|
|
464
|
+
function renderRenderResult(request, doc) {
|
|
465
|
+
const root = doc.createElement("div");
|
|
466
|
+
root.className = "webskill-result";
|
|
467
|
+
if (request.title) {
|
|
468
|
+
const title = doc.createElement("div");
|
|
469
|
+
title.className = "webskill-form__title";
|
|
470
|
+
title.textContent = request.title;
|
|
471
|
+
root.appendChild(title);
|
|
472
|
+
}
|
|
473
|
+
if (request.summary) {
|
|
474
|
+
const summary = doc.createElement("div");
|
|
475
|
+
summary.className = "webskill-result__summary";
|
|
476
|
+
summary.textContent = request.summary;
|
|
477
|
+
root.appendChild(summary);
|
|
478
|
+
}
|
|
479
|
+
renderBlocks(root, request.blocks, doc);
|
|
480
|
+
return root;
|
|
481
|
+
}
|
|
482
|
+
/** 最小样式(BEM class,应用可整体覆写) */
|
|
483
|
+
const WEBSKILL_STYLES_CSS = `
|
|
484
|
+
.webskill-form { display: block; padding: 12px; border: 1px solid #d0d0d0; border-radius: 8px; font-family: system-ui, sans-serif; max-width: 420px; }
|
|
485
|
+
.webskill-form__title { margin: 0 0 8px; font-size: 15px; font-weight: 600; }
|
|
486
|
+
.webskill-form__message { margin: 0 0 10px; font-size: 14px; }
|
|
487
|
+
.webskill-form__control { margin-bottom: 10px; display: flex; flex-direction: column; gap: 4px; }
|
|
488
|
+
.webskill-form__control--invalid .webskill-form__input { border-color: #d33; }
|
|
489
|
+
.webskill-form__label { font-size: 13px; font-weight: 500; }
|
|
490
|
+
.webskill-form__label-required::after { content: ' *'; color: #d33; }
|
|
491
|
+
.webskill-form__description { font-size: 12px; color: #666; }
|
|
492
|
+
.webskill-form__error { font-size: 12px; color: #d33; display: none; }
|
|
493
|
+
.webskill-form__control--invalid .webskill-form__error { display: block; }
|
|
494
|
+
.webskill-form__input { padding: 6px 8px; border: 1px solid #bbb; border-radius: 4px; font-size: 14px; font-family: inherit; }
|
|
495
|
+
.webskill-form__actions { display: flex; gap: 8px; margin-top: 12px; }
|
|
496
|
+
.webskill-form__button { padding: 6px 14px; border-radius: 4px; border: 1px solid #bbb; background: #fff; cursor: pointer; font-size: 14px; }
|
|
497
|
+
.webskill-form__button--primary { background: #2563eb; border-color: #2563eb; color: #fff; }
|
|
498
|
+
.webskill-form__button--ghost { background: transparent; }
|
|
499
|
+
.webskill-result { font-family: system-ui, sans-serif; font-size: 14px; }
|
|
500
|
+
.webskill-result__summary { font-size: 12px; color: #666; margin-bottom: 6px; }
|
|
501
|
+
.webskill-result__json { background: #f5f5f5; padding: 8px; border-radius: 4px; overflow-x: auto; }
|
|
502
|
+
.webskill-result__file { padding: 4px 0; font-size: 13px; }
|
|
503
|
+
.webskill-md pre { background: #f5f5f5; padding: 8px; border-radius: 4px; overflow-x: auto; }
|
|
504
|
+
.webskill-md h1, .webskill-md h2, .webskill-md h3 { margin: 8px 0 4px; }
|
|
505
|
+
.webskill-md p { margin: 4px 0; }
|
|
506
|
+
.webskill-md ul { margin: 4px 0; padding-left: 20px; }
|
|
507
|
+
`;
|
|
508
|
+
/** 每个 document 只注入一次 */
|
|
509
|
+
function ensureStyles(doc) {
|
|
510
|
+
if (doc.querySelector("style[data-webskill]")) return;
|
|
511
|
+
const style = doc.createElement("style");
|
|
512
|
+
style.setAttribute("data-webskill", "");
|
|
513
|
+
style.textContent = WEBSKILL_STYLES_CSS;
|
|
514
|
+
doc.head.appendChild(style);
|
|
515
|
+
}
|
|
516
|
+
const CONTROL_TAG = "data-webskill-control";
|
|
517
|
+
/**
|
|
518
|
+
* 框架无关原生 DOM 的 UiBridge:request 渲染表单并返回 Promise
|
|
519
|
+
* (提交 resolve / 取消 {cancelled:true}),完成后卸载 DOM。
|
|
520
|
+
* document 可注入(jsdom 测试);BEM class 样式可覆写。
|
|
521
|
+
*/
|
|
522
|
+
var WebFormBridge = class {
|
|
523
|
+
#mount;
|
|
524
|
+
#doc;
|
|
525
|
+
#styles;
|
|
526
|
+
#progressEl;
|
|
527
|
+
constructor(options) {
|
|
528
|
+
this.#mount = options.mount;
|
|
529
|
+
this.#doc = options.document ?? options.mount.ownerDocument;
|
|
530
|
+
this.#styles = options.styles ?? true;
|
|
531
|
+
}
|
|
532
|
+
request(input) {
|
|
533
|
+
if (this.#styles) ensureStyles(this.#doc);
|
|
534
|
+
this.#clearProgress();
|
|
535
|
+
const model = interactionToFormModel(input);
|
|
536
|
+
const { form, cleanup } = this.#renderForm(model);
|
|
537
|
+
return new Promise((resolve) => {
|
|
538
|
+
form.addEventListener("submit", (event) => {
|
|
539
|
+
event.preventDefault();
|
|
540
|
+
const { values, missingRequired } = collectValues(model.controls, form);
|
|
541
|
+
if (missingRequired.length > 0) {
|
|
542
|
+
for (const name of missingRequired) form.querySelector(`[${CONTROL_TAG}="${name}"]`)?.closest(".webskill-form__control")?.classList.add("webskill-form__control--invalid");
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
cleanup();
|
|
546
|
+
resolve({
|
|
547
|
+
id: input.id,
|
|
548
|
+
value: shapeInteractionValue(model, values)
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
form.querySelector("[data-webskill-cancel]")?.addEventListener("click", (event) => {
|
|
552
|
+
event.preventDefault();
|
|
553
|
+
cleanup();
|
|
554
|
+
resolve({
|
|
555
|
+
id: input.id,
|
|
556
|
+
cancelled: true
|
|
557
|
+
});
|
|
558
|
+
});
|
|
559
|
+
this.#mount.appendChild(form);
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
async renderResult(input) {
|
|
563
|
+
if (this.#styles) ensureStyles(this.#doc);
|
|
564
|
+
this.#clearProgress();
|
|
565
|
+
this.#mount.querySelector(".webskill-result")?.remove();
|
|
566
|
+
this.#mount.appendChild(renderRenderResult(input, this.#doc));
|
|
567
|
+
}
|
|
568
|
+
async progress(input) {
|
|
569
|
+
if (!this.#progressEl) {
|
|
570
|
+
this.#progressEl = this.#doc.createElement("div");
|
|
571
|
+
this.#progressEl.className = "webskill-result__summary";
|
|
572
|
+
this.#mount.appendChild(this.#progressEl);
|
|
573
|
+
}
|
|
574
|
+
const pct = input.value !== void 0 ? ` (${Math.round(input.value * 100)}%)` : "";
|
|
575
|
+
this.#progressEl.textContent = `${input.message}${pct}`;
|
|
576
|
+
}
|
|
577
|
+
/** 新一轮交互/结果渲染开始时移除遗留 progress 元素 */
|
|
578
|
+
#clearProgress() {
|
|
579
|
+
this.#progressEl?.remove();
|
|
580
|
+
this.#progressEl = void 0;
|
|
581
|
+
}
|
|
582
|
+
#renderForm(model) {
|
|
583
|
+
const doc = this.#doc;
|
|
584
|
+
const form = doc.createElement("form");
|
|
585
|
+
form.className = "webskill-form";
|
|
586
|
+
form.noValidate = true;
|
|
587
|
+
if (model.title) {
|
|
588
|
+
const title = doc.createElement("div");
|
|
589
|
+
title.className = "webskill-form__title";
|
|
590
|
+
title.textContent = model.title;
|
|
591
|
+
form.appendChild(title);
|
|
592
|
+
}
|
|
593
|
+
if (model.message && model.kind !== "confirm" && model.kind !== "ask" && model.kind !== "select") {
|
|
594
|
+
const message = doc.createElement("p");
|
|
595
|
+
message.className = "webskill-form__message";
|
|
596
|
+
message.textContent = model.message;
|
|
597
|
+
form.appendChild(message);
|
|
598
|
+
}
|
|
599
|
+
for (const control of model.controls) form.appendChild(this.#renderControl(control));
|
|
600
|
+
const actions = doc.createElement("div");
|
|
601
|
+
actions.className = "webskill-form__actions";
|
|
602
|
+
const submit = doc.createElement("button");
|
|
603
|
+
submit.type = "submit";
|
|
604
|
+
submit.className = "webskill-form__button webskill-form__button--primary";
|
|
605
|
+
submit.textContent = model.submitLabel;
|
|
606
|
+
actions.appendChild(submit);
|
|
607
|
+
if (model.cancelLabel) {
|
|
608
|
+
const cancel = doc.createElement("button");
|
|
609
|
+
cancel.type = "button";
|
|
610
|
+
cancel.className = "webskill-form__button webskill-form__button--ghost";
|
|
611
|
+
cancel.textContent = model.cancelLabel;
|
|
612
|
+
cancel.setAttribute("data-webskill-cancel", "");
|
|
613
|
+
actions.appendChild(cancel);
|
|
614
|
+
}
|
|
615
|
+
form.appendChild(actions);
|
|
616
|
+
return {
|
|
617
|
+
form,
|
|
618
|
+
cleanup: () => form.remove()
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
#renderControl(control) {
|
|
622
|
+
const doc = this.#doc;
|
|
623
|
+
const wrapper = doc.createElement("div");
|
|
624
|
+
wrapper.className = "webskill-form__control";
|
|
625
|
+
const label = doc.createElement("label");
|
|
626
|
+
label.className = control.required ? "webskill-form__label webskill-form__label-required" : "webskill-form__label";
|
|
627
|
+
label.textContent = control.label;
|
|
628
|
+
wrapper.appendChild(label);
|
|
629
|
+
if (control.description) {
|
|
630
|
+
const desc = doc.createElement("div");
|
|
631
|
+
desc.className = "webskill-form__description";
|
|
632
|
+
desc.textContent = control.description;
|
|
633
|
+
wrapper.appendChild(desc);
|
|
634
|
+
}
|
|
635
|
+
let input;
|
|
636
|
+
if (control.control === "select") {
|
|
637
|
+
const select = doc.createElement("select");
|
|
638
|
+
select.className = "webskill-form__input";
|
|
639
|
+
for (const option of control.options ?? []) {
|
|
640
|
+
const opt = doc.createElement("option");
|
|
641
|
+
opt.value = JSON.stringify(option.value);
|
|
642
|
+
opt.textContent = option.label;
|
|
643
|
+
if (control.defaultValue !== void 0 && JSON.stringify(control.defaultValue) === opt.value) opt.selected = true;
|
|
644
|
+
select.appendChild(opt);
|
|
645
|
+
}
|
|
646
|
+
input = select;
|
|
647
|
+
} else if (control.control === "boolean") {
|
|
648
|
+
const checkbox = doc.createElement("input");
|
|
649
|
+
checkbox.type = "checkbox";
|
|
650
|
+
checkbox.className = "webskill-form__input";
|
|
651
|
+
checkbox.checked = control.defaultValue === true;
|
|
652
|
+
input = checkbox;
|
|
653
|
+
} else if (control.control === "textarea") {
|
|
654
|
+
const textarea = doc.createElement("textarea");
|
|
655
|
+
textarea.className = "webskill-form__input";
|
|
656
|
+
textarea.value = control.defaultValue !== void 0 ? String(control.defaultValue) : "";
|
|
657
|
+
input = textarea;
|
|
658
|
+
} else {
|
|
659
|
+
const textInput = doc.createElement("input");
|
|
660
|
+
textInput.type = control.control === "number" ? "number" : "text";
|
|
661
|
+
textInput.className = "webskill-form__input";
|
|
662
|
+
if (control.defaultValue !== void 0) textInput.value = String(control.defaultValue);
|
|
663
|
+
input = textInput;
|
|
664
|
+
}
|
|
665
|
+
input.setAttribute(CONTROL_TAG, control.name);
|
|
666
|
+
wrapper.appendChild(input);
|
|
667
|
+
const error = doc.createElement("div");
|
|
668
|
+
error.className = "webskill-form__error";
|
|
669
|
+
error.textContent = "This field is required";
|
|
670
|
+
wrapper.appendChild(error);
|
|
671
|
+
return wrapper;
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
const VERCEL_INTERACTION_TOOL_NAME = "webskill_interaction";
|
|
675
|
+
/** InteractionRequest → SDK tool UI 结构(应用聊天 UI 直接消费) */
|
|
676
|
+
function toVercelToolInvocation(request) {
|
|
677
|
+
return {
|
|
678
|
+
type: "tool-call",
|
|
679
|
+
toolCallId: request.id,
|
|
680
|
+
toolName: VERCEL_INTERACTION_TOOL_NAME,
|
|
681
|
+
input: request
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
/** SDK 回传(tool result / output)→ InteractionResponse */
|
|
685
|
+
function fromVercelToolResult(part) {
|
|
686
|
+
const p = part ?? {};
|
|
687
|
+
const id = String(p["toolCallId"] ?? p["id"] ?? "");
|
|
688
|
+
if (p["cancelled"] === true) return {
|
|
689
|
+
id,
|
|
690
|
+
cancelled: true
|
|
691
|
+
};
|
|
692
|
+
return {
|
|
693
|
+
id,
|
|
694
|
+
value: p["output"] ?? p["result"] ?? p["value"]
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* 薄桥接类:request → SDK tool UI 结构 → 应用的聊天 UI(sendInteraction 回调实现)
|
|
699
|
+
* → SDK 回传 → InteractionResponse。
|
|
700
|
+
*/
|
|
701
|
+
var VercelUiBridge = class {
|
|
702
|
+
#sendInteraction;
|
|
703
|
+
constructor(deps) {
|
|
704
|
+
this.#sendInteraction = deps.sendInteraction;
|
|
705
|
+
}
|
|
706
|
+
async request(input) {
|
|
707
|
+
return fromVercelToolResult(await this.#sendInteraction(toVercelToolInvocation(input)));
|
|
708
|
+
}
|
|
709
|
+
};
|
|
710
|
+
/**
|
|
711
|
+
* InteractionRequest ↔ openui-lang 双向转换。
|
|
712
|
+
* openui-lang 语法(@openuidev/react-lang 实测):
|
|
713
|
+
* - 每行一条语句 `identifier = Expression`,`root = Component(...)` 为入口
|
|
714
|
+
* - 组件调用参数为位置参数;字符串双引号反斜杠转义
|
|
715
|
+
* - 组件词汇(与应用 Library 约定):Form(title, children)、
|
|
716
|
+
* TextField(name, label, required?, defaultValue?)、NumberField、BooleanField(name, label, default?)、
|
|
717
|
+
* SelectField(name, label, options)、SubmitButton(label, actionType, requestId)、
|
|
718
|
+
* CancelButton(label, actionType, requestId)
|
|
719
|
+
*/
|
|
720
|
+
const OPENUI_SUBMIT_ACTION = "webskill:submit";
|
|
721
|
+
const OPENUI_CANCEL_ACTION = "webskill:cancel";
|
|
722
|
+
const str = (s) => JSON.stringify(s);
|
|
723
|
+
const val = (v) => JSON.stringify(v ?? null);
|
|
724
|
+
function fieldStatements(fields) {
|
|
725
|
+
return fields.map((f, i) => {
|
|
726
|
+
const id = `f${i}`;
|
|
727
|
+
const required = f.required ? ", true" : "";
|
|
728
|
+
switch (f.control) {
|
|
729
|
+
case "number": return `${id} = NumberField(${str(f.name)}, ${str(f.label)}${required})`;
|
|
730
|
+
case "boolean": return `${id} = BooleanField(${str(f.name)}, ${str(f.label)}${f.defaultValue !== void 0 ? `, ${val(f.defaultValue)}` : ""})`;
|
|
731
|
+
case "select": return `${id} = SelectField(${str(f.name)}, ${str(f.label)}, ${val(f.options ?? [])})`;
|
|
732
|
+
case "textarea": return `${id} = TextField(${str(f.name)}, ${str(f.label)}${required})`;
|
|
733
|
+
default: {
|
|
734
|
+
const args = [str(f.name), str(f.label)];
|
|
735
|
+
if (f.required || f.defaultValue !== void 0) args.push(f.required ? "true" : "null");
|
|
736
|
+
if (f.defaultValue !== void 0) args.push(val(f.defaultValue));
|
|
737
|
+
return `${id} = TextField(${args.join(", ")})`;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
/** @experimental */
|
|
743
|
+
function toOpenUiLang(request) {
|
|
744
|
+
const lines = [];
|
|
745
|
+
let fields = [];
|
|
746
|
+
let title = null;
|
|
747
|
+
switch (request.type) {
|
|
748
|
+
case "ask":
|
|
749
|
+
title = "Question";
|
|
750
|
+
fields = [{
|
|
751
|
+
name: "answer",
|
|
752
|
+
label: request.message,
|
|
753
|
+
control: "text",
|
|
754
|
+
required: true
|
|
755
|
+
}];
|
|
756
|
+
break;
|
|
757
|
+
case "confirm":
|
|
758
|
+
title = "Confirm";
|
|
759
|
+
fields = [{
|
|
760
|
+
name: "confirmed",
|
|
761
|
+
label: request.message,
|
|
762
|
+
control: "boolean",
|
|
763
|
+
defaultValue: request.defaultValue ?? true
|
|
764
|
+
}];
|
|
765
|
+
break;
|
|
766
|
+
case "form":
|
|
767
|
+
title = request.title ?? "Form";
|
|
768
|
+
fields = request.fields.map((f) => ({
|
|
769
|
+
name: f.name,
|
|
770
|
+
label: f.label,
|
|
771
|
+
control: f.type,
|
|
772
|
+
...f.required ? { required: true } : {},
|
|
773
|
+
...f.defaultValue !== void 0 ? { defaultValue: f.defaultValue } : {},
|
|
774
|
+
...f.options ? { options: f.options } : {}
|
|
775
|
+
}));
|
|
776
|
+
break;
|
|
777
|
+
case "select":
|
|
778
|
+
title = "Select";
|
|
779
|
+
fields = [{
|
|
780
|
+
name: "selected",
|
|
781
|
+
label: request.message,
|
|
782
|
+
control: "select",
|
|
783
|
+
options: request.options
|
|
784
|
+
}];
|
|
785
|
+
break;
|
|
786
|
+
case "authorize":
|
|
787
|
+
title = "Authorization required";
|
|
788
|
+
fields = [{
|
|
789
|
+
name: "approved",
|
|
790
|
+
label: request.message,
|
|
791
|
+
control: "boolean",
|
|
792
|
+
defaultValue: false
|
|
793
|
+
}];
|
|
794
|
+
break;
|
|
795
|
+
}
|
|
796
|
+
lines.push(...fieldStatements(fields));
|
|
797
|
+
const children = fields.map((_, i) => `f${i}`);
|
|
798
|
+
lines.push(`submit = SubmitButton("Submit", ${str(OPENUI_SUBMIT_ACTION)}, ${str(request.id)})`);
|
|
799
|
+
lines.push(`cancel = CancelButton("Cancel", ${str(OPENUI_CANCEL_ACTION)}, ${str(request.id)})`);
|
|
800
|
+
lines.push(`root = Form(${val(title)}, [${[
|
|
801
|
+
...children,
|
|
802
|
+
"submit",
|
|
803
|
+
"cancel"
|
|
804
|
+
].join(", ")}])`);
|
|
805
|
+
return lines.join("\n");
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* OpenUI ActionEvent → InteractionResponse(formState 优先,取消单独分支)
|
|
809
|
+
* @experimental
|
|
810
|
+
*/
|
|
811
|
+
function fromOpenUiAction(action) {
|
|
812
|
+
const a = action ?? {};
|
|
813
|
+
const id = String(a.params?.["id"] ?? "");
|
|
814
|
+
if (a.type === "webskill:cancel") return {
|
|
815
|
+
id,
|
|
816
|
+
cancelled: true
|
|
817
|
+
};
|
|
818
|
+
if (a.type === "webskill:submit") {
|
|
819
|
+
const formState = a.formState;
|
|
820
|
+
if (formState) return {
|
|
821
|
+
id,
|
|
822
|
+
value: Object.fromEntries(Object.entries(formState).map(([key, v]) => [key, typeof v === "object" && v !== null && "value" in v ? v.value : v]))
|
|
823
|
+
};
|
|
824
|
+
return {
|
|
825
|
+
id,
|
|
826
|
+
value: a.params?.["values"]
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
return {
|
|
830
|
+
id,
|
|
831
|
+
value: a.params
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* InteractionRequest → A2UI 协议消息(v0.9.1,Basic Catalog)。
|
|
836
|
+
* 依据 https://a2ui.org/specification/v0.9-a2ui/:
|
|
837
|
+
* - 信封 {version, createSurface|updateComponents|updateDataModel|deleteSurface}
|
|
838
|
+
* - 组件为扁平邻接表(id 引用),root 必需
|
|
839
|
+
* - 输入组件与数据模型经 JSON Pointer 双向绑定;action.context 回传
|
|
840
|
+
* - createSurface.sendDataModel: true → 客户端动作时携带完整数据模型
|
|
841
|
+
*
|
|
842
|
+
* action 约定:submit → {name: 'webskill:submit', context: {requestId}};
|
|
843
|
+
* cancel → {name: 'webskill:cancel', context: {requestId}}。
|
|
844
|
+
*/
|
|
845
|
+
const A2UI_VERSION = "v0.9.1";
|
|
846
|
+
const A2UI_BASIC_CATALOG_ID = "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json";
|
|
847
|
+
const A2UI_SUBMIT_ACTION = "webskill:submit";
|
|
848
|
+
const A2UI_CANCEL_ACTION = "webskill:cancel";
|
|
849
|
+
const text = (id, content, variant) => ({
|
|
850
|
+
id,
|
|
851
|
+
component: "Text",
|
|
852
|
+
text: content,
|
|
853
|
+
...variant ? { variant } : {}
|
|
854
|
+
});
|
|
855
|
+
/** @experimental */
|
|
856
|
+
function toA2uiMessages(request) {
|
|
857
|
+
const surfaceId = `webskill-${request.id}`;
|
|
858
|
+
const components = [];
|
|
859
|
+
const children = [];
|
|
860
|
+
const defaults = {};
|
|
861
|
+
if (request.type === "form" && request.title) {
|
|
862
|
+
components.push(text("title", request.title, "h3"));
|
|
863
|
+
children.push("title");
|
|
864
|
+
}
|
|
865
|
+
if (request.type === "authorize") {
|
|
866
|
+
components.push(text("title", "Authorization required", "h3"));
|
|
867
|
+
children.push("title");
|
|
868
|
+
}
|
|
869
|
+
const message = request.type === "ask" || request.type === "confirm" || request.type === "select" || request.type === "authorize" ? request.message : void 0;
|
|
870
|
+
if (message) {
|
|
871
|
+
components.push(text("message", message));
|
|
872
|
+
children.push("message");
|
|
873
|
+
}
|
|
874
|
+
const fields = request.type === "form" ? request.fields.map((f) => ({ ...f })) : request.type === "ask" ? [{
|
|
875
|
+
name: "answer",
|
|
876
|
+
label: request.message,
|
|
877
|
+
type: "text",
|
|
878
|
+
required: true
|
|
879
|
+
}] : request.type === "confirm" ? [{
|
|
880
|
+
name: "confirmed",
|
|
881
|
+
label: request.message,
|
|
882
|
+
type: "boolean",
|
|
883
|
+
defaultValue: request.defaultValue ?? true
|
|
884
|
+
}] : request.type === "authorize" ? [] : [{
|
|
885
|
+
name: "selected",
|
|
886
|
+
label: request.message,
|
|
887
|
+
type: "select",
|
|
888
|
+
options: request.options
|
|
889
|
+
}];
|
|
890
|
+
for (const field of fields) {
|
|
891
|
+
const path = `/form/${field.name}`;
|
|
892
|
+
if (field.defaultValue !== void 0) defaults[field.name] = field.defaultValue;
|
|
893
|
+
const checks = field.required ? [{
|
|
894
|
+
call: "required",
|
|
895
|
+
args: { value: { path } },
|
|
896
|
+
message: `${field.label} is required`
|
|
897
|
+
}] : void 0;
|
|
898
|
+
if (field.type === "boolean") components.push({
|
|
899
|
+
id: `field_${field.name}`,
|
|
900
|
+
component: "CheckBox",
|
|
901
|
+
label: field.label,
|
|
902
|
+
value: { path },
|
|
903
|
+
...checks ? { checks } : {}
|
|
904
|
+
});
|
|
905
|
+
else if (field.type === "select") components.push({
|
|
906
|
+
id: `field_${field.name}`,
|
|
907
|
+
component: "ChoicePicker",
|
|
908
|
+
variant: "mutuallyExclusive",
|
|
909
|
+
label: field.label,
|
|
910
|
+
options: (field.options ?? []).map((o) => ({
|
|
911
|
+
label: o.label,
|
|
912
|
+
value: typeof o.value === "string" ? o.value : JSON.stringify(o.value)
|
|
913
|
+
})),
|
|
914
|
+
value: { path },
|
|
915
|
+
...checks ? { checks } : {}
|
|
916
|
+
});
|
|
917
|
+
else components.push({
|
|
918
|
+
id: `field_${field.name}`,
|
|
919
|
+
component: "TextField",
|
|
920
|
+
label: field.label,
|
|
921
|
+
value: { path },
|
|
922
|
+
variant: field.type === "textarea" ? "longText" : "shortText",
|
|
923
|
+
...checks ? { checks } : {}
|
|
924
|
+
});
|
|
925
|
+
children.push(`field_${field.name}`);
|
|
926
|
+
}
|
|
927
|
+
components.push(text("submit_label", request.type === "authorize" ? "Allow" : "Submit"));
|
|
928
|
+
components.push({
|
|
929
|
+
id: "submit",
|
|
930
|
+
component: "Button",
|
|
931
|
+
child: "submit_label",
|
|
932
|
+
variant: "primary",
|
|
933
|
+
action: { event: {
|
|
934
|
+
name: A2UI_SUBMIT_ACTION,
|
|
935
|
+
context: { requestId: request.id }
|
|
936
|
+
} }
|
|
937
|
+
});
|
|
938
|
+
components.push(text("cancel_label", request.type === "authorize" ? "Deny" : "Cancel"));
|
|
939
|
+
components.push({
|
|
940
|
+
id: "cancel",
|
|
941
|
+
component: "Button",
|
|
942
|
+
child: "cancel_label",
|
|
943
|
+
variant: "borderless",
|
|
944
|
+
action: { event: {
|
|
945
|
+
name: A2UI_CANCEL_ACTION,
|
|
946
|
+
context: { requestId: request.id }
|
|
947
|
+
} }
|
|
948
|
+
});
|
|
949
|
+
children.push("submit", "cancel");
|
|
950
|
+
components.unshift({
|
|
951
|
+
id: "root",
|
|
952
|
+
component: "Column",
|
|
953
|
+
children,
|
|
954
|
+
justify: "start",
|
|
955
|
+
align: "stretch"
|
|
956
|
+
});
|
|
957
|
+
const messages = [{
|
|
958
|
+
version: A2UI_VERSION,
|
|
959
|
+
createSurface: {
|
|
960
|
+
surfaceId,
|
|
961
|
+
catalogId: A2UI_BASIC_CATALOG_ID,
|
|
962
|
+
sendDataModel: true
|
|
963
|
+
}
|
|
964
|
+
}, {
|
|
965
|
+
version: A2UI_VERSION,
|
|
966
|
+
updateComponents: {
|
|
967
|
+
surfaceId,
|
|
968
|
+
components
|
|
969
|
+
}
|
|
970
|
+
}];
|
|
971
|
+
if (Object.keys(defaults).length > 0) messages.push({
|
|
972
|
+
version: A2UI_VERSION,
|
|
973
|
+
updateDataModel: {
|
|
974
|
+
surfaceId,
|
|
975
|
+
path: "/form",
|
|
976
|
+
value: defaults
|
|
977
|
+
}
|
|
978
|
+
});
|
|
979
|
+
return messages;
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* A2UI client→server action 事件 → InteractionResponse
|
|
983
|
+
* @experimental
|
|
984
|
+
*/
|
|
985
|
+
function fromA2uiAction(event) {
|
|
986
|
+
const e = event ?? {};
|
|
987
|
+
const id = String(e.context?.["requestId"] ?? "");
|
|
988
|
+
if (e.name === "webskill:cancel") return {
|
|
989
|
+
id,
|
|
990
|
+
cancelled: true
|
|
991
|
+
};
|
|
992
|
+
if (e.name === "webskill:submit") return {
|
|
993
|
+
id,
|
|
994
|
+
value: e.context?.["values"] ?? e.context?.["form"] ?? e.context
|
|
995
|
+
};
|
|
996
|
+
return {
|
|
997
|
+
id,
|
|
998
|
+
value: e.context
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
let loading;
|
|
1002
|
+
function loadA2uiRuntime() {
|
|
1003
|
+
loading ??= (async () => {
|
|
1004
|
+
try {
|
|
1005
|
+
const [core, lit] = await Promise.all([import("@a2ui/web_core/v0_9"), import("@a2ui/lit/v0_9")]);
|
|
1006
|
+
lit.A2uiSurface;
|
|
1007
|
+
return {
|
|
1008
|
+
MessageProcessor: core.MessageProcessor,
|
|
1009
|
+
basicCatalog: lit.basicCatalog
|
|
1010
|
+
};
|
|
1011
|
+
} catch (e) {
|
|
1012
|
+
throw new WebSkillError("UI_UNAVAILABLE", "The A2UI Lit runtime is unavailable; install @a2ui/lit and @a2ui/web_core to use LitRendererBridge", e);
|
|
1013
|
+
}
|
|
1014
|
+
})();
|
|
1015
|
+
return loading;
|
|
1016
|
+
}
|
|
1017
|
+
/** @experimental */
|
|
1018
|
+
var LitRendererBridge = class {
|
|
1019
|
+
#mount;
|
|
1020
|
+
#doc;
|
|
1021
|
+
constructor(deps) {
|
|
1022
|
+
this.#mount = deps.mount;
|
|
1023
|
+
this.#doc = deps.document ?? deps.mount.ownerDocument;
|
|
1024
|
+
}
|
|
1025
|
+
async request(input) {
|
|
1026
|
+
const { MessageProcessor, basicCatalog } = await loadA2uiRuntime();
|
|
1027
|
+
const messages = toA2uiMessages(input);
|
|
1028
|
+
const surfaceId = `webskill-${input.id}`;
|
|
1029
|
+
return new Promise((resolve) => {
|
|
1030
|
+
const cleanup = (el) => el?.remove();
|
|
1031
|
+
let surfaceEl;
|
|
1032
|
+
const processor = new MessageProcessor([basicCatalog], (action) => {
|
|
1033
|
+
const surfaces = processor.getClientDataModel("v0.9.1")?.surfaces;
|
|
1034
|
+
const formValues = typeof surfaces?.[surfaceId] === "object" && surfaces[surfaceId] !== null ? surfaces[surfaceId]["form"] ?? action.context : action.context;
|
|
1035
|
+
cleanup(surfaceEl);
|
|
1036
|
+
const response = fromA2uiAction({
|
|
1037
|
+
...action,
|
|
1038
|
+
context: {
|
|
1039
|
+
...action.context,
|
|
1040
|
+
values: formValues
|
|
1041
|
+
}
|
|
1042
|
+
});
|
|
1043
|
+
if (input.type === "authorize" && !response.cancelled) response.value = true;
|
|
1044
|
+
resolve(response);
|
|
1045
|
+
}, { version: "v0.9.1" });
|
|
1046
|
+
processor.onSurfaceCreated((surface) => {
|
|
1047
|
+
const el = this.#doc.createElement("a2ui-surface");
|
|
1048
|
+
el.surface = surface;
|
|
1049
|
+
surfaceEl = el;
|
|
1050
|
+
this.#mount.appendChild(el);
|
|
1051
|
+
});
|
|
1052
|
+
processor.processMessages(messages);
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
};
|
|
1056
|
+
|
|
1057
|
+
//#endregion
|
|
1058
|
+
export { renderRenderResult as C, toVercelToolInvocation as D, toOpenUiLang as E, renderMiniMarkdown as S, toA2uiMessages as T, fromOpenUiAction as _, CHART_PALETTE as a, renderBlocks as b, OPENUI_SUBMIT_ACTION as c, WEBSKILL_STYLES_CSS as d, WebFormBridge as f, fromA2uiAction as g, ensureStyles as h, A2UI_VERSION as i, VERCEL_INTERACTION_TOOL_NAME as l, collectValues as m, A2UI_CANCEL_ACTION as n, LitRendererBridge as o, chartToTable as p, A2UI_SUBMIT_ACTION as r, OPENUI_CANCEL_ACTION as s, A2UI_BASIC_CATALOG_ID as t, VercelUiBridge as u, fromVercelToolResult as v, shapeInteractionValue as w, renderMiniChart as x, interactionToFormModel as y };
|