@changfenhuang/dsh-genui 0.9.1
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/CHANGELOG.md +367 -0
- package/LICENSE +21 -0
- package/README.md +261 -0
- package/README.zh-CN.md +261 -0
- package/SKILL.md +115 -0
- package/cordis.patch.yml +9 -0
- package/demo-prompts.md +118 -0
- package/lib/assets/echarts.js +64 -0
- package/lib/assets/mermaid.js +3591 -0
- package/lib/assets/three.js +3844 -0
- package/lib/client.js +5 -0
- package/lib/index.js +1895 -0
- package/lib/invariant.js +19 -0
- package/lib/types/client/EChartNode.d.ts +4 -0
- package/lib/types/client/ErrorBoundary.d.ts +30 -0
- package/lib/types/client/GenuiBlock.d.ts +7 -0
- package/lib/types/client/PlotBlock.d.ts +44 -0
- package/lib/types/client/action-context.d.ts +7 -0
- package/lib/types/client/asset-echarts.d.ts +15 -0
- package/lib/types/client/asset-loader.d.ts +31 -0
- package/lib/types/client/asset-mermaid.d.ts +1 -0
- package/lib/types/client/asset-three.d.ts +1 -0
- package/lib/types/client/blocks/advanced.d.ts +104 -0
- package/lib/types/client/blocks/basic.d.ts +27 -0
- package/lib/types/client/blocks/charts.d.ts +38 -0
- package/lib/types/client/blocks/diagram/geometry.d.ts +73 -0
- package/lib/types/client/blocks/diagram/index.d.ts +5 -0
- package/lib/types/client/blocks/diagram/layout.d.ts +41 -0
- package/lib/types/client/blocks/diagram/theme.d.ts +35 -0
- package/lib/types/client/blocks/forms.d.ts +83 -0
- package/lib/types/client/blocks/render-node.d.ts +10 -0
- package/lib/types/client/blocks/state.d.ts +58 -0
- package/lib/types/client/dom-fence.d.ts +14 -0
- package/lib/types/client/echarts-lazy.d.ts +17 -0
- package/lib/types/client/fence-render.d.ts +58 -0
- package/lib/types/client/gallery.d.ts +9 -0
- package/lib/types/client/guard.d.ts +126 -0
- package/lib/types/client/index.d.ts +36 -0
- package/lib/types/client/interaction-store.d.ts +43 -0
- package/lib/types/client/mermaid-core.d.ts +8 -0
- package/lib/types/client/mermaid-lazy.d.ts +9 -0
- package/lib/types/client/mermaid-safe.d.ts +52 -0
- package/lib/types/client/panel-command.d.ts +29 -0
- package/lib/types/client/panel-store.d.ts +94 -0
- package/lib/types/client/panel.d.ts +15 -0
- package/lib/types/client/parse-partial.d.ts +37 -0
- package/lib/types/client/safe-math.d.ts +43 -0
- package/lib/types/client/scene3d-core.d.ts +21 -0
- package/lib/types/client/scene3d-lazy.d.ts +8 -0
- package/lib/types/client/spec.d.ts +610 -0
- package/lib/types/client/toolview.d.ts +7 -0
- package/lib/types/index.d.ts +1 -0
- package/lib/types/plugin/index.d.ts +25 -0
- package/lib/types/plugin/invariant.d.ts +15 -0
- package/lib/types/plugin/tool.d.ts +27 -0
- package/lib/types/shared/fence-repair.d.ts +67 -0
- package/package.json +121 -0
- package/src/client/EChartNode.tsx +242 -0
- package/src/client/ErrorBoundary.tsx +66 -0
- package/src/client/GenuiBlock.module.css +1263 -0
- package/src/client/GenuiBlock.tsx +166 -0
- package/src/client/PlotBlock.module.css +155 -0
- package/src/client/PlotBlock.tsx +431 -0
- package/src/client/action-context.ts +32 -0
- package/src/client/asset-echarts.ts +25 -0
- package/src/client/asset-loader.ts +79 -0
- package/src/client/asset-mermaid.ts +13 -0
- package/src/client/asset-three.ts +12 -0
- package/src/client/blocks/advanced.tsx +435 -0
- package/src/client/blocks/basic.tsx +116 -0
- package/src/client/blocks/charts.tsx +319 -0
- package/src/client/blocks/diagram/geometry.ts +151 -0
- package/src/client/blocks/diagram/index.tsx +284 -0
- package/src/client/blocks/diagram/layout.ts +199 -0
- package/src/client/blocks/diagram/theme.ts +108 -0
- package/src/client/blocks/forms.tsx +523 -0
- package/src/client/blocks/render-node.tsx +260 -0
- package/src/client/blocks/state.ts +61 -0
- package/src/client/dom-fence.tsx +641 -0
- package/src/client/echarts-lazy.ts +35 -0
- package/src/client/fence-render.tsx +230 -0
- package/src/client/gallery.ts +128 -0
- package/src/client/guard.ts +1450 -0
- package/src/client/index.tsx +195 -0
- package/src/client/interaction-store.ts +122 -0
- package/src/client/mermaid-core.ts +121 -0
- package/src/client/mermaid-lazy.ts +31 -0
- package/src/client/mermaid-safe.ts +122 -0
- package/src/client/panel-command.ts +119 -0
- package/src/client/panel-store.ts +471 -0
- package/src/client/panel.tsx +185 -0
- package/src/client/parse-partial.ts +162 -0
- package/src/client/safe-math.ts +270 -0
- package/src/client/scene3d-core.ts +192 -0
- package/src/client/scene3d-lazy.ts +26 -0
- package/src/client/spec.ts +767 -0
- package/src/client/toolview.tsx +66 -0
- package/src/css-modules.d.ts +5 -0
- package/src/index.ts +12 -0
- package/src/plugin/index.ts +173 -0
- package/src/plugin/invariant.ts +30 -0
- package/src/plugin/node-shims.d.ts +31 -0
- package/src/plugin/tool.ts +330 -0
- package/src/shared/fence-repair.ts +244 -0
- package/tsconfig.json +35 -0
- package/tsdown.config.ts +193 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,1895 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
//#region src/client/spec.ts
|
|
5
|
+
/**
|
|
6
|
+
* Wrap a bare component object into a col root. Returns null when `value` is
|
|
7
|
+
* not component-shaped (no usable `type`). `panel`/`append` live on the root
|
|
8
|
+
* spec, so they are hoisted onto the wrapper.
|
|
9
|
+
*/
|
|
10
|
+
function wrapSingleComponentRoot(value) {
|
|
11
|
+
if (typeof value !== "object" || value === null) return null;
|
|
12
|
+
const v = value;
|
|
13
|
+
if (typeof v.type !== "string" || v.type === "") return null;
|
|
14
|
+
const root = {
|
|
15
|
+
type: "col",
|
|
16
|
+
items: [value]
|
|
17
|
+
};
|
|
18
|
+
if (v.panel === true) root.panel = true;
|
|
19
|
+
if (v.append === true) root.append = true;
|
|
20
|
+
return root;
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region src/client/guard.ts
|
|
24
|
+
/** Hard resource limits enforced by repair (and mirrored at render time). */
|
|
25
|
+
const GENUI_LIMITS = {
|
|
26
|
+
/** Maximum nesting depth of the component tree. */
|
|
27
|
+
maxDepth: 8,
|
|
28
|
+
/** Maximum total nodes across the whole spec. */
|
|
29
|
+
maxNodes: 200,
|
|
30
|
+
/** Maximum length of any plain string field. */
|
|
31
|
+
maxString: 2e3,
|
|
32
|
+
/** Maximum length of a `code` body. */
|
|
33
|
+
maxCode: 12e3,
|
|
34
|
+
/** Maximum length of a mermaid source. */
|
|
35
|
+
maxMermaid: 8e3,
|
|
36
|
+
/** Maximum `grid` columns. */
|
|
37
|
+
maxGridCols: 12,
|
|
38
|
+
/** Maximum `tabs` count. */
|
|
39
|
+
maxTabs: 12,
|
|
40
|
+
/** Maximum `accordion` items. */
|
|
41
|
+
maxAccordionItems: 24,
|
|
42
|
+
/** Maximum `list` items. */
|
|
43
|
+
maxListItems: 50,
|
|
44
|
+
/** Maximum `select`/`radio` options. */
|
|
45
|
+
maxOptions: 50,
|
|
46
|
+
/** Maximum `table` rows / columns. */
|
|
47
|
+
maxTableRows: 50,
|
|
48
|
+
maxTableCols: 12,
|
|
49
|
+
/** Maximum `chart` data points per series. */
|
|
50
|
+
maxChartPoints: 60,
|
|
51
|
+
/** Maximum `plot` series and per-series parameters. */
|
|
52
|
+
maxPlotSeries: 8,
|
|
53
|
+
maxPlotParams: 6,
|
|
54
|
+
/** Maximum `scene3d` meshes. */
|
|
55
|
+
maxMeshes: 5,
|
|
56
|
+
/** Maximum `quiz` options. */
|
|
57
|
+
maxQuizOptions: 8,
|
|
58
|
+
/** Maximum `steps` / `timeline` / `breadcrumb` / `keyvalue` entries. */
|
|
59
|
+
maxSteps: 24,
|
|
60
|
+
maxTimelineItems: 24,
|
|
61
|
+
maxBreadcrumbItems: 12,
|
|
62
|
+
maxKeyValuePairs: 24,
|
|
63
|
+
/** Maximum `file-tree` nesting. */
|
|
64
|
+
maxTreeDepth: 6,
|
|
65
|
+
/** Maximum `diagram` nodes / edges / zones / focal accents (editorial
|
|
66
|
+
* complexity budget, mirroring diagram-design's §7 limits). */
|
|
67
|
+
maxDiagramNodes: 9,
|
|
68
|
+
maxDiagramEdges: 12,
|
|
69
|
+
maxDiagramZones: 3,
|
|
70
|
+
maxDiagramFocal: 2,
|
|
71
|
+
maxDiagramLabel: 14,
|
|
72
|
+
/** Maximum depth of an `echart` option object (prevents pathological nested
|
|
73
|
+
* ECharts configs from stalling the guard walk). */
|
|
74
|
+
maxEChartOptionDepth: 10,
|
|
75
|
+
/** Maximum length of any single array inside an `echart` option (prevents
|
|
76
|
+
* a model from stalling rendering with `series.data` of hundreds of
|
|
77
|
+
* thousands of points). */
|
|
78
|
+
maxEChartArrayLen: 500,
|
|
79
|
+
/** Maximum total entries (object keys + array elements) traversed while
|
|
80
|
+
* sanitizing an `echart` option. Bounds the walk so a pathologically
|
|
81
|
+
* large option object cannot stall the guard. */
|
|
82
|
+
maxEChartOptionNodes: 2e3
|
|
83
|
+
};
|
|
84
|
+
/** Is `v` one of `values`? (enum guard) */
|
|
85
|
+
function inEnum(v, values) {
|
|
86
|
+
return typeof v === "string" && values.includes(v);
|
|
87
|
+
}
|
|
88
|
+
/** String field: truncate a string to `cap`, or undefined when not a string. */
|
|
89
|
+
function str(v, cap) {
|
|
90
|
+
return typeof v === "string" ? v.slice(0, cap) : void 0;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Color field: the value lands in an inline `style` (background/stroke) or
|
|
94
|
+
* THREE.Color. Arbitrary CSS values are an exfiltration channel — a model
|
|
95
|
+
* (or a hostile spec) could emit `url(https://attacker/track?...)` and the
|
|
96
|
+
* browser would fetch it. Only formats that name a color pass: hex, rgb/hsl
|
|
97
|
+
* functions, and host design tokens (`var(--dsw-*)`). Anything else degrades
|
|
98
|
+
* to the component's default palette.
|
|
99
|
+
*/
|
|
100
|
+
const SAFE_COLOR_RE = /^(?:#[\da-fA-F]{3,8}|rgba?\([^)]{0,64}\)|hsla?\([^)]{0,64}\)|var\(--dsw-[\w-]+(?:,\s*#[0-9a-fA-F]{3,8})?\))$/;
|
|
101
|
+
function color(v) {
|
|
102
|
+
if (typeof v !== "string") return void 0;
|
|
103
|
+
const s = v.trim();
|
|
104
|
+
return s.length <= 64 && SAFE_COLOR_RE.test(s) ? s : void 0;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Link target field: only http(s) and mailto survive. `javascript:`/`data:`
|
|
108
|
+
* and every other scheme degrade to a plain-text node — the model's link is
|
|
109
|
+
* display, not an execution channel.
|
|
110
|
+
*/
|
|
111
|
+
function safeHref(v) {
|
|
112
|
+
if (typeof v !== "string") return void 0;
|
|
113
|
+
const s = v.trim();
|
|
114
|
+
if (s.length > 2048) return void 0;
|
|
115
|
+
return /^https?:\/\//i.test(s) || /^mailto:[^@\s]+@[^@\s]+$/i.test(s) ? s : void 0;
|
|
116
|
+
}
|
|
117
|
+
/** Media loads bytes, so accept only browser-reachable http(s) or same-origin
|
|
118
|
+
* relative paths. Active/local schemes and protocol-relative URLs are
|
|
119
|
+
* rejected. The renderer always keeps playback user-controlled. */
|
|
120
|
+
function safeMediaSrc(v) {
|
|
121
|
+
if (typeof v !== "string") return void 0;
|
|
122
|
+
const s = v.trim();
|
|
123
|
+
if (s === "" || s.length > 2048) return void 0;
|
|
124
|
+
if (/^https?:\/\//i.test(s)) return s;
|
|
125
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(s) || /^[/\\]{2}/.test(s)) return void 0;
|
|
126
|
+
return s;
|
|
127
|
+
}
|
|
128
|
+
/** Finite-number field: clamp into [min, max], or undefined when not finite. */
|
|
129
|
+
function num(v, min, max) {
|
|
130
|
+
return typeof v === "number" && Number.isFinite(v) ? Math.min(max, Math.max(min, v)) : void 0;
|
|
131
|
+
}
|
|
132
|
+
/** Integer field: clamp into [min, max], or undefined when not a finite integer. */
|
|
133
|
+
function int(v, min, max) {
|
|
134
|
+
return typeof v === "number" && Number.isFinite(v) ? Math.min(max, Math.max(min, Math.trunc(v))) : void 0;
|
|
135
|
+
}
|
|
136
|
+
/** Optional enum field: the value when it matches, otherwise undefined. */
|
|
137
|
+
function enu(v, values) {
|
|
138
|
+
return inEnum(v, values) ? v : void 0;
|
|
139
|
+
}
|
|
140
|
+
/** Plain object (not array, not null). */
|
|
141
|
+
function obj(v) {
|
|
142
|
+
return typeof v === "object" && v !== null && !Array.isArray(v) ? v : void 0;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Optional-field spread helper. `exactOptionalPropertyTypes` forbids
|
|
146
|
+
* `{ gap: number | undefined }`; computing the value into a const first and
|
|
147
|
+
* spreading `opt('gap', g)` keeps every optional field either absent or a
|
|
148
|
+
* plain value.
|
|
149
|
+
*/
|
|
150
|
+
function opt(key, value) {
|
|
151
|
+
return value === void 0 ? {} : { [key]: value };
|
|
152
|
+
}
|
|
153
|
+
const TEXT_SIZES = [
|
|
154
|
+
"h1",
|
|
155
|
+
"h2",
|
|
156
|
+
"h3",
|
|
157
|
+
"body",
|
|
158
|
+
"muted",
|
|
159
|
+
"caption"
|
|
160
|
+
];
|
|
161
|
+
const BUTTON_TONES = [
|
|
162
|
+
"primary",
|
|
163
|
+
"danger",
|
|
164
|
+
"success",
|
|
165
|
+
"ghost"
|
|
166
|
+
];
|
|
167
|
+
const BADGE_TONES = [
|
|
168
|
+
"success",
|
|
169
|
+
"warn",
|
|
170
|
+
"danger",
|
|
171
|
+
"accent"
|
|
172
|
+
];
|
|
173
|
+
const INPUT_TYPES = [
|
|
174
|
+
"text",
|
|
175
|
+
"email",
|
|
176
|
+
"password"
|
|
177
|
+
];
|
|
178
|
+
const CALLOUT_TONES = [
|
|
179
|
+
"info",
|
|
180
|
+
"success",
|
|
181
|
+
"warning",
|
|
182
|
+
"error"
|
|
183
|
+
];
|
|
184
|
+
const CHART_KINDS = [
|
|
185
|
+
"bars",
|
|
186
|
+
"line",
|
|
187
|
+
"donut"
|
|
188
|
+
];
|
|
189
|
+
const PLOT_KINDS = [
|
|
190
|
+
"line",
|
|
191
|
+
"area",
|
|
192
|
+
"scatter"
|
|
193
|
+
];
|
|
194
|
+
const MEDIA_ASPECT_RATIOS = [
|
|
195
|
+
"16:9",
|
|
196
|
+
"4:3",
|
|
197
|
+
"1:1",
|
|
198
|
+
"9:16"
|
|
199
|
+
];
|
|
200
|
+
const MESH_SHAPES = [
|
|
201
|
+
"box",
|
|
202
|
+
"sphere",
|
|
203
|
+
"cone",
|
|
204
|
+
"cylinder",
|
|
205
|
+
"torus"
|
|
206
|
+
];
|
|
207
|
+
const FILE_TYPES = ["file", "dir"];
|
|
208
|
+
const DIAGRAM_KINDS = [
|
|
209
|
+
"architecture",
|
|
210
|
+
"it-state",
|
|
211
|
+
"flowchart",
|
|
212
|
+
"sequence",
|
|
213
|
+
"state",
|
|
214
|
+
"er",
|
|
215
|
+
"timeline",
|
|
216
|
+
"swimlane",
|
|
217
|
+
"quadrant",
|
|
218
|
+
"radar",
|
|
219
|
+
"loop",
|
|
220
|
+
"nested",
|
|
221
|
+
"tree",
|
|
222
|
+
"org-chart",
|
|
223
|
+
"layers",
|
|
224
|
+
"venn",
|
|
225
|
+
"pyramid",
|
|
226
|
+
"bar",
|
|
227
|
+
"line",
|
|
228
|
+
"gantt",
|
|
229
|
+
"scatter",
|
|
230
|
+
"high-level",
|
|
231
|
+
"process",
|
|
232
|
+
"medallion",
|
|
233
|
+
"data-flow",
|
|
234
|
+
"dp-integration",
|
|
235
|
+
"dp-security-matrix"
|
|
236
|
+
];
|
|
237
|
+
const DIAGRAM_NODE_TYPES = [
|
|
238
|
+
"focal",
|
|
239
|
+
"backend",
|
|
240
|
+
"store",
|
|
241
|
+
"external",
|
|
242
|
+
"input",
|
|
243
|
+
"optional",
|
|
244
|
+
"security"
|
|
245
|
+
];
|
|
246
|
+
const DIAGRAM_VARIANTS = [
|
|
247
|
+
"light",
|
|
248
|
+
"dark",
|
|
249
|
+
"editorial"
|
|
250
|
+
];
|
|
251
|
+
const DIAGRAM_EDGE_KINDS = [
|
|
252
|
+
"solid",
|
|
253
|
+
"dashed",
|
|
254
|
+
"accent",
|
|
255
|
+
"link"
|
|
256
|
+
];
|
|
257
|
+
const DIAGRAM_ROUTES = [
|
|
258
|
+
"auto",
|
|
259
|
+
"orthogonal",
|
|
260
|
+
"straight"
|
|
261
|
+
];
|
|
262
|
+
const ECHART_PRESETS = [
|
|
263
|
+
"bar",
|
|
264
|
+
"line",
|
|
265
|
+
"area",
|
|
266
|
+
"pie",
|
|
267
|
+
"scatter"
|
|
268
|
+
];
|
|
269
|
+
/** Walk `list` with the shared node budget; drops invalid entries. */
|
|
270
|
+
function repairItems(list, ctx, depth) {
|
|
271
|
+
if (!Array.isArray(list)) return [];
|
|
272
|
+
const out = [];
|
|
273
|
+
for (const item of list) {
|
|
274
|
+
if (ctx.remaining <= 0) break;
|
|
275
|
+
ctx.remaining -= 1;
|
|
276
|
+
const node = repairNode(item, ctx, depth);
|
|
277
|
+
if (node !== null) out.push(node);
|
|
278
|
+
}
|
|
279
|
+
return out;
|
|
280
|
+
}
|
|
281
|
+
function repairNode(value, ctx, depth) {
|
|
282
|
+
if (depth > GENUI_LIMITS.maxDepth) return null;
|
|
283
|
+
const v = obj(value);
|
|
284
|
+
if (v === void 0) return null;
|
|
285
|
+
const type = v.type;
|
|
286
|
+
if (typeof type !== "string") return null;
|
|
287
|
+
switch (type) {
|
|
288
|
+
case "text": {
|
|
289
|
+
const content = str(v.content, GENUI_LIMITS.maxString) ?? str(v.text, GENUI_LIMITS.maxString);
|
|
290
|
+
if (content === void 0) return null;
|
|
291
|
+
return {
|
|
292
|
+
type: "text",
|
|
293
|
+
content,
|
|
294
|
+
...opt("size", enu(v.size, TEXT_SIZES)),
|
|
295
|
+
...opt("center", v.center === true ? true : void 0)
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
case "row": return {
|
|
299
|
+
type: "row",
|
|
300
|
+
items: repairItems(v.items, ctx, depth + 1),
|
|
301
|
+
...opt("wrap", v.wrap === true ? true : void 0),
|
|
302
|
+
...opt("spacer", v.spacer === true ? true : void 0)
|
|
303
|
+
};
|
|
304
|
+
case "col": return {
|
|
305
|
+
type: "col",
|
|
306
|
+
items: repairItems(v.items, ctx, depth + 1),
|
|
307
|
+
...opt("gap", num(v.gap, 0, 96))
|
|
308
|
+
};
|
|
309
|
+
case "grid": return {
|
|
310
|
+
type: "grid",
|
|
311
|
+
cols: int(v.cols, 1, GENUI_LIMITS.maxGridCols) ?? 1,
|
|
312
|
+
items: repairItems(v.items, ctx, depth + 1)
|
|
313
|
+
};
|
|
314
|
+
case "card": return {
|
|
315
|
+
type: "card",
|
|
316
|
+
items: repairItems(v.items, ctx, depth + 1),
|
|
317
|
+
...opt("title", str(v.title, GENUI_LIMITS.maxString))
|
|
318
|
+
};
|
|
319
|
+
case "button": {
|
|
320
|
+
const label = str(v.label, GENUI_LIMITS.maxString);
|
|
321
|
+
if (label === void 0) return null;
|
|
322
|
+
return {
|
|
323
|
+
type: "button",
|
|
324
|
+
label,
|
|
325
|
+
...opt("tone", enu(v.tone, BUTTON_TONES)),
|
|
326
|
+
...opt("full", v.full === true ? true : void 0),
|
|
327
|
+
...opt("small", v.small === true ? true : void 0),
|
|
328
|
+
...opt("icon", str(v.icon, 64)),
|
|
329
|
+
...opt("action", str(v.action, 200))
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
case "input": return {
|
|
333
|
+
type: "input",
|
|
334
|
+
...opt("label", str(v.label, GENUI_LIMITS.maxString)),
|
|
335
|
+
...opt("placeholder", str(v.placeholder, GENUI_LIMITS.maxString)),
|
|
336
|
+
...opt("value", str(v.value, GENUI_LIMITS.maxString)),
|
|
337
|
+
...opt("inputType", enu(v.inputType, INPUT_TYPES)),
|
|
338
|
+
...opt("action", str(v.action, 200)),
|
|
339
|
+
...opt("id", str(v.id, 200))
|
|
340
|
+
};
|
|
341
|
+
case "select": {
|
|
342
|
+
const options = repairStrings(v.options, GENUI_LIMITS.maxOptions, GENUI_LIMITS.maxString);
|
|
343
|
+
if (options === void 0) return null;
|
|
344
|
+
return {
|
|
345
|
+
type: "select",
|
|
346
|
+
options,
|
|
347
|
+
...opt("label", str(v.label, GENUI_LIMITS.maxString)),
|
|
348
|
+
...opt("action", str(v.action, 200)),
|
|
349
|
+
...opt("selected", int(v.selected, 0, options.length - 1)),
|
|
350
|
+
...opt("id", str(v.id, 200))
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
case "checkbox": {
|
|
354
|
+
const label = str(v.label, GENUI_LIMITS.maxString);
|
|
355
|
+
if (label === void 0) return null;
|
|
356
|
+
return {
|
|
357
|
+
type: "checkbox",
|
|
358
|
+
label,
|
|
359
|
+
...opt("checked", v.checked === true ? true : void 0),
|
|
360
|
+
...opt("action", str(v.action, 200))
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
case "link": {
|
|
364
|
+
const label = str(v.label, GENUI_LIMITS.maxString);
|
|
365
|
+
if (label === void 0) return null;
|
|
366
|
+
return {
|
|
367
|
+
type: "link",
|
|
368
|
+
label,
|
|
369
|
+
...opt("href", safeHref(v.href))
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
case "audio": {
|
|
373
|
+
const src = safeMediaSrc(v.src);
|
|
374
|
+
if (src === void 0) return null;
|
|
375
|
+
return {
|
|
376
|
+
type: "audio",
|
|
377
|
+
src,
|
|
378
|
+
...opt("alt", str(v.alt, GENUI_LIMITS.maxString)),
|
|
379
|
+
...opt("loop", v.loop === true ? true : void 0)
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
case "video": {
|
|
383
|
+
const src = safeMediaSrc(v.src);
|
|
384
|
+
if (src === void 0) return null;
|
|
385
|
+
return {
|
|
386
|
+
type: "video",
|
|
387
|
+
src,
|
|
388
|
+
...opt("alt", str(v.alt, GENUI_LIMITS.maxString)),
|
|
389
|
+
...opt("poster", safeMediaSrc(v.poster)),
|
|
390
|
+
...opt("loop", v.loop === true ? true : void 0),
|
|
391
|
+
...opt("muted", v.muted === true ? true : void 0),
|
|
392
|
+
...opt("aspectRatio", enu(v.aspectRatio, MEDIA_ASPECT_RATIOS))
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
case "badge": {
|
|
396
|
+
const label = str(v.label, GENUI_LIMITS.maxString) ?? str(v.text, GENUI_LIMITS.maxString) ?? str(v.value, GENUI_LIMITS.maxString);
|
|
397
|
+
if (label === void 0) return null;
|
|
398
|
+
return {
|
|
399
|
+
type: "badge",
|
|
400
|
+
label,
|
|
401
|
+
...opt("tone", enu(v.tone, BADGE_TONES)),
|
|
402
|
+
...opt("icon", str(v.icon, 64))
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
case "stat": {
|
|
406
|
+
const label = str(v.label, GENUI_LIMITS.maxString);
|
|
407
|
+
const value = str(v.value, 128);
|
|
408
|
+
if (label === void 0 || value === void 0) return null;
|
|
409
|
+
return {
|
|
410
|
+
type: "stat",
|
|
411
|
+
label,
|
|
412
|
+
value,
|
|
413
|
+
...opt("delta", str(v.delta, 64))
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
case "progress": {
|
|
417
|
+
const value = num(v.value, 0, 100);
|
|
418
|
+
if (value === void 0) return null;
|
|
419
|
+
return {
|
|
420
|
+
type: "progress",
|
|
421
|
+
value,
|
|
422
|
+
...opt("label", str(v.label, GENUI_LIMITS.maxString)),
|
|
423
|
+
...opt("valueLabel", str(v.valueLabel, 64))
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
case "divider": return { type: "divider" };
|
|
427
|
+
case "spacer": return { type: "spacer" };
|
|
428
|
+
case "avatar": {
|
|
429
|
+
const name = str(v.name, 64);
|
|
430
|
+
if (name === void 0) return null;
|
|
431
|
+
return {
|
|
432
|
+
type: "avatar",
|
|
433
|
+
name,
|
|
434
|
+
...opt("color", color(v.color))
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
case "list": {
|
|
438
|
+
const items = repairListItems(v.items, GENUI_LIMITS.maxListItems, ctx, depth + 1);
|
|
439
|
+
if (items === void 0) return null;
|
|
440
|
+
return {
|
|
441
|
+
type: "list",
|
|
442
|
+
items
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
case "table": {
|
|
446
|
+
let rawCols = v.columns;
|
|
447
|
+
let rawRows = v.rows !== void 0 ? v.rows : v.data;
|
|
448
|
+
if (Array.isArray(rawCols) && rawCols.length > 0 && typeof rawCols[0] === "object" && rawCols[0] !== null) rawCols = rawCols.map((c) => columnHeaderText(c));
|
|
449
|
+
if (Array.isArray(rawRows) && rawRows.length > 0 && typeof rawRows[0] === "object" && rawRows[0] !== null && !Array.isArray(rawRows[0])) {
|
|
450
|
+
const keys = Array.isArray(v.columns) && v.columns.length > 0 && typeof v.columns[0] === "object" && v.columns[0] !== null ? v.columns.map((c) => columnKeyOf(c)).filter((k) => k !== void 0) : Object.keys(rawRows[0]);
|
|
451
|
+
rawRows = rawRows.map((row) => keys.map((k) => cellText(row[k])));
|
|
452
|
+
}
|
|
453
|
+
const columns = repairStrings(rawCols, GENUI_LIMITS.maxTableCols, 128);
|
|
454
|
+
const rows = repairRows(rawRows, GENUI_LIMITS.maxTableRows, GENUI_LIMITS.maxTableCols);
|
|
455
|
+
if (columns === void 0 || rows === void 0) return null;
|
|
456
|
+
return {
|
|
457
|
+
type: "table",
|
|
458
|
+
columns,
|
|
459
|
+
rows
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
case "chart": {
|
|
463
|
+
const data = repairChartData(v.data, GENUI_LIMITS.maxChartPoints);
|
|
464
|
+
const series = Array.isArray(v.series) ? repairSeries(v.series, GENUI_LIMITS.maxPlotSeries, GENUI_LIMITS.maxChartPoints) : void 0;
|
|
465
|
+
if (data === void 0 && series === void 0) return null;
|
|
466
|
+
return {
|
|
467
|
+
type: "chart",
|
|
468
|
+
data: data ?? [],
|
|
469
|
+
...opt("kind", enu(v.kind, CHART_KINDS)),
|
|
470
|
+
...opt("series", series)
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
case "tabs": {
|
|
474
|
+
const tabs = repairTabs(v.tabs, ctx, depth);
|
|
475
|
+
if (tabs === void 0) return null;
|
|
476
|
+
return {
|
|
477
|
+
type: "tabs",
|
|
478
|
+
tabs
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
case "plot": {
|
|
482
|
+
const series = repairPlotSeries(v.series, GENUI_LIMITS.maxPlotSeries);
|
|
483
|
+
if (series === void 0) return null;
|
|
484
|
+
return {
|
|
485
|
+
type: "plot",
|
|
486
|
+
series,
|
|
487
|
+
...opt("xMin", num(v.xMin, -1e6, 1e6)),
|
|
488
|
+
...opt("xMax", num(v.xMax, -1e6, 1e6)),
|
|
489
|
+
...opt("yMin", num(v.yMin, -1e9, 1e9)),
|
|
490
|
+
...opt("yMax", num(v.yMax, -1e9, 1e9)),
|
|
491
|
+
...opt("title", str(v.title, GENUI_LIMITS.maxString))
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
case "callout": {
|
|
495
|
+
const content = str(v.content, GENUI_LIMITS.maxString);
|
|
496
|
+
if (content === void 0) return null;
|
|
497
|
+
return {
|
|
498
|
+
type: "callout",
|
|
499
|
+
content,
|
|
500
|
+
...opt("tone", enu(v.tone, CALLOUT_TONES)),
|
|
501
|
+
...opt("title", str(v.title, GENUI_LIMITS.maxString))
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
case "steps": {
|
|
505
|
+
const steps = repairSteps(v.steps);
|
|
506
|
+
if (steps === void 0) return null;
|
|
507
|
+
return {
|
|
508
|
+
type: "steps",
|
|
509
|
+
steps,
|
|
510
|
+
...opt("current", int(v.current, 0, steps.length))
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
case "keyvalue": {
|
|
514
|
+
const pairs = repairPairs(v.pairs, GENUI_LIMITS.maxKeyValuePairs);
|
|
515
|
+
if (pairs === void 0) return null;
|
|
516
|
+
return {
|
|
517
|
+
type: "keyvalue",
|
|
518
|
+
pairs
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
case "diff": {
|
|
522
|
+
const diffs = repairDiffs(v.diffs);
|
|
523
|
+
if (diffs === void 0) return null;
|
|
524
|
+
return {
|
|
525
|
+
type: "diff",
|
|
526
|
+
diffs
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
case "json":
|
|
530
|
+
if (!("value" in v)) return null;
|
|
531
|
+
return {
|
|
532
|
+
type: "json",
|
|
533
|
+
value: v.value
|
|
534
|
+
};
|
|
535
|
+
case "code": {
|
|
536
|
+
const code = str(v.code, GENUI_LIMITS.maxCode);
|
|
537
|
+
if (code === void 0) return null;
|
|
538
|
+
return {
|
|
539
|
+
type: "code",
|
|
540
|
+
code,
|
|
541
|
+
...opt("lang", str(v.lang, 64))
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
case "radio": {
|
|
545
|
+
const options = repairStrings(v.options, GENUI_LIMITS.maxOptions, GENUI_LIMITS.maxString);
|
|
546
|
+
if (options === void 0) return null;
|
|
547
|
+
return {
|
|
548
|
+
type: "radio",
|
|
549
|
+
options,
|
|
550
|
+
...opt("label", str(v.label, GENUI_LIMITS.maxString)),
|
|
551
|
+
...opt("selected", int(v.selected, 0, options.length - 1)),
|
|
552
|
+
...opt("action", str(v.action, 200)),
|
|
553
|
+
...opt("group", str(v.group, 200)),
|
|
554
|
+
...opt("answer", typeof v.answer === "number" && Number.isFinite(v.answer) && v.answer >= 0 && v.answer < options.length ? Math.trunc(v.answer) : typeof v.answer === "string" ? v.answer.slice(0, 512) : void 0),
|
|
555
|
+
...opt("explanation", str(v.explanation, GENUI_LIMITS.maxString))
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
case "submit": {
|
|
559
|
+
const label = str(v.label, GENUI_LIMITS.maxString);
|
|
560
|
+
const action = str(v.action, 200);
|
|
561
|
+
if (label === void 0) return null;
|
|
562
|
+
return {
|
|
563
|
+
type: "submit",
|
|
564
|
+
label,
|
|
565
|
+
...opt("action", action),
|
|
566
|
+
...opt("resetAction", str(v.resetAction, 200)),
|
|
567
|
+
...opt("groups", repairStrings(v.groups, GENUI_LIMITS.maxOptions, 200))
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
case "switch": {
|
|
571
|
+
const label = str(v.label, GENUI_LIMITS.maxString);
|
|
572
|
+
if (label === void 0) return null;
|
|
573
|
+
return {
|
|
574
|
+
type: "switch",
|
|
575
|
+
label,
|
|
576
|
+
...opt("checked", v.checked === true ? true : void 0),
|
|
577
|
+
...opt("action", str(v.action, 200))
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
case "slider": {
|
|
581
|
+
const min = num(v.min, -1e9, 1e9) ?? 0;
|
|
582
|
+
const max = num(v.max, -1e9, 1e9) ?? 100;
|
|
583
|
+
const lo = Math.min(min, max);
|
|
584
|
+
const hi = Math.max(min, max);
|
|
585
|
+
const step = num(v.step, 1e-9, Math.max(hi - lo, 1e-9));
|
|
586
|
+
const value = num(v.value, lo, hi) ?? lo;
|
|
587
|
+
return {
|
|
588
|
+
type: "slider",
|
|
589
|
+
min: lo,
|
|
590
|
+
max: hi,
|
|
591
|
+
...opt("step", step),
|
|
592
|
+
value,
|
|
593
|
+
...opt("label", str(v.label, GENUI_LIMITS.maxString)),
|
|
594
|
+
...opt("action", str(v.action, 200)),
|
|
595
|
+
...opt("id", str(v.id, 200))
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
case "textarea": return {
|
|
599
|
+
type: "textarea",
|
|
600
|
+
...opt("label", str(v.label, GENUI_LIMITS.maxString)),
|
|
601
|
+
...opt("placeholder", str(v.placeholder, GENUI_LIMITS.maxString)),
|
|
602
|
+
...opt("rows", int(v.rows, 1, 30)),
|
|
603
|
+
...opt("value", str(v.value, GENUI_LIMITS.maxString)),
|
|
604
|
+
...opt("action", str(v.action, 200)),
|
|
605
|
+
...opt("id", str(v.id, 200))
|
|
606
|
+
};
|
|
607
|
+
case "accordion": {
|
|
608
|
+
const items = repairAccordion(v.items, ctx, depth);
|
|
609
|
+
if (items === void 0) return null;
|
|
610
|
+
return {
|
|
611
|
+
type: "accordion",
|
|
612
|
+
items
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
case "copy": {
|
|
616
|
+
const text = str(v.text, GENUI_LIMITS.maxCode);
|
|
617
|
+
if (text === void 0) return null;
|
|
618
|
+
return {
|
|
619
|
+
type: "copy",
|
|
620
|
+
text,
|
|
621
|
+
...opt("label", str(v.label, 128))
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
case "mermaid": {
|
|
625
|
+
const code = str(v.code, GENUI_LIMITS.maxMermaid);
|
|
626
|
+
if (code === void 0) return null;
|
|
627
|
+
return {
|
|
628
|
+
type: "mermaid",
|
|
629
|
+
code
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
case "scene3d": {
|
|
633
|
+
const meshes = repairMeshes(v.meshes);
|
|
634
|
+
if (meshes === void 0) return null;
|
|
635
|
+
return {
|
|
636
|
+
type: "scene3d",
|
|
637
|
+
meshes,
|
|
638
|
+
...opt("title", str(v.title, GENUI_LIMITS.maxString)),
|
|
639
|
+
...opt("ambient", num(v.ambient, 0, 2)),
|
|
640
|
+
...opt("background", color(v.background))
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
case "diagram": return repairDiagram(v);
|
|
644
|
+
case "timeline": {
|
|
645
|
+
const items = repairTimeline(v.items, GENUI_LIMITS.maxTimelineItems);
|
|
646
|
+
if (items === void 0) return null;
|
|
647
|
+
return {
|
|
648
|
+
type: "timeline",
|
|
649
|
+
items
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
case "file-tree": {
|
|
653
|
+
const items = repairTree(v.items, GENUI_LIMITS.maxListItems);
|
|
654
|
+
if (items === void 0) return null;
|
|
655
|
+
return {
|
|
656
|
+
type: "file-tree",
|
|
657
|
+
items
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
case "breadcrumb": {
|
|
661
|
+
const items = repairStrings(v.items, GENUI_LIMITS.maxBreadcrumbItems, GENUI_LIMITS.maxString);
|
|
662
|
+
if (items === void 0) return null;
|
|
663
|
+
return {
|
|
664
|
+
type: "breadcrumb",
|
|
665
|
+
items
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
case "quiz": {
|
|
669
|
+
const question = str(v.question, GENUI_LIMITS.maxString);
|
|
670
|
+
const options = repairQuizOptions(v.options);
|
|
671
|
+
if (question === void 0 || options === void 0) return null;
|
|
672
|
+
return {
|
|
673
|
+
type: "quiz",
|
|
674
|
+
question,
|
|
675
|
+
options,
|
|
676
|
+
...opt("explanation", str(v.explanation, GENUI_LIMITS.maxString)),
|
|
677
|
+
...opt("id", str(v.id, 200)),
|
|
678
|
+
...opt("action", str(v.action, 200))
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
case "echart": {
|
|
682
|
+
const data = v.data !== void 0 ? repairChartData(v.data, GENUI_LIMITS.maxChartPoints) : void 0;
|
|
683
|
+
const series = v.series !== void 0 && Array.isArray(v.series) ? repairSeries(v.series, GENUI_LIMITS.maxPlotSeries, GENUI_LIMITS.maxChartPoints) : void 0;
|
|
684
|
+
const sanitized = v.option !== void 0 ? sanitizeEChartOption(v.option, 0, { count: GENUI_LIMITS.maxEChartOptionNodes }) : void 0;
|
|
685
|
+
const option = sanitized === void 0 || typeof sanitized !== "object" || sanitized === null || Array.isArray(sanitized) ? void 0 : sanitized;
|
|
686
|
+
if (option === void 0 && data === void 0 && series === void 0) return null;
|
|
687
|
+
return {
|
|
688
|
+
type: "echart",
|
|
689
|
+
...opt("title", str(v.title, GENUI_LIMITS.maxString)),
|
|
690
|
+
...opt("height", int(v.height, 100, 800)),
|
|
691
|
+
...opt("preset", enu(v.preset, ECHART_PRESETS)),
|
|
692
|
+
...opt("data", data),
|
|
693
|
+
...opt("series", series),
|
|
694
|
+
...opt("option", option)
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
default: return value;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
function repairStrings(v, cap, strCap) {
|
|
701
|
+
if (!Array.isArray(v)) return void 0;
|
|
702
|
+
const out = [];
|
|
703
|
+
for (const item of v) {
|
|
704
|
+
if (out.length >= cap) break;
|
|
705
|
+
if (typeof item === "string") out.push(item.slice(0, strCap));
|
|
706
|
+
else if (item !== null && typeof item === "object") {
|
|
707
|
+
const o = item;
|
|
708
|
+
const s = typeof o.label === "string" ? o.label : typeof o.value === "string" ? o.value : typeof o.title === "string" ? o.title : JSON.stringify(item);
|
|
709
|
+
out.push(s.slice(0, strCap));
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
return out;
|
|
713
|
+
}
|
|
714
|
+
function repairListItems(v, cap, ctx, depth) {
|
|
715
|
+
if (!Array.isArray(v)) return void 0;
|
|
716
|
+
const out = [];
|
|
717
|
+
for (const item of v) {
|
|
718
|
+
if (out.length >= cap) break;
|
|
719
|
+
if (typeof item === "string") {
|
|
720
|
+
out.push(item.slice(0, GENUI_LIMITS.maxString));
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
const o = obj(item);
|
|
724
|
+
const title = o === void 0 ? void 0 : str(o.title, GENUI_LIMITS.maxString);
|
|
725
|
+
if (title !== void 0) {
|
|
726
|
+
out.push({
|
|
727
|
+
title,
|
|
728
|
+
...opt("desc", o === void 0 ? void 0 : str(o.desc, GENUI_LIMITS.maxString))
|
|
729
|
+
});
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
if (o !== void 0 && typeof o.type === "string") {
|
|
733
|
+
if (ctx.remaining <= 0) break;
|
|
734
|
+
ctx.remaining -= 1;
|
|
735
|
+
const node = repairNode(o, ctx, depth);
|
|
736
|
+
if (node !== null) out.push(node);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
return out;
|
|
740
|
+
}
|
|
741
|
+
function repairRows(v, rowCap, colCap) {
|
|
742
|
+
if (!Array.isArray(v)) return void 0;
|
|
743
|
+
const out = [];
|
|
744
|
+
for (const row of v) {
|
|
745
|
+
if (out.length >= rowCap) break;
|
|
746
|
+
if (!Array.isArray(row)) continue;
|
|
747
|
+
const cells = [];
|
|
748
|
+
for (const cell of row) {
|
|
749
|
+
if (cells.length >= colCap) break;
|
|
750
|
+
if (typeof cell === "string") cells.push(cell.slice(0, 256));
|
|
751
|
+
else if (typeof cell === "number" && Number.isFinite(cell)) cells.push(cell);
|
|
752
|
+
}
|
|
753
|
+
if (cells.length > 0) out.push(cells);
|
|
754
|
+
}
|
|
755
|
+
return out;
|
|
756
|
+
}
|
|
757
|
+
function repairChartData(v, cap) {
|
|
758
|
+
if (!Array.isArray(v)) return void 0;
|
|
759
|
+
const out = [];
|
|
760
|
+
for (const datum of v) {
|
|
761
|
+
if (out.length >= cap) break;
|
|
762
|
+
const o = obj(datum);
|
|
763
|
+
const label = o === void 0 ? void 0 : str(o.label, 128);
|
|
764
|
+
const value = o === void 0 ? void 0 : num(o.value, -0xe8d4a51000, 0xe8d4a51000);
|
|
765
|
+
if (label === void 0 || value === void 0) continue;
|
|
766
|
+
out.push({
|
|
767
|
+
label,
|
|
768
|
+
value,
|
|
769
|
+
...opt("color", o === void 0 ? void 0 : color(o.color))
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
return out;
|
|
773
|
+
}
|
|
774
|
+
function repairSeries(v, cap, pointCap) {
|
|
775
|
+
if (!Array.isArray(v)) return void 0;
|
|
776
|
+
const out = [];
|
|
777
|
+
for (const s of v) {
|
|
778
|
+
if (out.length >= cap) break;
|
|
779
|
+
const o = obj(s);
|
|
780
|
+
const label = o === void 0 ? void 0 : str(o.label, 128);
|
|
781
|
+
const data = o === void 0 ? void 0 : repairChartData(o.data, pointCap);
|
|
782
|
+
if (label === void 0 || data === void 0) continue;
|
|
783
|
+
out.push({
|
|
784
|
+
label,
|
|
785
|
+
data,
|
|
786
|
+
...opt("color", o === void 0 ? void 0 : color(o.color))
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
return out;
|
|
790
|
+
}
|
|
791
|
+
function repairTabs(v, ctx, depth) {
|
|
792
|
+
if (!Array.isArray(v)) return void 0;
|
|
793
|
+
const out = [];
|
|
794
|
+
for (const tab of v) {
|
|
795
|
+
if (out.length >= GENUI_LIMITS.maxTabs) break;
|
|
796
|
+
const o = obj(tab);
|
|
797
|
+
const label = o === void 0 ? void 0 : str(o.label, 128);
|
|
798
|
+
if (label === void 0 || o === void 0) continue;
|
|
799
|
+
const rawItems = o.items !== void 0 ? o.items : o.content !== void 0 ? Array.isArray(o.content) ? o.content : [o.content] : void 0;
|
|
800
|
+
out.push({
|
|
801
|
+
label,
|
|
802
|
+
items: repairItems(rawItems, ctx, depth + 1)
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
return out;
|
|
806
|
+
}
|
|
807
|
+
/** Header text for an object-shaped table column ({title,key} antd style). */
|
|
808
|
+
function columnHeaderText(c) {
|
|
809
|
+
const o = obj(c);
|
|
810
|
+
if (o === void 0) return String(c);
|
|
811
|
+
for (const k of [
|
|
812
|
+
"title",
|
|
813
|
+
"label",
|
|
814
|
+
"key",
|
|
815
|
+
"dataIndex"
|
|
816
|
+
]) {
|
|
817
|
+
const s = o[k];
|
|
818
|
+
if (typeof s === "string" && s !== "") return s;
|
|
819
|
+
}
|
|
820
|
+
return JSON.stringify(c);
|
|
821
|
+
}
|
|
822
|
+
/** Row key for an object-shaped column, mirroring columnHeaderText's order. */
|
|
823
|
+
function columnKeyOf(c) {
|
|
824
|
+
const o = obj(c);
|
|
825
|
+
if (o === void 0) return void 0;
|
|
826
|
+
for (const k of [
|
|
827
|
+
"key",
|
|
828
|
+
"dataIndex",
|
|
829
|
+
"title",
|
|
830
|
+
"label"
|
|
831
|
+
]) {
|
|
832
|
+
const s = o[k];
|
|
833
|
+
if (typeof s === "string" && s !== "") return s;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
/** Cell text for object-array rows: strings/finite numbers pass through,
|
|
837
|
+
* everything else stringifies so the column alignment is preserved
|
|
838
|
+
* (repairRows would drop null/undefined cells and shift the row). */
|
|
839
|
+
function cellText(v) {
|
|
840
|
+
if (typeof v === "string") return v;
|
|
841
|
+
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
842
|
+
if (v === null || v === void 0) return "";
|
|
843
|
+
return JSON.stringify(v);
|
|
844
|
+
}
|
|
845
|
+
function repairPlotSeries(v, cap) {
|
|
846
|
+
if (!Array.isArray(v)) return void 0;
|
|
847
|
+
const out = [];
|
|
848
|
+
for (const s of v) {
|
|
849
|
+
if (out.length >= cap) break;
|
|
850
|
+
const o = obj(s);
|
|
851
|
+
const expr = o === void 0 ? void 0 : str(o.expr, 512);
|
|
852
|
+
if (expr === void 0 || o === void 0) continue;
|
|
853
|
+
const params = [];
|
|
854
|
+
if (Array.isArray(o.params)) for (const p of o.params) {
|
|
855
|
+
if (params.length >= GENUI_LIMITS.maxPlotParams) break;
|
|
856
|
+
const po = obj(p);
|
|
857
|
+
const name = po === void 0 ? void 0 : str(po.name, 64);
|
|
858
|
+
const value = po === void 0 ? void 0 : num(po.value, -1e9, 1e9);
|
|
859
|
+
if (name === void 0 || value === void 0) continue;
|
|
860
|
+
params.push({
|
|
861
|
+
name,
|
|
862
|
+
value,
|
|
863
|
+
...opt("min", po === void 0 ? void 0 : num(po.min, -1e9, 1e9)),
|
|
864
|
+
...opt("max", po === void 0 ? void 0 : num(po.max, -1e9, 1e9)),
|
|
865
|
+
...opt("step", po === void 0 ? void 0 : num(po.step, 1e-9, 1e9)),
|
|
866
|
+
...opt("animateTo", po === void 0 ? void 0 : num(po.animateTo, -1e9, 1e9)),
|
|
867
|
+
...opt("durationMs", po === void 0 ? void 0 : num(po.durationMs, 1, 12e4)),
|
|
868
|
+
...opt("loop", po === void 0 ? void 0 : po.loop === true ? true : void 0)
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
out.push({
|
|
872
|
+
expr,
|
|
873
|
+
...opt("label", str(o.label, 128)),
|
|
874
|
+
...opt("color", color(o.color)),
|
|
875
|
+
...opt("kind", enu(o.kind, PLOT_KINDS)),
|
|
876
|
+
...opt("params", params.length > 0 ? params : void 0)
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
return out;
|
|
880
|
+
}
|
|
881
|
+
function repairSteps(v) {
|
|
882
|
+
if (!Array.isArray(v)) return void 0;
|
|
883
|
+
const out = [];
|
|
884
|
+
for (const s of v) {
|
|
885
|
+
if (out.length >= GENUI_LIMITS.maxSteps) break;
|
|
886
|
+
const o = obj(s);
|
|
887
|
+
const title = o === void 0 ? void 0 : str(o.title, 256);
|
|
888
|
+
if (title === void 0) continue;
|
|
889
|
+
out.push({
|
|
890
|
+
title,
|
|
891
|
+
...opt("desc", o === void 0 ? void 0 : str(o.desc, GENUI_LIMITS.maxString))
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
return out;
|
|
895
|
+
}
|
|
896
|
+
function repairPairs(v, cap) {
|
|
897
|
+
if (!Array.isArray(v)) return void 0;
|
|
898
|
+
const out = [];
|
|
899
|
+
for (const p of v) {
|
|
900
|
+
if (out.length >= cap) break;
|
|
901
|
+
const o = obj(p);
|
|
902
|
+
const key = o === void 0 ? void 0 : str(o.key, 256);
|
|
903
|
+
const value = o === void 0 ? void 0 : str(o.value, GENUI_LIMITS.maxString);
|
|
904
|
+
if (key === void 0 || value === void 0) continue;
|
|
905
|
+
out.push({
|
|
906
|
+
key,
|
|
907
|
+
value
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
return out;
|
|
911
|
+
}
|
|
912
|
+
function repairDiffs(v) {
|
|
913
|
+
if (!Array.isArray(v)) return void 0;
|
|
914
|
+
const out = [];
|
|
915
|
+
for (const d of v) {
|
|
916
|
+
if (out.length >= 24) break;
|
|
917
|
+
const o = obj(d);
|
|
918
|
+
const path = o === void 0 ? void 0 : str(o.path, 1024);
|
|
919
|
+
const newText = o === void 0 ? void 0 : str(o.newText, 2e4);
|
|
920
|
+
if (path === void 0 || newText === void 0) continue;
|
|
921
|
+
const old = o === void 0 ? void 0 : o.oldText;
|
|
922
|
+
out.push({
|
|
923
|
+
path,
|
|
924
|
+
newText,
|
|
925
|
+
oldText: old === null || typeof old !== "string" ? null : old.slice(0, 2e4)
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
return out;
|
|
929
|
+
}
|
|
930
|
+
function repairAccordion(v, ctx, depth) {
|
|
931
|
+
if (!Array.isArray(v)) return void 0;
|
|
932
|
+
const out = [];
|
|
933
|
+
for (const item of v) {
|
|
934
|
+
if (out.length >= GENUI_LIMITS.maxAccordionItems) break;
|
|
935
|
+
const o = obj(item);
|
|
936
|
+
const title = o === void 0 ? void 0 : str(o.title, 256);
|
|
937
|
+
if (title === void 0 || o === void 0) continue;
|
|
938
|
+
out.push({
|
|
939
|
+
title,
|
|
940
|
+
items: repairItems(o.items, ctx, depth + 1)
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
return out;
|
|
944
|
+
}
|
|
945
|
+
function repairMeshes(v) {
|
|
946
|
+
if (!Array.isArray(v)) return void 0;
|
|
947
|
+
const out = [];
|
|
948
|
+
for (const m of v) {
|
|
949
|
+
if (out.length >= GENUI_LIMITS.maxMeshes) break;
|
|
950
|
+
const o = obj(m);
|
|
951
|
+
const shape = o === void 0 ? void 0 : enu(o.shape, MESH_SHAPES);
|
|
952
|
+
if (shape === void 0) continue;
|
|
953
|
+
const scale = o === void 0 ? void 0 : num(o.scale, -1e6, 1e6) ?? tuple3(o.scale);
|
|
954
|
+
const size = o === void 0 ? void 0 : num(o.size, -1e6, 1e6) ?? tuple3(o.size);
|
|
955
|
+
out.push({
|
|
956
|
+
shape,
|
|
957
|
+
...opt("color", o === void 0 ? void 0 : color(o.color)),
|
|
958
|
+
...opt("position", o === void 0 ? void 0 : tuple3(o.position)),
|
|
959
|
+
...opt("rotation", o === void 0 ? void 0 : tuple3(o.rotation)),
|
|
960
|
+
...opt("scale", scale),
|
|
961
|
+
...opt("size", size)
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
return out;
|
|
965
|
+
}
|
|
966
|
+
function tuple3(v) {
|
|
967
|
+
if (!Array.isArray(v) || v.length !== 3) return void 0;
|
|
968
|
+
const [a, b, c] = v;
|
|
969
|
+
if (typeof a !== "number" || !Number.isFinite(a) || typeof b !== "number" || !Number.isFinite(b) || typeof c !== "number" || !Number.isFinite(c)) return void 0;
|
|
970
|
+
return [
|
|
971
|
+
Math.min(1e6, Math.max(-1e6, a)),
|
|
972
|
+
Math.min(1e6, Math.max(-1e6, b)),
|
|
973
|
+
Math.min(1e6, Math.max(-1e6, c))
|
|
974
|
+
];
|
|
975
|
+
}
|
|
976
|
+
/** Clamp a coordinate/size to the 4px editorial grid. */
|
|
977
|
+
function grid4(v, min, max) {
|
|
978
|
+
return Math.min(max, Math.max(min, Math.round(v / 4) * 4));
|
|
979
|
+
}
|
|
980
|
+
function repairDiagramNodes(v) {
|
|
981
|
+
if (!Array.isArray(v)) return void 0;
|
|
982
|
+
const out = [];
|
|
983
|
+
const seen = /* @__PURE__ */ new Set();
|
|
984
|
+
for (const raw of v) {
|
|
985
|
+
if (out.length >= GENUI_LIMITS.maxDiagramNodes) break;
|
|
986
|
+
const o = obj(raw);
|
|
987
|
+
if (o === void 0) continue;
|
|
988
|
+
const id = str(o.id, 128);
|
|
989
|
+
const label = str(o.label, GENUI_LIMITS.maxString);
|
|
990
|
+
if (id === void 0 || label === void 0) continue;
|
|
991
|
+
if (seen.has(id)) continue;
|
|
992
|
+
seen.add(id);
|
|
993
|
+
const nodeType = enu(o.type, DIAGRAM_NODE_TYPES);
|
|
994
|
+
const x = o.x === void 0 ? void 0 : grid4(num(o.x, -1e6, 1e6) ?? 0, 0, 1e6);
|
|
995
|
+
const y = o.y === void 0 ? void 0 : grid4(num(o.y, -1e6, 1e6) ?? 0, 0, 1e6);
|
|
996
|
+
const w = o.w === void 0 ? void 0 : grid4(num(o.w, -1e6, 1e6) ?? 96, 40, 2e3);
|
|
997
|
+
const h = o.h === void 0 ? void 0 : grid4(num(o.h, -1e6, 1e6) ?? 48, 24, 1200);
|
|
998
|
+
out.push({
|
|
999
|
+
id,
|
|
1000
|
+
label,
|
|
1001
|
+
...opt("sub", str(o.sub, 256)),
|
|
1002
|
+
...opt("type", nodeType),
|
|
1003
|
+
...opt("x", x),
|
|
1004
|
+
...opt("y", y),
|
|
1005
|
+
...opt("w", w),
|
|
1006
|
+
...opt("h", h),
|
|
1007
|
+
...opt("tag", str(o.tag, 32))
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
return out;
|
|
1011
|
+
}
|
|
1012
|
+
function repairDiagramEdges(v) {
|
|
1013
|
+
if (v === void 0) return [];
|
|
1014
|
+
if (!Array.isArray(v)) return void 0;
|
|
1015
|
+
const out = [];
|
|
1016
|
+
for (const raw of v) {
|
|
1017
|
+
if (out.length >= GENUI_LIMITS.maxDiagramEdges) break;
|
|
1018
|
+
const o = obj(raw);
|
|
1019
|
+
if (o === void 0) continue;
|
|
1020
|
+
const from = str(o.from, 128);
|
|
1021
|
+
const to = str(o.to, 128);
|
|
1022
|
+
if (from === void 0 || to === void 0) continue;
|
|
1023
|
+
out.push({
|
|
1024
|
+
from,
|
|
1025
|
+
to,
|
|
1026
|
+
...opt("label", str(o.label, GENUI_LIMITS.maxDiagramLabel)),
|
|
1027
|
+
...opt("kind", enu(o.kind, DIAGRAM_EDGE_KINDS)),
|
|
1028
|
+
...opt("route", enu(o.route, DIAGRAM_ROUTES))
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
return out;
|
|
1032
|
+
}
|
|
1033
|
+
function repairDiagramTheme(v) {
|
|
1034
|
+
const o = obj(v);
|
|
1035
|
+
if (o === void 0) return void 0;
|
|
1036
|
+
const out = {};
|
|
1037
|
+
for (const key of [
|
|
1038
|
+
"paper",
|
|
1039
|
+
"paper-2",
|
|
1040
|
+
"ink",
|
|
1041
|
+
"muted",
|
|
1042
|
+
"soft",
|
|
1043
|
+
"rule",
|
|
1044
|
+
"accent",
|
|
1045
|
+
"accent-tint",
|
|
1046
|
+
"link"
|
|
1047
|
+
]) {
|
|
1048
|
+
const c = color(o[key]);
|
|
1049
|
+
if (c !== void 0) out[key] = c;
|
|
1050
|
+
}
|
|
1051
|
+
return Object.keys(out).length === 0 ? void 0 : out;
|
|
1052
|
+
}
|
|
1053
|
+
function repairDiagramZones(v) {
|
|
1054
|
+
if (v === void 0) return [];
|
|
1055
|
+
if (!Array.isArray(v)) return void 0;
|
|
1056
|
+
const out = [];
|
|
1057
|
+
for (const raw of v) {
|
|
1058
|
+
if (out.length >= GENUI_LIMITS.maxDiagramZones) break;
|
|
1059
|
+
const o = obj(raw);
|
|
1060
|
+
if (o === void 0) continue;
|
|
1061
|
+
const label = str(o.label, 64);
|
|
1062
|
+
if (label === void 0) continue;
|
|
1063
|
+
out.push({
|
|
1064
|
+
label,
|
|
1065
|
+
...opt("x", o.x === void 0 ? void 0 : grid4(num(o.x, -1e6, 1e6) ?? 0, 0, 1e6)),
|
|
1066
|
+
...opt("y", o.y === void 0 ? void 0 : grid4(num(o.y, -1e6, 1e6) ?? 0, 0, 1e6)),
|
|
1067
|
+
...opt("w", o.w === void 0 ? void 0 : grid4(num(o.w, -1e6, 1e6) ?? 100, 40, 2e3)),
|
|
1068
|
+
...opt("h", o.h === void 0 ? void 0 : grid4(num(o.h, -1e6, 1e6) ?? 100, 40, 1200))
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
return out;
|
|
1072
|
+
}
|
|
1073
|
+
function repairDiagram(v) {
|
|
1074
|
+
const o = obj(v);
|
|
1075
|
+
if (o === void 0) return null;
|
|
1076
|
+
const kind = enu(o.kind, DIAGRAM_KINDS);
|
|
1077
|
+
if (kind === void 0) return null;
|
|
1078
|
+
const nodes = repairDiagramNodes(o.nodes);
|
|
1079
|
+
if (nodes === void 0) return null;
|
|
1080
|
+
const edges = repairDiagramEdges(o.edges);
|
|
1081
|
+
if (edges === void 0) return null;
|
|
1082
|
+
const zones = repairDiagramZones(o.zones);
|
|
1083
|
+
if (zones === void 0) return null;
|
|
1084
|
+
return {
|
|
1085
|
+
type: "diagram",
|
|
1086
|
+
kind,
|
|
1087
|
+
nodes,
|
|
1088
|
+
edges,
|
|
1089
|
+
zones,
|
|
1090
|
+
...opt("variant", enu(o.variant, DIAGRAM_VARIANTS)),
|
|
1091
|
+
...opt("title", str(o.title, 256)),
|
|
1092
|
+
...opt("theme", repairDiagramTheme(o.theme))
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
function repairTimeline(v, cap) {
|
|
1096
|
+
if (!Array.isArray(v)) return void 0;
|
|
1097
|
+
const out = [];
|
|
1098
|
+
for (const item of v) {
|
|
1099
|
+
if (out.length >= cap) break;
|
|
1100
|
+
const o = obj(item);
|
|
1101
|
+
const title = o === void 0 ? void 0 : str(o.title, 256);
|
|
1102
|
+
if (title === void 0) continue;
|
|
1103
|
+
out.push({
|
|
1104
|
+
title,
|
|
1105
|
+
...opt("desc", o === void 0 ? void 0 : str(o.desc, GENUI_LIMITS.maxString)),
|
|
1106
|
+
...opt("time", o === void 0 ? void 0 : str(o.time, 128))
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
return out;
|
|
1110
|
+
}
|
|
1111
|
+
function repairTree(v, cap) {
|
|
1112
|
+
return walkTree(v, cap, GENUI_LIMITS.maxTreeDepth);
|
|
1113
|
+
}
|
|
1114
|
+
function walkTree(v, cap, depthLeft) {
|
|
1115
|
+
if (!Array.isArray(v)) return void 0;
|
|
1116
|
+
const out = [];
|
|
1117
|
+
for (const item of v) {
|
|
1118
|
+
if (out.length >= cap) break;
|
|
1119
|
+
const o = obj(item);
|
|
1120
|
+
const name = o === void 0 ? void 0 : str(o.name, 256);
|
|
1121
|
+
if (name === void 0) continue;
|
|
1122
|
+
const children = o !== void 0 && depthLeft > 0 && Array.isArray(o.children) ? walkTree(o.children, cap, depthLeft - 1) : void 0;
|
|
1123
|
+
out.push({
|
|
1124
|
+
name,
|
|
1125
|
+
...opt("type", o === void 0 ? void 0 : enu(o.type, FILE_TYPES)),
|
|
1126
|
+
...opt("children", children)
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
return out;
|
|
1130
|
+
}
|
|
1131
|
+
function repairQuizOptions(v) {
|
|
1132
|
+
if (!Array.isArray(v)) return void 0;
|
|
1133
|
+
const out = [];
|
|
1134
|
+
for (const optItem of v) {
|
|
1135
|
+
if (out.length >= GENUI_LIMITS.maxQuizOptions) break;
|
|
1136
|
+
const o = obj(optItem);
|
|
1137
|
+
const label = o === void 0 ? void 0 : str(o.label, 512);
|
|
1138
|
+
if (label === void 0) continue;
|
|
1139
|
+
out.push({
|
|
1140
|
+
label,
|
|
1141
|
+
...opt("correct", o === void 0 ? void 0 : o.correct === true ? true : void 0),
|
|
1142
|
+
...opt("feedback", o === void 0 ? void 0 : str(o.feedback, GENUI_LIMITS.maxString))
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
return out;
|
|
1146
|
+
}
|
|
1147
|
+
/**
|
|
1148
|
+
* Patterns that indicate HTML/script injection in a string field. ECharts
|
|
1149
|
+
* default `tooltip.renderMode: 'html'` writes tooltip content via
|
|
1150
|
+
* `innerHTML`; even with renderMode forced to 'richText' (see below),
|
|
1151
|
+
* filtering these patterns is defense-in-depth — a model (or a
|
|
1152
|
+
* prompt-injected model) should never emit `<script>`, `onerror=`, or
|
|
1153
|
+
* `javascript:` inside a chart option string.
|
|
1154
|
+
*/
|
|
1155
|
+
const ECHART_HTML_DANGER_RE = /<(?:script|img|svg|iframe|video|audio|object|embed|source)\b|on[a-z]+\s*=|javascript:/i;
|
|
1156
|
+
/**
|
|
1157
|
+
* Sanitize an ECharts option object: depth-bounded, budget-bounded
|
|
1158
|
+
* pass-through that strips dangerous values (functions, `url()` in styles,
|
|
1159
|
+
* HTML/script injection patterns in strings) but preserves the object shape
|
|
1160
|
+
* ECharts needs. Scalars are KEPT: ECharts options are full of them,
|
|
1161
|
+
* including inside `data` arrays (`data: [120, 150, 180]`,
|
|
1162
|
+
* `xAxis.data: ['1月', '2月']`). Previously a scalar hit the plain-object
|
|
1163
|
+
* gate below and returned undefined, so every primitive-valued array was
|
|
1164
|
+
* filtered to empty and dropped — a chart with a full `option` rendered
|
|
1165
|
+
* with empty series (blank canvas). This is a safety walk, not an ECharts
|
|
1166
|
+
* semantic validator.
|
|
1167
|
+
*
|
|
1168
|
+
* Security: `tooltip.renderMode` is forced to `'richText'` on every tooltip
|
|
1169
|
+
* object. ECharts' default `'html'` mode writes tooltip content via
|
|
1170
|
+
* `innerHTML`, which is an XSS vector when the option originates from model
|
|
1171
|
+
* output — a prompt-injected model could emit
|
|
1172
|
+
* `{"tooltip":{"formatter":"<img src=x onerror=...>"}}` and execute
|
|
1173
|
+
* arbitrary script. `richText` renders as text, never touching innerHTML.
|
|
1174
|
+
*/
|
|
1175
|
+
function sanitizeEChartOption(v, depth, budget) {
|
|
1176
|
+
if (budget.count <= 0) return void 0;
|
|
1177
|
+
budget.count -= 1;
|
|
1178
|
+
if (depth > GENUI_LIMITS.maxEChartOptionDepth) return void 0;
|
|
1179
|
+
if (typeof v === "string") {
|
|
1180
|
+
const s = v.slice(0, GENUI_LIMITS.maxString);
|
|
1181
|
+
if (s.toLowerCase().includes("url(") || ECHART_HTML_DANGER_RE.test(s)) return void 0;
|
|
1182
|
+
return s;
|
|
1183
|
+
}
|
|
1184
|
+
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
1185
|
+
if (typeof v === "boolean") return v;
|
|
1186
|
+
if (v === null) return null;
|
|
1187
|
+
if (Array.isArray(v)) {
|
|
1188
|
+
const cap = Math.min(v.length, GENUI_LIMITS.maxEChartArrayLen);
|
|
1189
|
+
const arr = [];
|
|
1190
|
+
for (let i = 0; i < cap; i++) {
|
|
1191
|
+
const s = sanitizeEChartOption(v[i], depth + 1, budget);
|
|
1192
|
+
if (s !== void 0) arr.push(s);
|
|
1193
|
+
}
|
|
1194
|
+
return arr.length > 0 ? arr : void 0;
|
|
1195
|
+
}
|
|
1196
|
+
const o = obj(v);
|
|
1197
|
+
if (o === void 0) return void 0;
|
|
1198
|
+
const out = {};
|
|
1199
|
+
for (const [key, val] of Object.entries(o)) {
|
|
1200
|
+
const s = sanitizeEChartOption(val, depth + 1, budget);
|
|
1201
|
+
if (s === void 0) continue;
|
|
1202
|
+
if (key === "tooltip" && typeof s === "object" && s !== null && !Array.isArray(s)) s.renderMode = "richText";
|
|
1203
|
+
out[key] = s;
|
|
1204
|
+
}
|
|
1205
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
1206
|
+
}
|
|
1207
|
+
/**
|
|
1208
|
+
* Deterministically repair a raw spec value into a renderable GenuiSpec.
|
|
1209
|
+
* Returns null only when the root is not an object with an `items` array
|
|
1210
|
+
* (a bare component root is wrapped into a col first — the documented fence
|
|
1211
|
+
* vocabulary allows single-component bodies); every other defect is healed by
|
|
1212
|
+
* dropping/clamping/truncating. Idempotent: repairing a repaired spec is a
|
|
1213
|
+
* no-op.
|
|
1214
|
+
*/
|
|
1215
|
+
function repairGenuiSpec(value) {
|
|
1216
|
+
const v = obj(value);
|
|
1217
|
+
if (v === void 0) return null;
|
|
1218
|
+
if (!Array.isArray(v.items)) {
|
|
1219
|
+
const wrapped = wrapSingleComponentRoot(value);
|
|
1220
|
+
if (wrapped === null) return null;
|
|
1221
|
+
return repairGenuiSpec(wrapped);
|
|
1222
|
+
}
|
|
1223
|
+
const ctx = { remaining: GENUI_LIMITS.maxNodes };
|
|
1224
|
+
return {
|
|
1225
|
+
...opt("title", str(v.title, GENUI_LIMITS.maxString)),
|
|
1226
|
+
...opt("gap", num(v.gap, 0, 96)),
|
|
1227
|
+
...opt("panel", v.panel === true ? true : void 0),
|
|
1228
|
+
...opt("append", v.append === true ? true : void 0),
|
|
1229
|
+
items: repairItems(v.items, ctx, 0)
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
/**
|
|
1233
|
+
* Count the nodes of a spec tree (every item, descending into tabs /
|
|
1234
|
+
* accordion / file-tree / list containers — the same descent
|
|
1235
|
+
* `validateGenuiSpec` walks). Shared by the panel fold (node-budget gate)
|
|
1236
|
+
* and validation, so the panel never runs a second, divergent traversal.
|
|
1237
|
+
* `cap` bounds the walk for hostile inputs; the panel passes
|
|
1238
|
+
* `PANEL_LIMITS.maxNodes + 1` to detect overflow without counting the whole
|
|
1239
|
+
* tree.
|
|
1240
|
+
*/
|
|
1241
|
+
function countGenuiNodes(value, cap = Number.POSITIVE_INFINITY) {
|
|
1242
|
+
let count = 0;
|
|
1243
|
+
const walk = (list) => {
|
|
1244
|
+
if (!Array.isArray(list)) return;
|
|
1245
|
+
for (const item of list) {
|
|
1246
|
+
if (count >= cap) return;
|
|
1247
|
+
count += 1;
|
|
1248
|
+
const v = obj(item);
|
|
1249
|
+
if (v === void 0) continue;
|
|
1250
|
+
if (v.type === "tabs" && Array.isArray(v.tabs)) for (const t of v.tabs) {
|
|
1251
|
+
if (count >= cap) return;
|
|
1252
|
+
const to = obj(t);
|
|
1253
|
+
if (to !== void 0) walk(to.items);
|
|
1254
|
+
}
|
|
1255
|
+
else if (v.type === "accordion" && Array.isArray(v.items)) for (const it of v.items) {
|
|
1256
|
+
if (count >= cap) return;
|
|
1257
|
+
const io = obj(it);
|
|
1258
|
+
if (io !== void 0) walk(io.items);
|
|
1259
|
+
}
|
|
1260
|
+
else if ((v.type === "row" || v.type === "col" || v.type === "grid" || v.type === "card") && Array.isArray(v.items)) walk(v.items);
|
|
1261
|
+
else if (v.type === "file-tree" && Array.isArray(v.items)) walk(v.items);
|
|
1262
|
+
else if (v.type === "list" && Array.isArray(v.items)) for (const li of v.items) {
|
|
1263
|
+
if (count >= cap) return;
|
|
1264
|
+
const lo = obj(li);
|
|
1265
|
+
if (lo !== void 0 && typeof lo.type === "string") walk([lo]);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
};
|
|
1269
|
+
const root = obj(value);
|
|
1270
|
+
walk(root === void 0 ? [] : root.items);
|
|
1271
|
+
return count;
|
|
1272
|
+
}
|
|
1273
|
+
/** Every white-listed node `type`. Keep in sync with the repairNode switch —
|
|
1274
|
+
* validate_dsh_ui uses it to tell declared GenUI nodes apart from unrelated
|
|
1275
|
+
* `"type"` strings (e.g. file-tree's `{type:'file'}` children). */
|
|
1276
|
+
const GENUI_NODE_TYPES = /* @__PURE__ */ new Set([
|
|
1277
|
+
"accordion",
|
|
1278
|
+
"audio",
|
|
1279
|
+
"avatar",
|
|
1280
|
+
"badge",
|
|
1281
|
+
"breadcrumb",
|
|
1282
|
+
"button",
|
|
1283
|
+
"callout",
|
|
1284
|
+
"card",
|
|
1285
|
+
"chart",
|
|
1286
|
+
"checkbox",
|
|
1287
|
+
"code",
|
|
1288
|
+
"col",
|
|
1289
|
+
"copy",
|
|
1290
|
+
"diff",
|
|
1291
|
+
"divider",
|
|
1292
|
+
"file-tree",
|
|
1293
|
+
"grid",
|
|
1294
|
+
"input",
|
|
1295
|
+
"json",
|
|
1296
|
+
"keyvalue",
|
|
1297
|
+
"link",
|
|
1298
|
+
"list",
|
|
1299
|
+
"mermaid",
|
|
1300
|
+
"plot",
|
|
1301
|
+
"progress",
|
|
1302
|
+
"quiz",
|
|
1303
|
+
"radio",
|
|
1304
|
+
"row",
|
|
1305
|
+
"scene3d",
|
|
1306
|
+
"select",
|
|
1307
|
+
"slider",
|
|
1308
|
+
"spacer",
|
|
1309
|
+
"stat",
|
|
1310
|
+
"steps",
|
|
1311
|
+
"submit",
|
|
1312
|
+
"switch",
|
|
1313
|
+
"table",
|
|
1314
|
+
"tabs",
|
|
1315
|
+
"text",
|
|
1316
|
+
"textarea",
|
|
1317
|
+
"timeline",
|
|
1318
|
+
"video",
|
|
1319
|
+
"echart",
|
|
1320
|
+
"diagram"
|
|
1321
|
+
]);
|
|
1322
|
+
/**
|
|
1323
|
+
* Count DECLARED nodes in a raw spec tree: objects whose `type` is a
|
|
1324
|
+
* white-listed string, descending the same containers `countGenuiNodes`
|
|
1325
|
+
* walks. `validate_dsh_ui` compares this with the repaired count to surface
|
|
1326
|
+
* children the repair silently dropped (blank-render class of bugs, issue
|
|
1327
|
+
* #42) instead of reporting a green check on a half-empty tree.
|
|
1328
|
+
*/
|
|
1329
|
+
function countDeclaredGenuiNodes(value, cap = Number.POSITIVE_INFINITY) {
|
|
1330
|
+
let count = 0;
|
|
1331
|
+
const declared = (candidate) => {
|
|
1332
|
+
const o = obj(candidate);
|
|
1333
|
+
return o !== void 0 && typeof o.type === "string" && GENUI_NODE_TYPES.has(o.type);
|
|
1334
|
+
};
|
|
1335
|
+
const walk = (list) => {
|
|
1336
|
+
if (!Array.isArray(list)) return;
|
|
1337
|
+
for (const item of list) {
|
|
1338
|
+
if (count >= cap) return;
|
|
1339
|
+
if (!declared(item)) continue;
|
|
1340
|
+
count += 1;
|
|
1341
|
+
const v = obj(item);
|
|
1342
|
+
if (v === void 0) continue;
|
|
1343
|
+
if (v.type === "tabs" && Array.isArray(v.tabs)) for (const t of v.tabs) walkItemsOf(t);
|
|
1344
|
+
else if (v.type === "accordion" && Array.isArray(v.items)) for (const it of v.items) walkItemsOf(it);
|
|
1345
|
+
else if ((v.type === "row" || v.type === "col" || v.type === "grid" || v.type === "card") && Array.isArray(v.items)) walk(v.items);
|
|
1346
|
+
else if (v.type === "list" && Array.isArray(v.items)) {
|
|
1347
|
+
for (const li of v.items) if (declared(li)) walk([li]);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
};
|
|
1351
|
+
const walkItemsOf = (holder) => {
|
|
1352
|
+
const o = obj(holder);
|
|
1353
|
+
if (o === void 0) return;
|
|
1354
|
+
const items = o.items !== void 0 ? o.items : o.content;
|
|
1355
|
+
if (Array.isArray(items)) walk(items);
|
|
1356
|
+
else if (declared(items)) walk([items]);
|
|
1357
|
+
};
|
|
1358
|
+
const root = obj(value);
|
|
1359
|
+
if (root === void 0) return count;
|
|
1360
|
+
if (!Array.isArray(root.items) && declared(value)) walk([value]);
|
|
1361
|
+
else walk(root.items);
|
|
1362
|
+
return count;
|
|
1363
|
+
}
|
|
1364
|
+
//#endregion
|
|
1365
|
+
//#region src/shared/fence-repair.ts
|
|
1366
|
+
/**
|
|
1367
|
+
* Tier-2 repair — SETTLED MESSAGES ONLY (never while streaming): heals
|
|
1368
|
+
* structural incompleteness — missing closing quotes/brackets — by appending
|
|
1369
|
+
* the missing terminators, and heals stray closers — a `]` mistyped as `}` or
|
|
1370
|
+
* a duplicated terminator — by skipping closers that do not match the open
|
|
1371
|
+
* stack (they cannot be legal JSON). Callers gate it on settled messages (the
|
|
1372
|
+
* client uses the host-provided fence source; the validate tool is by
|
|
1373
|
+
* definition pre-emission), so a streaming half can never flash premature UI.
|
|
1374
|
+
*
|
|
1375
|
+
* ONE unified scan: the tier-1 fixes (quote escaping + trailing-comma drops)
|
|
1376
|
+
* are folded into the same pass, so bodies that combine BOTH defect classes
|
|
1377
|
+
* (a trailing comma AND a missing closer) heal in one shot — the old
|
|
1378
|
+
* two-phase chain lost tier-1's partial work when its whole-body parse
|
|
1379
|
+
* failed, and re-scanning the raw text could not compose the repairs.
|
|
1380
|
+
* Adopted only when the completed body parses as whole JSON.
|
|
1381
|
+
*/
|
|
1382
|
+
function completeFenceJson(raw) {
|
|
1383
|
+
try {
|
|
1384
|
+
JSON.parse(raw);
|
|
1385
|
+
return null;
|
|
1386
|
+
} catch {}
|
|
1387
|
+
let out = "";
|
|
1388
|
+
const stack = [];
|
|
1389
|
+
let inString = false;
|
|
1390
|
+
let escaped = false;
|
|
1391
|
+
let repairs = 0;
|
|
1392
|
+
for (let i = 0; i < raw.length; i++) {
|
|
1393
|
+
const ch = raw[i];
|
|
1394
|
+
if (escaped) {
|
|
1395
|
+
out += ch;
|
|
1396
|
+
escaped = false;
|
|
1397
|
+
continue;
|
|
1398
|
+
}
|
|
1399
|
+
if (inString) {
|
|
1400
|
+
if (ch === "\\") {
|
|
1401
|
+
out += ch;
|
|
1402
|
+
escaped = true;
|
|
1403
|
+
continue;
|
|
1404
|
+
}
|
|
1405
|
+
if (ch !== "\"") {
|
|
1406
|
+
out += ch;
|
|
1407
|
+
continue;
|
|
1408
|
+
}
|
|
1409
|
+
let j = i + 1;
|
|
1410
|
+
while (j < raw.length && (raw[j] === " " || raw[j] === " " || raw[j] === "\n" || raw[j] === "\r")) j++;
|
|
1411
|
+
const next = j < raw.length ? raw[j] : "";
|
|
1412
|
+
if (next === "," || next === "]" || next === "}" || next === ":" || next === "") {
|
|
1413
|
+
inString = false;
|
|
1414
|
+
out += ch;
|
|
1415
|
+
} else {
|
|
1416
|
+
out += "\\\"";
|
|
1417
|
+
repairs++;
|
|
1418
|
+
}
|
|
1419
|
+
continue;
|
|
1420
|
+
}
|
|
1421
|
+
if (ch === "\"") {
|
|
1422
|
+
inString = true;
|
|
1423
|
+
out += ch;
|
|
1424
|
+
continue;
|
|
1425
|
+
}
|
|
1426
|
+
if (ch === "{") {
|
|
1427
|
+
stack.push("}");
|
|
1428
|
+
out += ch;
|
|
1429
|
+
continue;
|
|
1430
|
+
}
|
|
1431
|
+
if (ch === "[") {
|
|
1432
|
+
stack.push("]");
|
|
1433
|
+
out += ch;
|
|
1434
|
+
continue;
|
|
1435
|
+
}
|
|
1436
|
+
if (ch === "}" || ch === "]") {
|
|
1437
|
+
if (stack[stack.length - 1] === ch) {
|
|
1438
|
+
stack.pop();
|
|
1439
|
+
out += ch;
|
|
1440
|
+
} else repairs++;
|
|
1441
|
+
continue;
|
|
1442
|
+
}
|
|
1443
|
+
if (ch === ",") {
|
|
1444
|
+
let j = i + 1;
|
|
1445
|
+
while (j < raw.length && (raw[j] === " " || raw[j] === " " || raw[j] === "\n" || raw[j] === "\r")) j++;
|
|
1446
|
+
const next = j < raw.length ? raw[j] : "";
|
|
1447
|
+
if (next === "}" || next === "]" || next === "") {
|
|
1448
|
+
repairs++;
|
|
1449
|
+
continue;
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
out += ch;
|
|
1453
|
+
}
|
|
1454
|
+
if (inString) {
|
|
1455
|
+
out += "\"";
|
|
1456
|
+
repairs++;
|
|
1457
|
+
}
|
|
1458
|
+
while (stack.length > 0) {
|
|
1459
|
+
out += stack.pop();
|
|
1460
|
+
repairs++;
|
|
1461
|
+
}
|
|
1462
|
+
if (repairs === 0) return null;
|
|
1463
|
+
try {
|
|
1464
|
+
JSON.parse(out);
|
|
1465
|
+
return {
|
|
1466
|
+
text: out,
|
|
1467
|
+
repairs
|
|
1468
|
+
};
|
|
1469
|
+
} catch {
|
|
1470
|
+
return null;
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
//#endregion
|
|
1474
|
+
//#region src/plugin/tool.ts
|
|
1475
|
+
/**
|
|
1476
|
+
* Arguments schema: an open `spec` slot. The schema must NOT reject anything
|
|
1477
|
+
* the guard could repair — the model's component trees are imperfect by
|
|
1478
|
+
* nature, and the guard heals them; argument validation would only strand
|
|
1479
|
+
* them. `additionalProperties: false` keeps the call shape honest.
|
|
1480
|
+
*
|
|
1481
|
+
* `spec` IS typed `object` on purpose: the guard can only repair plain
|
|
1482
|
+
* records (a serialized JSON string, array, or scalar root is unusable), so
|
|
1483
|
+
* argument validation rejecting non-objects loses nothing repairable — and
|
|
1484
|
+
* it stops the model from double-encoding the tree as a string (observed
|
|
1485
|
+
* twice in the wild), failing fast with a clear schema error instead.
|
|
1486
|
+
*/
|
|
1487
|
+
const RENDER_UI_PARAMETERS = {
|
|
1488
|
+
type: "object",
|
|
1489
|
+
properties: { spec: {
|
|
1490
|
+
type: "object",
|
|
1491
|
+
description: "GenUI component tree (white-listed vocabulary, see the dsh-ui fence section in the system prompt). Deep-validated and repaired by the renderer. Pass the spec as a JSON OBJECT — never as a serialized JSON string (a string fails argument validation).",
|
|
1492
|
+
properties: {
|
|
1493
|
+
title: {
|
|
1494
|
+
type: "string",
|
|
1495
|
+
description: "Short title shown as the card banner."
|
|
1496
|
+
},
|
|
1497
|
+
gap: {
|
|
1498
|
+
type: "number",
|
|
1499
|
+
description: "Vertical gap between root items in px."
|
|
1500
|
+
},
|
|
1501
|
+
panel: {
|
|
1502
|
+
type: "boolean",
|
|
1503
|
+
description: "Panel-only: renders into the session panel dock instead of the message flow."
|
|
1504
|
+
},
|
|
1505
|
+
items: {
|
|
1506
|
+
type: "array",
|
|
1507
|
+
description: "Root component list (white-listed vocabulary).",
|
|
1508
|
+
items: { type: "object" }
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
} },
|
|
1512
|
+
required: ["spec"],
|
|
1513
|
+
additionalProperties: false
|
|
1514
|
+
};
|
|
1515
|
+
/** The tool's canonical value is a short model-facing summary string. */
|
|
1516
|
+
const RENDER_UI_OUTPUT_SCHEMA = {
|
|
1517
|
+
type: "string",
|
|
1518
|
+
description: "One-line human-readable render summary for the model."
|
|
1519
|
+
};
|
|
1520
|
+
/**
|
|
1521
|
+
* Read the `spec` argument defensively (presenters run on replayed args).
|
|
1522
|
+
*
|
|
1523
|
+
* The harness tool-call bridge has been observed to deliver arguments in
|
|
1524
|
+
* shapes other than the authored `{ spec: <object> }`:
|
|
1525
|
+
* - `{ spec: "<JSON string>" }` — spec serialized to text;
|
|
1526
|
+
* - `{ arguments: "<JSON string>" }` / `{ arguments: <object> }` — a
|
|
1527
|
+
* double-encoded wrapper from the SDK tool-call bridge (seen live in the
|
|
1528
|
+
* web GUI: small specs arrived wrapped this way, large specs arrived with
|
|
1529
|
+
* their JSON corrupted mid-stream);
|
|
1530
|
+
* - a bare JSON string (double-encoded root).
|
|
1531
|
+
* Each shape is unwrapped here so the guard can repair the actual tree.
|
|
1532
|
+
* Corrupted JSON cannot be recovered (bytes were lost in transit): it yields
|
|
1533
|
+
* `undefined` plus a diagnostic log line for the transport-layer bug.
|
|
1534
|
+
*/
|
|
1535
|
+
function specOf(args) {
|
|
1536
|
+
if (typeof args === "string") return parseSpecJson(args, "bare-string");
|
|
1537
|
+
if (typeof args !== "object" || args === null) return void 0;
|
|
1538
|
+
const record = args;
|
|
1539
|
+
if ("spec" in record) {
|
|
1540
|
+
const s = record.spec;
|
|
1541
|
+
if (typeof s === "string") return parseSpecJson(s, "spec-string");
|
|
1542
|
+
return unwrapSpec(s, "spec");
|
|
1543
|
+
}
|
|
1544
|
+
if ("arguments" in record) {
|
|
1545
|
+
const a = record.arguments;
|
|
1546
|
+
if (typeof a === "string") return parseSpecJson(a, "arguments-string");
|
|
1547
|
+
if (typeof a === "object" && a !== null) return unwrapSpec(a, "arguments");
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
/**
|
|
1551
|
+
* Peel nested `{ spec: ... }` wrapper layers. Observed bridge shapes nest the
|
|
1552
|
+
* authored `spec` object one or more levels deep (e.g. the serialized text
|
|
1553
|
+
* inside `{ arguments: "..." }` is itself `{ spec: { title, gap, items } }`),
|
|
1554
|
+
* so unwrapping stops only at a value that carries no `spec` key.
|
|
1555
|
+
*/
|
|
1556
|
+
function unwrapSpec(value, shape) {
|
|
1557
|
+
if (typeof value === "object" && value !== null) {
|
|
1558
|
+
const record = value;
|
|
1559
|
+
if ("spec" in record) {
|
|
1560
|
+
const s = record.spec;
|
|
1561
|
+
if (typeof s === "string") return parseSpecJson(s, `${shape}/spec-string`);
|
|
1562
|
+
return unwrapSpec(s, `${shape}/spec`);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
return value;
|
|
1566
|
+
}
|
|
1567
|
+
/** Try to decode a serialized spec; log a diagnostic when it is broken. */
|
|
1568
|
+
function parseSpecJson(raw, shape) {
|
|
1569
|
+
try {
|
|
1570
|
+
return unwrapSpec(JSON.parse(raw), shape);
|
|
1571
|
+
} catch (error) {
|
|
1572
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1573
|
+
const pos = /position (\d+)/.exec(detail)?.[1] ?? "?";
|
|
1574
|
+
console.error(`[genui-tool] spec wrapped as ${shape} but its JSON is broken (${raw.length} bytes, error at ${pos}); cannot recover — bytes lost in transit`);
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
/** Total node count of a repaired spec — the shared guard traversal
|
|
1579
|
+
* (`countGenuiNodes`) so the tool reports the SAME number the panel fold and
|
|
1580
|
+
* validation use. A local walker used to under-count specs whose content
|
|
1581
|
+
* lives inside tabs/accordion/file-tree (their children are not `.items`). */
|
|
1582
|
+
function countNodes(spec) {
|
|
1583
|
+
return countGenuiNodes(spec, GENUI_LIMITS.maxNodes);
|
|
1584
|
+
}
|
|
1585
|
+
/** Tool-call title shared by the pending and completed presentations. */
|
|
1586
|
+
function cardTitle(args) {
|
|
1587
|
+
const spec = repairGenuiSpec(specOf(args));
|
|
1588
|
+
return spec === null ? void 0 : `渲染 UI:${spec.title ?? "未命名"}`;
|
|
1589
|
+
}
|
|
1590
|
+
/**
|
|
1591
|
+
* Build the render_ui tool definition. Registered by the plugin node half;
|
|
1592
|
+
* `ctx.tools.register` consumes it exactly like a `defineTool` result.
|
|
1593
|
+
*/
|
|
1594
|
+
function createRenderUiTool() {
|
|
1595
|
+
return {
|
|
1596
|
+
name: "render_ui",
|
|
1597
|
+
description: "Render an interactive UI card in the conversation tool row by passing a GenUI spec (a white-listed component tree; the same vocabulary as the ```dsh-ui fence, see the system prompt). Use it when the user asks for a structured panel, dashboard, or form that belongs in the tool row rather than inline in the reply. The card is interactive client-side (tabs, buttons, inputs, switches); components carrying an \"action\" field send [genui-action] back to you when the user interacts, and you should re-render the updated UI.",
|
|
1598
|
+
parameters: RENDER_UI_PARAMETERS,
|
|
1599
|
+
output: {
|
|
1600
|
+
schema: RENDER_UI_OUTPUT_SCHEMA,
|
|
1601
|
+
render(_args, value) {
|
|
1602
|
+
return [{
|
|
1603
|
+
type: "text",
|
|
1604
|
+
text: String(value)
|
|
1605
|
+
}];
|
|
1606
|
+
},
|
|
1607
|
+
presentationMeta(args) {
|
|
1608
|
+
return repairGenuiSpec(specOf(args));
|
|
1609
|
+
}
|
|
1610
|
+
},
|
|
1611
|
+
async execute(args) {
|
|
1612
|
+
const spec = repairGenuiSpec(specOf(args));
|
|
1613
|
+
if (spec === null) return "render_ui:spec 无效 —— 根对象需要 \"items\" 数组(组件树白名单见系统提示词),请修正后重试。";
|
|
1614
|
+
return `已渲染 UI「${spec.title ?? "未命名"}」(${countNodes(spec)} 个组件)。用户现在可以看到这张卡片;组件带 action 时,用户交互会以 [genui-action] 消息发回给你,届时请重新渲染更新后的界面。`;
|
|
1615
|
+
},
|
|
1616
|
+
presentCall(args) {
|
|
1617
|
+
const title = cardTitle(args);
|
|
1618
|
+
return title === void 0 ? void 0 : {
|
|
1619
|
+
card: "generic",
|
|
1620
|
+
title,
|
|
1621
|
+
kind: "other"
|
|
1622
|
+
};
|
|
1623
|
+
},
|
|
1624
|
+
presentResult(args) {
|
|
1625
|
+
const title = cardTitle(args);
|
|
1626
|
+
return title === void 0 ? void 0 : {
|
|
1627
|
+
card: "generic",
|
|
1628
|
+
title
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
};
|
|
1632
|
+
}
|
|
1633
|
+
/**
|
|
1634
|
+
* The `validate_dsh_ui` tool: a model-facing pre-flight check for the
|
|
1635
|
+
* ```dsh-ui fence channel. The model calls it with the JSON text it is about
|
|
1636
|
+
* to put inside a fence; it reports whether the body parses as a valid GenUI
|
|
1637
|
+
* spec, and when it does not, WHERE it breaks and WHAT is likely wrong
|
|
1638
|
+
* (bracket counts, common typo classes) so the model can fix and re-validate
|
|
1639
|
+
* before emitting — turning "render a red banner after the fact" into
|
|
1640
|
+
* "verify before you send". Purely local: no LLM, no network, no DOM.
|
|
1641
|
+
*/
|
|
1642
|
+
const VALIDATE_DESCRIPTION = "Validate the JSON body of a ```dsh-ui fence BEFORE emitting it — use for non-trivial specs (≥3 nodes or containing a table); skip for trivial ones (≤2 nodes). Pass the exact JSON text you are about to put inside the fence as the \"spec\" argument (a string). Returns ✅ when it parses as a valid GenUI spec, or ❌ with the exact position, bracket counts, and likely causes when it does not — fix the JSON, re-validate, and only then emit the fence. When the JSON is broken but repairable (unescaped quotes, trailing commas, missing closers), the ❌ reply INCLUDES the auto-repaired JSON — copy it verbatim into the fence instead of rewriting by hand.";
|
|
1643
|
+
const VALIDATE_PARAMETERS = {
|
|
1644
|
+
type: "object",
|
|
1645
|
+
properties: { spec: {
|
|
1646
|
+
oneOf: [{
|
|
1647
|
+
type: "string",
|
|
1648
|
+
description: "The exact JSON text of the fence body."
|
|
1649
|
+
}, {
|
|
1650
|
+
type: "object",
|
|
1651
|
+
description: "The spec object (serialized before validation)."
|
|
1652
|
+
}],
|
|
1653
|
+
description: "The dsh-ui fence body to validate: pass the JSON as a string for an exact check, or as the spec object."
|
|
1654
|
+
} },
|
|
1655
|
+
required: ["spec"],
|
|
1656
|
+
additionalProperties: false
|
|
1657
|
+
};
|
|
1658
|
+
/** Read the fence-body text from the call args (string preferred, object serialized). */
|
|
1659
|
+
function fenceTextOf(args) {
|
|
1660
|
+
if (typeof args === "string") return args;
|
|
1661
|
+
if (typeof args !== "object" || args === null) return null;
|
|
1662
|
+
const record = args;
|
|
1663
|
+
const s = "spec" in record ? record.spec : "arguments" in record ? record.arguments : void 0;
|
|
1664
|
+
if (typeof s === "string") return s;
|
|
1665
|
+
if (typeof s === "object" && s !== null) return JSON.stringify(s);
|
|
1666
|
+
return null;
|
|
1667
|
+
}
|
|
1668
|
+
/** Count structural brackets outside string literals. */
|
|
1669
|
+
function bracketCounts(raw) {
|
|
1670
|
+
const counts = {
|
|
1671
|
+
"{": 0,
|
|
1672
|
+
"}": 0,
|
|
1673
|
+
"[": 0,
|
|
1674
|
+
"]": 0
|
|
1675
|
+
};
|
|
1676
|
+
let inString = false;
|
|
1677
|
+
let escaped = false;
|
|
1678
|
+
for (let i = 0; i < raw.length; i++) {
|
|
1679
|
+
const ch = raw[i];
|
|
1680
|
+
if (escaped) {
|
|
1681
|
+
escaped = false;
|
|
1682
|
+
continue;
|
|
1683
|
+
}
|
|
1684
|
+
if (inString) {
|
|
1685
|
+
if (ch === "\\") escaped = true;
|
|
1686
|
+
else if (ch === "\"") inString = false;
|
|
1687
|
+
continue;
|
|
1688
|
+
}
|
|
1689
|
+
if (ch === "\"") {
|
|
1690
|
+
inString = true;
|
|
1691
|
+
continue;
|
|
1692
|
+
}
|
|
1693
|
+
if (ch === "{") counts["{"] += 1;
|
|
1694
|
+
else if (ch === "}") counts["}"] += 1;
|
|
1695
|
+
else if (ch === "[") counts["["] += 1;
|
|
1696
|
+
else if (ch === "]") counts["]"] += 1;
|
|
1697
|
+
}
|
|
1698
|
+
return counts;
|
|
1699
|
+
}
|
|
1700
|
+
/** Short structural hint from bracket counts (empty when balanced). */
|
|
1701
|
+
function bracketDiagnostic(raw) {
|
|
1702
|
+
const c = bracketCounts(raw);
|
|
1703
|
+
const diffs = [];
|
|
1704
|
+
if (c["{"] !== c["}"]) {
|
|
1705
|
+
const d = c["{"] - c["}"];
|
|
1706
|
+
diffs.push(`{ ×${c["{"]} / } ×${c["}"]} → ${d > 0 ? `缺 ${d} 个 }` : `多 ${-d} 个 }`}`);
|
|
1707
|
+
}
|
|
1708
|
+
if (c["["] !== c["]"]) {
|
|
1709
|
+
const d = c["["] - c["]"];
|
|
1710
|
+
diffs.push(`[ ×${c["["]} / ] ×${c["]"]} → ${d > 0 ? `缺 ${d} 个 ]` : `多 ${-d} 个 ]`}`);
|
|
1711
|
+
}
|
|
1712
|
+
return diffs.length === 0 ? "" : ` 括号计数:${diffs.join(";")}(长表格最易在收尾处错位,如把 ]]}]} 写成 ]}]}]})\n`;
|
|
1713
|
+
}
|
|
1714
|
+
const COMMON_CAUSES = "常见原因:① 收尾括号错位/缺失({ 与 }、[ 与 ] 数量不相等)② 字符串值内用了半角引号 \"(中文引语请用 “” 或 「」)③ 尾随逗号 ④ 字符串未闭合";
|
|
1715
|
+
/** Build the validate_dsh_ui tool definition (registered alongside render_ui). */
|
|
1716
|
+
function createValidateDshUiTool() {
|
|
1717
|
+
return {
|
|
1718
|
+
name: "validate_dsh_ui",
|
|
1719
|
+
description: VALIDATE_DESCRIPTION,
|
|
1720
|
+
parameters: VALIDATE_PARAMETERS,
|
|
1721
|
+
output: {
|
|
1722
|
+
schema: {
|
|
1723
|
+
type: "string",
|
|
1724
|
+
description: "Validation verdict for the model."
|
|
1725
|
+
},
|
|
1726
|
+
render(_args, value) {
|
|
1727
|
+
return [{
|
|
1728
|
+
type: "text",
|
|
1729
|
+
text: String(value)
|
|
1730
|
+
}];
|
|
1731
|
+
}
|
|
1732
|
+
},
|
|
1733
|
+
async execute(args) {
|
|
1734
|
+
const raw = fenceTextOf(args);
|
|
1735
|
+
if (raw === null || raw.trim() === "") return "❌ validate_dsh_ui:缺少 spec 参数 —— 把围栏 JSON 文本作为 spec 传入。";
|
|
1736
|
+
let parsed;
|
|
1737
|
+
try {
|
|
1738
|
+
parsed = JSON.parse(raw);
|
|
1739
|
+
} catch (error) {
|
|
1740
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1741
|
+
const repaired = completeFenceJson(raw);
|
|
1742
|
+
if (repaired !== null && repairGenuiSpec(JSON.parse(repaired.text)) !== null) return `❌ dsh-ui 围栏 JSON 解析失败:${detail}。\n${bracketDiagnostic(raw)} 已自动修复 ${repaired.repairs} 处,下面是修复后的 JSON,直接作为围栏正文发出即可(无需再验证):\n\`\`\`\n${repaired.text}\n\`\`\``;
|
|
1743
|
+
return `❌ dsh-ui 围栏 JSON 解析失败:${detail}。\n${bracketDiagnostic(raw)} 自动修复未能恢复(结构损坏),请按错误信息修正后重新调用本工具验证,通过后再发出围栏。\n${COMMON_CAUSES}`;
|
|
1744
|
+
}
|
|
1745
|
+
const spec = repairGenuiSpec(parsed);
|
|
1746
|
+
if (spec === null) return "❌ 不是合法 GenUI spec:根对象需要 \"items\" 数组,且每个节点 type 必须在白名单内(见系统提示词)。请修正后重新验证。";
|
|
1747
|
+
const validCount = countNodes(spec);
|
|
1748
|
+
const declaredCount = countDeclaredGenuiNodes(parsed, GENUI_LIMITS.maxNodes + 1);
|
|
1749
|
+
if (declaredCount > validCount) return `❌ 验证未通过:检测到声明了 ${declaredCount} 个组件,但仅成功解析出 ${validCount} 个(有 ${declaredCount - validCount} 个组件因字段格式异常被丢弃)。常见原因:table 的 columns/rows 不是二维字符串数组、tabs 的 items/content 缺失、嵌套组件字段类型不符。请修正后重新验证。`;
|
|
1750
|
+
return `✅ dsh-ui spec 合法(${validCount} 个组件),可以发出围栏。`;
|
|
1751
|
+
},
|
|
1752
|
+
presentCall() {
|
|
1753
|
+
return {
|
|
1754
|
+
card: "generic",
|
|
1755
|
+
title: "验证 dsh-ui 围栏",
|
|
1756
|
+
kind: "other"
|
|
1757
|
+
};
|
|
1758
|
+
},
|
|
1759
|
+
presentResult() {
|
|
1760
|
+
return {
|
|
1761
|
+
card: "generic",
|
|
1762
|
+
title: "验证 dsh-ui 围栏"
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
1765
|
+
};
|
|
1766
|
+
}
|
|
1767
|
+
//#endregion
|
|
1768
|
+
//#region src/plugin/index.ts
|
|
1769
|
+
/** Convention: tool guidance uses 100–199; bash's section is 104. */
|
|
1770
|
+
const GENUI_SECTION_ORDER = 105;
|
|
1771
|
+
/**
|
|
1772
|
+
* The mermaid/three engines ship as standalone IIFE bundles under
|
|
1773
|
+
* `lib/assets/` and are fetched by the client ONLY when a spec needs them.
|
|
1774
|
+
* This route serves them from the plugin's own package directory through the
|
|
1775
|
+
* host webserver service — the longest-prefix rule lets it win over the
|
|
1776
|
+
* generic `/plugins` bundle route, and no host source change is needed. The
|
|
1777
|
+
* service is optional at this plugin's start time (same ordering reality as
|
|
1778
|
+
* the tools registry), so registration probes immediately AND on the
|
|
1779
|
+
* `internal/service` event, exactly like the tools registration below.
|
|
1780
|
+
*/
|
|
1781
|
+
/** Route prefix under /plugins; anything under it is this plugin's asset. */
|
|
1782
|
+
const ASSET_ROUTE_PATH = "/plugins/@changfenhuang/dsh-genui/assets";
|
|
1783
|
+
/** Safe flat file names only: no slashes, no traversal, js assets only. */
|
|
1784
|
+
const ASSET_FILE_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*\.js$/;
|
|
1785
|
+
/** The handler itself (registered via the optional webServer probe). */
|
|
1786
|
+
async function serveGenuiAsset(req, res) {
|
|
1787
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
1788
|
+
res.writeHead(405);
|
|
1789
|
+
res.end();
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1792
|
+
let pathname;
|
|
1793
|
+
try {
|
|
1794
|
+
pathname = decodeURIComponent(new URL(req.url ?? "/", "http://x").pathname);
|
|
1795
|
+
} catch {
|
|
1796
|
+
res.writeHead(400);
|
|
1797
|
+
res.end();
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
const rel = pathname.startsWith(`${ASSET_ROUTE_PATH}/`) ? pathname.slice(40) : null;
|
|
1801
|
+
if (rel === null) {
|
|
1802
|
+
res.writeHead(404);
|
|
1803
|
+
res.end();
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
const file = rel.slice(1);
|
|
1807
|
+
if (!ASSET_FILE_RE.test(file)) {
|
|
1808
|
+
res.writeHead(404);
|
|
1809
|
+
res.end();
|
|
1810
|
+
return;
|
|
1811
|
+
}
|
|
1812
|
+
try {
|
|
1813
|
+
const dir = fileURLToPath(new URL("./assets/", import.meta.url));
|
|
1814
|
+
const body = await readFile(join(dir, file));
|
|
1815
|
+
res.writeHead(200, {
|
|
1816
|
+
"content-type": "text/javascript; charset=utf-8",
|
|
1817
|
+
"cache-control": "no-cache"
|
|
1818
|
+
});
|
|
1819
|
+
res.end(body);
|
|
1820
|
+
} catch {
|
|
1821
|
+
res.writeHead(404);
|
|
1822
|
+
res.end();
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
/** The fence language description injected into every assembled system prompt.
|
|
1826
|
+
* Deliberately slim: the `genui` skill carries the full component→field
|
|
1827
|
+
* mapping; this section keeps only the contract that must always be
|
|
1828
|
+
* present (fence syntax, type whitelist, and critical behavioral rules). */
|
|
1829
|
+
const GENUI_SECTION_TEXT = `You can render interactive UI components INSIDE your reply — between paragraphs — by emitting a fenced block with the language tag \`dsh-ui\` containing a JSON spec:
|
|
1830
|
+
|
|
1831
|
+
\`\`\`dsh-ui
|
|
1832
|
+
{"title":"可选标题","gap":14,"items":[...]}
|
|
1833
|
+
\`\`\`
|
|
1834
|
+
|
|
1835
|
+
The spec is a white-listed component tree rendered inline where the fence sits. Only these \`type\` values; the \`genui\` skill, when available, carries the full content→component mapping and per-component field details:
|
|
1836
|
+
|
|
1837
|
+
- 布局: text · row · col · grid · card · divider · spacer
|
|
1838
|
+
- 展示: badge · stat · progress · list · table · keyvalue · avatar · audio · video · timeline · file-tree · breadcrumb · callout · steps · diff · json · code · copy
|
|
1839
|
+
- 图表: chart (bars|line|donut) · echart (preset|option) · plot (函数图)
|
|
1840
|
+
- 交互: button · input · textarea · select · checkbox · switch · slider · radio · submit · quiz · link · tabs · accordion
|
|
1841
|
+
- 高级: mermaid (flowchart/sequence/class/gantt/pie/er/state/journey) · diagram (编辑级架构/流程图,27 种 kind) · scene3d (3D WebGL)
|
|
1842
|
+
|
|
1843
|
+
Rules:
|
|
1844
|
+
- 触发: 结构化表达优于纯文本时主动用(要点、强调、对比、流程、步骤、状态、数据、演示),纯问答与一句话不套 UI;一个主题一个主组件,每次 3–8 个组件,同一数据不重复出现。
|
|
1845
|
+
- JSON 严格: 坏围栏降级为代码块;≥3 节点或含 table 的围栏发出前调用 validate_dsh_ui,❌ 修好再发(若附「已自动修复」JSON 照抄即可)。
|
|
1846
|
+
- 规模: ≤200 节点、嵌套≤8 层(超出被截断);3D mesh 1–5;plot 给合理 xMin/xMax。
|
|
1847
|
+
- LOCAL-FIRST + actions: UI 能自己做的状态变化(判卷、判题、重置、展开、选中)就地完成,零往返;action 只用于必须模型参与的事。交互组件带 "action":"name",交互以 [genui-action] name + 组件数据回传,届时重渲染更新 UI;无 action 的按钮禁用。
|
|
1848
|
+
- Durable state: 交互状态按「会话+内容指纹」持久化——刷新/重放恢复;重渲染相同内容保留,新内容重置。
|
|
1849
|
+
- 卷子模式: 每题一个 radio(group+answer+explanation)+ 一个 submit(groups 全列),本地判分。
|
|
1850
|
+
- Secrets ban: 不索取密码、API Key、Token、恢复码;需要时拒绝并解释。
|
|
1851
|
+
- Tool channel: render_ui 工具把同一 spec 渲染为工具行卡片(交付物型界面用);围栏用于回答内联 UI。
|
|
1852
|
+
- Panel: "panel":true 只渲染进会话面板 dock 并原地更新;"append":true 追加合并(同标签 tabs 追加/新标签加入/尾部追加);上限 200 节点/200 次追加,满了发 replace 重建。面板组件来的 [genui-action] 只回一个 panel:true 围栏 + 至多一行 10 字内确认,不解释、不用普通围栏。`;
|
|
1853
|
+
/**
|
|
1854
|
+
* Register the GenUI output-language section and the render_ui tool.
|
|
1855
|
+
* @param ctx - cordis context.
|
|
1856
|
+
*/
|
|
1857
|
+
const inject = ["systemPrompt"];
|
|
1858
|
+
function apply(ctx) {
|
|
1859
|
+
ctx.systemPrompt.section({
|
|
1860
|
+
name: "genui:fence",
|
|
1861
|
+
order: 105,
|
|
1862
|
+
text: GENUI_SECTION_TEXT
|
|
1863
|
+
});
|
|
1864
|
+
let registered = false;
|
|
1865
|
+
const tryRegister = (value) => {
|
|
1866
|
+
if (registered) return;
|
|
1867
|
+
const tools = value ?? ctx.reflect.get("tools", false);
|
|
1868
|
+
if (tools === void 0) return;
|
|
1869
|
+
tools.register(createRenderUiTool());
|
|
1870
|
+
tools.register(createValidateDshUiTool());
|
|
1871
|
+
registered = true;
|
|
1872
|
+
};
|
|
1873
|
+
tryRegister(void 0);
|
|
1874
|
+
ctx.on("internal/service", (name, value) => {
|
|
1875
|
+
if (name === "tools") tryRegister(value);
|
|
1876
|
+
});
|
|
1877
|
+
let assetsRegistered = false;
|
|
1878
|
+
const tryRegisterAssets = (value) => {
|
|
1879
|
+
if (assetsRegistered) return;
|
|
1880
|
+
const webServer = value ?? ctx.reflect.get("webServer", false);
|
|
1881
|
+
if (webServer === void 0) return;
|
|
1882
|
+
webServer.register({
|
|
1883
|
+
kind: "prefix",
|
|
1884
|
+
path: ASSET_ROUTE_PATH,
|
|
1885
|
+
handler: serveGenuiAsset
|
|
1886
|
+
});
|
|
1887
|
+
assetsRegistered = true;
|
|
1888
|
+
};
|
|
1889
|
+
tryRegisterAssets(void 0);
|
|
1890
|
+
ctx.on("internal/service", (name, value) => {
|
|
1891
|
+
if (name === "webServer") tryRegisterAssets(value);
|
|
1892
|
+
});
|
|
1893
|
+
}
|
|
1894
|
+
//#endregion
|
|
1895
|
+
export { GENUI_SECTION_ORDER, GENUI_SECTION_TEXT, apply, inject };
|