@changfenhuang/dsh-genui 0.9.3 → 0.9.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +7 -18
- package/README.zh-CN.md +13 -18
- package/SKILL.md +4 -2
- package/lib/assets/three.js +1 -1
- package/lib/client.js +3 -3
- package/lib/index.js +273 -61
- package/lib/types/client/GenuiBlock.d.ts +5 -2
- package/lib/types/client/TemplateDrawer.d.ts +7 -0
- package/lib/types/client/achievement-store.d.ts +35 -0
- package/lib/types/client/achievement-toast.d.ts +2 -0
- package/lib/types/client/achievements.d.ts +48 -0
- package/lib/types/client/blocks/advanced.d.ts +1 -1
- package/lib/types/client/blocks/image.d.ts +6 -0
- package/lib/types/client/blocks/state.d.ts +2 -0
- package/lib/types/client/fence-render.d.ts +3 -0
- package/lib/types/client/gallery.d.ts +3 -5
- package/lib/types/client/guard.d.ts +10 -5
- package/lib/types/client/panel.d.ts +4 -1
- package/lib/types/client/spec.d.ts +8 -1
- package/lib/types/client/templates.d.ts +22 -0
- package/lib/types/plugin/chart-contract.d.ts +7 -0
- package/lib/types/plugin/index.d.ts +1 -1
- package/package.json +9 -8
- package/src/client/GenuiBlock.module.css +141 -10
- package/src/client/GenuiBlock.tsx +53 -25
- package/src/client/PlotBlock.tsx +25 -10
- package/src/client/TemplateDrawer.module.css +152 -0
- package/src/client/TemplateDrawer.tsx +137 -0
- package/src/client/achievement-store.ts +151 -0
- package/src/client/achievement-toast.tsx +68 -0
- package/src/client/achievements.ts +111 -0
- package/src/client/blocks/advanced.tsx +34 -25
- package/src/client/blocks/charts.tsx +4 -4
- package/src/client/blocks/image.tsx +25 -0
- package/src/client/blocks/render-node.tsx +3 -0
- package/src/client/blocks/state.ts +2 -0
- package/src/client/dom-fence.tsx +12 -5
- package/src/client/fence-render.tsx +35 -11
- package/src/client/gallery.ts +4 -5
- package/src/client/guard.ts +125 -29
- package/src/client/index.tsx +13 -0
- package/src/client/panel.tsx +109 -10
- package/src/client/safe-math.ts +29 -1
- package/src/client/scene3d-core.ts +5 -2
- package/src/client/spec.ts +9 -1
- package/src/client/templates.ts +280 -0
- package/src/plugin/chart-contract.ts +103 -0
- package/src/plugin/index.ts +48 -2
- package/src/plugin/tool.ts +25 -4
package/lib/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
1
2
|
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
5
|
//#region src/client/spec.ts
|
|
5
6
|
/**
|
|
@@ -138,7 +139,7 @@ function enu(v, values) {
|
|
|
138
139
|
return inEnum(v, values) ? v : void 0;
|
|
139
140
|
}
|
|
140
141
|
/** Plain object (not array, not null). */
|
|
141
|
-
function obj(v) {
|
|
142
|
+
function obj$1(v) {
|
|
142
143
|
return typeof v === "object" && v !== null && !Array.isArray(v) ? v : void 0;
|
|
143
144
|
}
|
|
144
145
|
/**
|
|
@@ -280,7 +281,7 @@ function repairItems(list, ctx, depth) {
|
|
|
280
281
|
}
|
|
281
282
|
function repairNode(value, ctx, depth) {
|
|
282
283
|
if (depth > GENUI_LIMITS.maxDepth) return null;
|
|
283
|
-
const v = obj(value);
|
|
284
|
+
const v = obj$1(value);
|
|
284
285
|
if (v === void 0) return null;
|
|
285
286
|
const type = v.type;
|
|
286
287
|
if (typeof type !== "string") return null;
|
|
@@ -369,6 +370,15 @@ function repairNode(value, ctx, depth) {
|
|
|
369
370
|
...opt("href", safeHref(v.href))
|
|
370
371
|
};
|
|
371
372
|
}
|
|
373
|
+
case "image": {
|
|
374
|
+
const src = safeMediaSrc(v.src);
|
|
375
|
+
if (src === void 0) return null;
|
|
376
|
+
return {
|
|
377
|
+
type: "image",
|
|
378
|
+
src,
|
|
379
|
+
...opt("alt", str(v.alt, GENUI_LIMITS.maxString))
|
|
380
|
+
};
|
|
381
|
+
}
|
|
372
382
|
case "audio": {
|
|
373
383
|
const src = safeMediaSrc(v.src);
|
|
374
384
|
if (src === void 0) return null;
|
|
@@ -720,7 +730,7 @@ function repairListItems(v, cap, ctx, depth) {
|
|
|
720
730
|
out.push(item.slice(0, GENUI_LIMITS.maxString));
|
|
721
731
|
continue;
|
|
722
732
|
}
|
|
723
|
-
const o = obj(item);
|
|
733
|
+
const o = obj$1(item);
|
|
724
734
|
const title = o === void 0 ? void 0 : str(o.title, GENUI_LIMITS.maxString);
|
|
725
735
|
if (title !== void 0) {
|
|
726
736
|
out.push({
|
|
@@ -759,7 +769,7 @@ function repairChartData(v, cap) {
|
|
|
759
769
|
const out = [];
|
|
760
770
|
for (const datum of v) {
|
|
761
771
|
if (out.length >= cap) break;
|
|
762
|
-
const o = obj(datum);
|
|
772
|
+
const o = obj$1(datum);
|
|
763
773
|
const label = o === void 0 ? void 0 : str(o.label, 128);
|
|
764
774
|
const value = o === void 0 ? void 0 : num(o.value, -0xe8d4a51000, 0xe8d4a51000);
|
|
765
775
|
if (label === void 0 || value === void 0) continue;
|
|
@@ -776,7 +786,7 @@ function repairSeries(v, cap, pointCap) {
|
|
|
776
786
|
const out = [];
|
|
777
787
|
for (const s of v) {
|
|
778
788
|
if (out.length >= cap) break;
|
|
779
|
-
const o = obj(s);
|
|
789
|
+
const o = obj$1(s);
|
|
780
790
|
const label = o === void 0 ? void 0 : str(o.label, 128);
|
|
781
791
|
const data = o === void 0 ? void 0 : repairChartData(o.data, pointCap);
|
|
782
792
|
if (label === void 0 || data === void 0) continue;
|
|
@@ -793,7 +803,7 @@ function repairTabs(v, ctx, depth) {
|
|
|
793
803
|
const out = [];
|
|
794
804
|
for (const tab of v) {
|
|
795
805
|
if (out.length >= GENUI_LIMITS.maxTabs) break;
|
|
796
|
-
const o = obj(tab);
|
|
806
|
+
const o = obj$1(tab);
|
|
797
807
|
const label = o === void 0 ? void 0 : str(o.label, 128);
|
|
798
808
|
if (label === void 0 || o === void 0) continue;
|
|
799
809
|
const rawItems = o.items !== void 0 ? o.items : o.content !== void 0 ? Array.isArray(o.content) ? o.content : [o.content] : void 0;
|
|
@@ -806,7 +816,7 @@ function repairTabs(v, ctx, depth) {
|
|
|
806
816
|
}
|
|
807
817
|
/** Header text for an object-shaped table column ({title,key} antd style). */
|
|
808
818
|
function columnHeaderText(c) {
|
|
809
|
-
const o = obj(c);
|
|
819
|
+
const o = obj$1(c);
|
|
810
820
|
if (o === void 0) return String(c);
|
|
811
821
|
for (const k of [
|
|
812
822
|
"title",
|
|
@@ -821,7 +831,7 @@ function columnHeaderText(c) {
|
|
|
821
831
|
}
|
|
822
832
|
/** Row key for an object-shaped column, mirroring columnHeaderText's order. */
|
|
823
833
|
function columnKeyOf(c) {
|
|
824
|
-
const o = obj(c);
|
|
834
|
+
const o = obj$1(c);
|
|
825
835
|
if (o === void 0) return void 0;
|
|
826
836
|
for (const k of [
|
|
827
837
|
"key",
|
|
@@ -847,13 +857,13 @@ function repairPlotSeries(v, cap) {
|
|
|
847
857
|
const out = [];
|
|
848
858
|
for (const s of v) {
|
|
849
859
|
if (out.length >= cap) break;
|
|
850
|
-
const o = obj(s);
|
|
860
|
+
const o = obj$1(s);
|
|
851
861
|
const expr = o === void 0 ? void 0 : str(o.expr, 512);
|
|
852
862
|
if (expr === void 0 || o === void 0) continue;
|
|
853
863
|
const params = [];
|
|
854
864
|
if (Array.isArray(o.params)) for (const p of o.params) {
|
|
855
865
|
if (params.length >= GENUI_LIMITS.maxPlotParams) break;
|
|
856
|
-
const po = obj(p);
|
|
866
|
+
const po = obj$1(p);
|
|
857
867
|
const name = po === void 0 ? void 0 : str(po.name, 64);
|
|
858
868
|
const value = po === void 0 ? void 0 : num(po.value, -1e9, 1e9);
|
|
859
869
|
if (name === void 0 || value === void 0) continue;
|
|
@@ -883,7 +893,7 @@ function repairSteps(v) {
|
|
|
883
893
|
const out = [];
|
|
884
894
|
for (const s of v) {
|
|
885
895
|
if (out.length >= GENUI_LIMITS.maxSteps) break;
|
|
886
|
-
const o = obj(s);
|
|
896
|
+
const o = obj$1(s);
|
|
887
897
|
const title = o === void 0 ? void 0 : str(o.title, 256);
|
|
888
898
|
if (title === void 0) continue;
|
|
889
899
|
out.push({
|
|
@@ -898,7 +908,7 @@ function repairPairs(v, cap) {
|
|
|
898
908
|
const out = [];
|
|
899
909
|
for (const p of v) {
|
|
900
910
|
if (out.length >= cap) break;
|
|
901
|
-
const o = obj(p);
|
|
911
|
+
const o = obj$1(p);
|
|
902
912
|
const key = o === void 0 ? void 0 : str(o.key, 256);
|
|
903
913
|
const value = o === void 0 ? void 0 : str(o.value, GENUI_LIMITS.maxString);
|
|
904
914
|
if (key === void 0 || value === void 0) continue;
|
|
@@ -914,7 +924,7 @@ function repairDiffs(v) {
|
|
|
914
924
|
const out = [];
|
|
915
925
|
for (const d of v) {
|
|
916
926
|
if (out.length >= 24) break;
|
|
917
|
-
const o = obj(d);
|
|
927
|
+
const o = obj$1(d);
|
|
918
928
|
const path = o === void 0 ? void 0 : str(o.path, 1024);
|
|
919
929
|
const newText = o === void 0 ? void 0 : str(o.newText, 2e4);
|
|
920
930
|
if (path === void 0 || newText === void 0) continue;
|
|
@@ -932,7 +942,7 @@ function repairAccordion(v, ctx, depth) {
|
|
|
932
942
|
const out = [];
|
|
933
943
|
for (const item of v) {
|
|
934
944
|
if (out.length >= GENUI_LIMITS.maxAccordionItems) break;
|
|
935
|
-
const o = obj(item);
|
|
945
|
+
const o = obj$1(item);
|
|
936
946
|
const title = o === void 0 ? void 0 : str(o.title, 256);
|
|
937
947
|
if (title === void 0 || o === void 0) continue;
|
|
938
948
|
out.push({
|
|
@@ -947,7 +957,7 @@ function repairMeshes(v) {
|
|
|
947
957
|
const out = [];
|
|
948
958
|
for (const m of v) {
|
|
949
959
|
if (out.length >= GENUI_LIMITS.maxMeshes) break;
|
|
950
|
-
const o = obj(m);
|
|
960
|
+
const o = obj$1(m);
|
|
951
961
|
const shape = o === void 0 ? void 0 : enu(o.shape, MESH_SHAPES);
|
|
952
962
|
if (shape === void 0) continue;
|
|
953
963
|
const scale = o === void 0 ? void 0 : num(o.scale, -1e6, 1e6) ?? tuple3(o.scale);
|
|
@@ -983,7 +993,7 @@ function repairDiagramNodes(v) {
|
|
|
983
993
|
const seen = /* @__PURE__ */ new Set();
|
|
984
994
|
for (const raw of v) {
|
|
985
995
|
if (out.length >= GENUI_LIMITS.maxDiagramNodes) break;
|
|
986
|
-
const o = obj(raw);
|
|
996
|
+
const o = obj$1(raw);
|
|
987
997
|
if (o === void 0) continue;
|
|
988
998
|
const id = str(o.id, 128);
|
|
989
999
|
const label = str(o.label, GENUI_LIMITS.maxString);
|
|
@@ -1015,7 +1025,7 @@ function repairDiagramEdges(v) {
|
|
|
1015
1025
|
const out = [];
|
|
1016
1026
|
for (const raw of v) {
|
|
1017
1027
|
if (out.length >= GENUI_LIMITS.maxDiagramEdges) break;
|
|
1018
|
-
const o = obj(raw);
|
|
1028
|
+
const o = obj$1(raw);
|
|
1019
1029
|
if (o === void 0) continue;
|
|
1020
1030
|
const from = str(o.from, 128);
|
|
1021
1031
|
const to = str(o.to, 128);
|
|
@@ -1031,7 +1041,7 @@ function repairDiagramEdges(v) {
|
|
|
1031
1041
|
return out;
|
|
1032
1042
|
}
|
|
1033
1043
|
function repairDiagramTheme(v) {
|
|
1034
|
-
const o = obj(v);
|
|
1044
|
+
const o = obj$1(v);
|
|
1035
1045
|
if (o === void 0) return void 0;
|
|
1036
1046
|
const out = {};
|
|
1037
1047
|
for (const key of [
|
|
@@ -1056,7 +1066,7 @@ function repairDiagramZones(v) {
|
|
|
1056
1066
|
const out = [];
|
|
1057
1067
|
for (const raw of v) {
|
|
1058
1068
|
if (out.length >= GENUI_LIMITS.maxDiagramZones) break;
|
|
1059
|
-
const o = obj(raw);
|
|
1069
|
+
const o = obj$1(raw);
|
|
1060
1070
|
if (o === void 0) continue;
|
|
1061
1071
|
const label = str(o.label, 64);
|
|
1062
1072
|
if (label === void 0) continue;
|
|
@@ -1071,7 +1081,7 @@ function repairDiagramZones(v) {
|
|
|
1071
1081
|
return out;
|
|
1072
1082
|
}
|
|
1073
1083
|
function repairDiagram(v) {
|
|
1074
|
-
const o = obj(v);
|
|
1084
|
+
const o = obj$1(v);
|
|
1075
1085
|
if (o === void 0) return null;
|
|
1076
1086
|
const kind = enu(o.kind, DIAGRAM_KINDS);
|
|
1077
1087
|
if (kind === void 0) return null;
|
|
@@ -1097,7 +1107,7 @@ function repairTimeline(v, cap) {
|
|
|
1097
1107
|
const out = [];
|
|
1098
1108
|
for (const item of v) {
|
|
1099
1109
|
if (out.length >= cap) break;
|
|
1100
|
-
const o = obj(item);
|
|
1110
|
+
const o = obj$1(item);
|
|
1101
1111
|
const title = o === void 0 ? void 0 : str(o.title, 256);
|
|
1102
1112
|
if (title === void 0) continue;
|
|
1103
1113
|
out.push({
|
|
@@ -1116,7 +1126,7 @@ function walkTree(v, cap, depthLeft) {
|
|
|
1116
1126
|
const out = [];
|
|
1117
1127
|
for (const item of v) {
|
|
1118
1128
|
if (out.length >= cap) break;
|
|
1119
|
-
const o = obj(item);
|
|
1129
|
+
const o = obj$1(item);
|
|
1120
1130
|
const name = o === void 0 ? void 0 : str(o.name, 256);
|
|
1121
1131
|
if (name === void 0) continue;
|
|
1122
1132
|
const children = o !== void 0 && depthLeft > 0 && Array.isArray(o.children) ? walkTree(o.children, cap, depthLeft - 1) : void 0;
|
|
@@ -1133,7 +1143,7 @@ function repairQuizOptions(v, answer) {
|
|
|
1133
1143
|
const out = [];
|
|
1134
1144
|
for (const optItem of v) {
|
|
1135
1145
|
if (out.length >= GENUI_LIMITS.maxQuizOptions) break;
|
|
1136
|
-
const o = obj(optItem);
|
|
1146
|
+
const o = obj$1(optItem);
|
|
1137
1147
|
const label = typeof optItem === "string" ? str(optItem, 512) : o === void 0 ? void 0 : str(o.label, 512);
|
|
1138
1148
|
if (label === void 0) continue;
|
|
1139
1149
|
out.push({
|
|
@@ -1200,7 +1210,7 @@ function sanitizeEChartOption(v, depth, budget) {
|
|
|
1200
1210
|
}
|
|
1201
1211
|
return arr.length > 0 ? arr : void 0;
|
|
1202
1212
|
}
|
|
1203
|
-
const o = obj(v);
|
|
1213
|
+
const o = obj$1(v);
|
|
1204
1214
|
if (o === void 0) return void 0;
|
|
1205
1215
|
const out = {};
|
|
1206
1216
|
for (const [key, val] of Object.entries(o)) {
|
|
@@ -1220,7 +1230,7 @@ function sanitizeEChartOption(v, depth, budget) {
|
|
|
1220
1230
|
* no-op.
|
|
1221
1231
|
*/
|
|
1222
1232
|
function repairGenuiSpec(value) {
|
|
1223
|
-
const v = obj(value);
|
|
1233
|
+
const v = obj$1(value);
|
|
1224
1234
|
if (v === void 0) return null;
|
|
1225
1235
|
if (!Array.isArray(v.items)) {
|
|
1226
1236
|
const wrapped = wrapSingleComponentRoot(value);
|
|
@@ -1252,28 +1262,28 @@ function countGenuiNodes(value, cap = Number.POSITIVE_INFINITY) {
|
|
|
1252
1262
|
for (const item of list) {
|
|
1253
1263
|
if (count >= cap) return;
|
|
1254
1264
|
count += 1;
|
|
1255
|
-
const v = obj(item);
|
|
1265
|
+
const v = obj$1(item);
|
|
1256
1266
|
if (v === void 0) continue;
|
|
1257
1267
|
if (v.type === "tabs" && Array.isArray(v.tabs)) for (const t of v.tabs) {
|
|
1258
1268
|
if (count >= cap) return;
|
|
1259
|
-
const to = obj(t);
|
|
1269
|
+
const to = obj$1(t);
|
|
1260
1270
|
if (to !== void 0) walk(to.items);
|
|
1261
1271
|
}
|
|
1262
1272
|
else if (v.type === "accordion" && Array.isArray(v.items)) for (const it of v.items) {
|
|
1263
1273
|
if (count >= cap) return;
|
|
1264
|
-
const io = obj(it);
|
|
1274
|
+
const io = obj$1(it);
|
|
1265
1275
|
if (io !== void 0) walk(io.items);
|
|
1266
1276
|
}
|
|
1267
1277
|
else if ((v.type === "row" || v.type === "col" || v.type === "grid" || v.type === "card") && Array.isArray(v.items)) walk(v.items);
|
|
1268
1278
|
else if (v.type === "file-tree" && Array.isArray(v.items)) walk(v.items);
|
|
1269
1279
|
else if (v.type === "list" && Array.isArray(v.items)) for (const li of v.items) {
|
|
1270
1280
|
if (count >= cap) return;
|
|
1271
|
-
const lo = obj(li);
|
|
1281
|
+
const lo = obj$1(li);
|
|
1272
1282
|
if (lo !== void 0 && typeof lo.type === "string") walk([lo]);
|
|
1273
1283
|
}
|
|
1274
1284
|
}
|
|
1275
1285
|
};
|
|
1276
|
-
const root = obj(value);
|
|
1286
|
+
const root = obj$1(value);
|
|
1277
1287
|
walk(root === void 0 ? [] : root.items);
|
|
1278
1288
|
return count;
|
|
1279
1289
|
}
|
|
@@ -1298,6 +1308,7 @@ const GENUI_NODE_TYPES = /* @__PURE__ */ new Set([
|
|
|
1298
1308
|
"divider",
|
|
1299
1309
|
"file-tree",
|
|
1300
1310
|
"grid",
|
|
1311
|
+
"image",
|
|
1301
1312
|
"input",
|
|
1302
1313
|
"json",
|
|
1303
1314
|
"keyvalue",
|
|
@@ -1327,47 +1338,108 @@ const GENUI_NODE_TYPES = /* @__PURE__ */ new Set([
|
|
|
1327
1338
|
"diagram"
|
|
1328
1339
|
]);
|
|
1329
1340
|
/**
|
|
1330
|
-
*
|
|
1341
|
+
* Visit and count declared nodes in a raw spec tree: objects whose `type` is a
|
|
1331
1342
|
* white-listed string, descending the same containers `countGenuiNodes`
|
|
1332
|
-
* walks.
|
|
1333
|
-
*
|
|
1334
|
-
* #42) instead of reporting a green check on a half-empty tree.
|
|
1343
|
+
* walks. Callers can inspect field semantics or compare the count with the
|
|
1344
|
+
* repaired tree without maintaining another traversal.
|
|
1335
1345
|
*/
|
|
1336
|
-
function
|
|
1346
|
+
function visitDeclaredGenuiNodes(value, cap, visit) {
|
|
1337
1347
|
let count = 0;
|
|
1338
1348
|
const declared = (candidate) => {
|
|
1339
|
-
const o = obj(candidate);
|
|
1349
|
+
const o = obj$1(candidate);
|
|
1340
1350
|
return o !== void 0 && typeof o.type === "string" && GENUI_NODE_TYPES.has(o.type);
|
|
1341
1351
|
};
|
|
1342
|
-
|
|
1352
|
+
function walk(list, path) {
|
|
1343
1353
|
if (!Array.isArray(list)) return;
|
|
1344
|
-
for (
|
|
1354
|
+
for (let index = 0; index < list.length; index++) {
|
|
1355
|
+
walkNode(list[index], `${path}[${index}]`);
|
|
1345
1356
|
if (count >= cap) return;
|
|
1346
|
-
if (!declared(item)) continue;
|
|
1347
|
-
count += 1;
|
|
1348
|
-
const v = obj(item);
|
|
1349
|
-
if (v === void 0) continue;
|
|
1350
|
-
if (v.type === "tabs" && Array.isArray(v.tabs)) for (const t of v.tabs) walkItemsOf(t);
|
|
1351
|
-
else if (v.type === "accordion" && Array.isArray(v.items)) for (const it of v.items) walkItemsOf(it);
|
|
1352
|
-
else if ((v.type === "row" || v.type === "col" || v.type === "grid" || v.type === "card") && Array.isArray(v.items)) walk(v.items);
|
|
1353
|
-
else if (v.type === "list" && Array.isArray(v.items)) {
|
|
1354
|
-
for (const li of v.items) if (declared(li)) walk([li]);
|
|
1355
|
-
}
|
|
1356
1357
|
}
|
|
1357
|
-
}
|
|
1358
|
-
|
|
1359
|
-
|
|
1358
|
+
}
|
|
1359
|
+
function walkNode(item, at) {
|
|
1360
|
+
if (count >= cap || !declared(item)) return;
|
|
1361
|
+
const v = obj$1(item);
|
|
1362
|
+
if (v === void 0) return;
|
|
1363
|
+
count += 1;
|
|
1364
|
+
visit(v, at);
|
|
1365
|
+
if (v.type === "tabs" && Array.isArray(v.tabs)) for (let tab = 0; tab < v.tabs.length; tab++) walkItemsOf(v.tabs[tab], `${at}.tabs[${tab}]`);
|
|
1366
|
+
else if (v.type === "accordion" && Array.isArray(v.items)) for (let row = 0; row < v.items.length; row++) walkItemsOf(v.items[row], `${at}.items[${row}]`);
|
|
1367
|
+
else if ((v.type === "row" || v.type === "col" || v.type === "grid" || v.type === "card") && Array.isArray(v.items)) walk(v.items, `${at}.items`);
|
|
1368
|
+
else if (v.type === "list" && Array.isArray(v.items)) for (let row = 0; row < v.items.length; row++) walkNode(v.items[row], `${at}.items[${row}]`);
|
|
1369
|
+
}
|
|
1370
|
+
function walkItemsOf(holder, path) {
|
|
1371
|
+
const o = obj$1(holder);
|
|
1360
1372
|
if (o === void 0) return;
|
|
1361
1373
|
const items = o.items !== void 0 ? o.items : o.content;
|
|
1362
|
-
if (Array.isArray(items)) walk(items);
|
|
1363
|
-
else
|
|
1364
|
-
}
|
|
1365
|
-
const root = obj(value);
|
|
1374
|
+
if (Array.isArray(items)) walk(items, `${path}.items`);
|
|
1375
|
+
else walkNode(items, `${path}.items`);
|
|
1376
|
+
}
|
|
1377
|
+
const root = obj$1(value);
|
|
1366
1378
|
if (root === void 0) return count;
|
|
1367
|
-
if (!Array.isArray(root.items) && declared(value))
|
|
1368
|
-
else walk(root.items);
|
|
1379
|
+
if (!Array.isArray(root.items) && declared(value)) walkNode(value, "spec");
|
|
1380
|
+
else walk(root.items, "items");
|
|
1369
1381
|
return count;
|
|
1370
1382
|
}
|
|
1383
|
+
/**
|
|
1384
|
+
* Count white-listed nodes declared by a raw spec before repair drops invalid entries.
|
|
1385
|
+
* @param value - raw GenUI spec.
|
|
1386
|
+
* @param cap - traversal ceiling.
|
|
1387
|
+
* @returns declared node count up to the ceiling.
|
|
1388
|
+
*/
|
|
1389
|
+
function countDeclaredGenuiNodes(value, cap = Number.POSITIVE_INFINITY) {
|
|
1390
|
+
return visitDeclaredGenuiNodes(value, cap, () => {});
|
|
1391
|
+
}
|
|
1392
|
+
/**
|
|
1393
|
+
* Return field-level chart errors without changing other repairable component families.
|
|
1394
|
+
* @param value - raw GenUI spec.
|
|
1395
|
+
* @returns chart semantic errors in tree order.
|
|
1396
|
+
*/
|
|
1397
|
+
function validateGenuiChartSemantics(value) {
|
|
1398
|
+
const errors = [];
|
|
1399
|
+
visitDeclaredGenuiNodes(value, GENUI_LIMITS.maxNodes + 1, (node, at) => {
|
|
1400
|
+
if (node.type !== "chart") return;
|
|
1401
|
+
validateChartNode(node, at, errors);
|
|
1402
|
+
});
|
|
1403
|
+
return errors;
|
|
1404
|
+
}
|
|
1405
|
+
function validateChartData(value, at, errors) {
|
|
1406
|
+
if (!Array.isArray(value)) return;
|
|
1407
|
+
for (let index = 0; index < value.length; index++) {
|
|
1408
|
+
const datum = obj$1(value[index]);
|
|
1409
|
+
const path = `${at}[${index}]`;
|
|
1410
|
+
if (datum === void 0) {
|
|
1411
|
+
errors.push(`${path} must be an object`);
|
|
1412
|
+
continue;
|
|
1413
|
+
}
|
|
1414
|
+
if (typeof datum.label !== "string") errors.push(`${path}.label must be a string`);
|
|
1415
|
+
if (typeof datum.value !== "number" || !Number.isFinite(datum.value)) errors.push(`${path}.value must be a finite number`);
|
|
1416
|
+
if (datum.color !== void 0 && typeof datum.color !== "string") errors.push(`${path}.color must be a string`);
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
function validateChartSeries(value, at, errors) {
|
|
1420
|
+
if (!Array.isArray(value)) return;
|
|
1421
|
+
for (let index = 0; index < value.length; index++) {
|
|
1422
|
+
const series = obj$1(value[index]);
|
|
1423
|
+
const path = `${at}[${index}]`;
|
|
1424
|
+
if (series === void 0) {
|
|
1425
|
+
errors.push(`${path} must be an object`);
|
|
1426
|
+
continue;
|
|
1427
|
+
}
|
|
1428
|
+
if (typeof series.label !== "string") errors.push(`${path}.label must be a string`);
|
|
1429
|
+
if (series.color !== void 0 && typeof series.color !== "string") errors.push(`${path}.color must be a string`);
|
|
1430
|
+
if (!Array.isArray(series.data)) errors.push(`${path}.data must be an array`);
|
|
1431
|
+
validateChartData(series.data, `${path}.data`, errors);
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
function validateChartNode(v, at, errors) {
|
|
1435
|
+
if (!Array.isArray(v.data) && !Array.isArray(v.series)) errors.push(`${at}: type 'chart' requires data or series (array)`);
|
|
1436
|
+
if (v.variant !== void 0) errors.push(`${at}.variant is unsupported; use kind`);
|
|
1437
|
+
if (v.data !== void 0 && !Array.isArray(v.data)) errors.push(`${at}.data must be an array`);
|
|
1438
|
+
if (v.series !== void 0 && !Array.isArray(v.series)) errors.push(`${at}.series must be an array`);
|
|
1439
|
+
if (v.kind !== void 0 && (typeof v.kind !== "string" || !CHART_KINDS.includes(v.kind))) errors.push(`${at}.kind must be bars, line, or donut`);
|
|
1440
|
+
validateChartData(v.data, `${at}.data`, errors);
|
|
1441
|
+
validateChartSeries(v.series, `${at}.series`, errors);
|
|
1442
|
+
}
|
|
1371
1443
|
//#endregion
|
|
1372
1444
|
//#region src/shared/fence-repair.ts
|
|
1373
1445
|
/**
|
|
@@ -1478,6 +1550,83 @@ function completeFenceJson(raw) {
|
|
|
1478
1550
|
}
|
|
1479
1551
|
}
|
|
1480
1552
|
//#endregion
|
|
1553
|
+
//#region src/plugin/chart-contract.ts
|
|
1554
|
+
/** Plain object helper for raw model-authored specs. */
|
|
1555
|
+
function obj(value) {
|
|
1556
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
1557
|
+
}
|
|
1558
|
+
/**
|
|
1559
|
+
* Visit chart nodes through the component-bearing containers understood by
|
|
1560
|
+
* the GenUI guard. This deliberately does not recursively inspect arbitrary
|
|
1561
|
+
* objects (for example `json.value` or `echart.option`), where a nested
|
|
1562
|
+
* `{type:"chart"}` is data rather than a GenUI node.
|
|
1563
|
+
*/
|
|
1564
|
+
function visitChartNodes(value, visit) {
|
|
1565
|
+
function walkNode(value, at) {
|
|
1566
|
+
const node = obj(value);
|
|
1567
|
+
if (node === void 0 || typeof node.type !== "string") return;
|
|
1568
|
+
if (node.type === "chart") visit(node, at);
|
|
1569
|
+
if ((node.type === "row" || node.type === "col" || node.type === "grid" || node.type === "card") && Array.isArray(node.items)) {
|
|
1570
|
+
walk(node.items, `${at}.items`);
|
|
1571
|
+
return;
|
|
1572
|
+
}
|
|
1573
|
+
if (node.type === "tabs" && Array.isArray(node.tabs)) {
|
|
1574
|
+
for (let index = 0; index < node.tabs.length; index++) {
|
|
1575
|
+
const tab = obj(node.tabs[index]);
|
|
1576
|
+
if (tab === void 0) continue;
|
|
1577
|
+
const items = tab.items !== void 0 ? tab.items : tab.content;
|
|
1578
|
+
if (Array.isArray(items)) walk(items, `${at}.tabs[${index}].items`);
|
|
1579
|
+
else walkNode(items, `${at}.tabs[${index}].items`);
|
|
1580
|
+
}
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1583
|
+
if (node.type === "accordion" && Array.isArray(node.items)) {
|
|
1584
|
+
for (let index = 0; index < node.items.length; index++) {
|
|
1585
|
+
const item = obj(node.items[index]);
|
|
1586
|
+
if (item === void 0) continue;
|
|
1587
|
+
const items = item.items !== void 0 ? item.items : item.content;
|
|
1588
|
+
if (Array.isArray(items)) walk(items, `${at}.items[${index}].items`);
|
|
1589
|
+
else walkNode(items, `${at}.items[${index}].items`);
|
|
1590
|
+
}
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
if (node.type === "list" && Array.isArray(node.items)) for (let index = 0; index < node.items.length; index++) walkNode(node.items[index], `${at}.items[${index}]`);
|
|
1594
|
+
}
|
|
1595
|
+
function walk(list, path) {
|
|
1596
|
+
if (!Array.isArray(list)) return;
|
|
1597
|
+
for (let index = 0; index < list.length; index++) walkNode(list[index], `${path}[${index}]`);
|
|
1598
|
+
}
|
|
1599
|
+
const root = obj(value);
|
|
1600
|
+
if (root === void 0) return;
|
|
1601
|
+
if (!Array.isArray(root.items) && root.type === "chart") walkNode(root, "spec");
|
|
1602
|
+
else walk(root.items, "items");
|
|
1603
|
+
}
|
|
1604
|
+
/**
|
|
1605
|
+
* Validate the native chart contract all the way to something the renderer
|
|
1606
|
+
* can actually draw. The shared guard still owns field/type validation; this
|
|
1607
|
+
* layer adds renderer-specific invariants that would otherwise produce an
|
|
1608
|
+
* empty line/donut chart or a grouped-bars shell with no points.
|
|
1609
|
+
*/
|
|
1610
|
+
function validateRenderableChartSemantics(value) {
|
|
1611
|
+
const errors = validateGenuiChartSemantics(value);
|
|
1612
|
+
visitChartNodes(value, (node, at) => {
|
|
1613
|
+
const kind = node.kind === void 0 ? "bars" : node.kind;
|
|
1614
|
+
if (Array.isArray(node.data) && node.data.length === 0) errors.push(`${at}.data must not be empty`);
|
|
1615
|
+
if (Array.isArray(node.series)) {
|
|
1616
|
+
if (node.series.length === 0) errors.push(`${at}.series must not be empty`);
|
|
1617
|
+
for (let index = 0; index < node.series.length; index++) {
|
|
1618
|
+
const series = obj(node.series[index]);
|
|
1619
|
+
if (series !== void 0 && Array.isArray(series.data) && series.data.length === 0) errors.push(`${at}.series[${index}].data must not be empty`);
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
if (kind === "line" || kind === "donut") {
|
|
1623
|
+
if (node.series !== void 0) errors.push(`${at}.series is only supported for bars`);
|
|
1624
|
+
if (node.data === void 0) errors.push(`${at}.data is required for ${kind}`);
|
|
1625
|
+
}
|
|
1626
|
+
});
|
|
1627
|
+
return errors;
|
|
1628
|
+
}
|
|
1629
|
+
//#endregion
|
|
1481
1630
|
//#region src/plugin/tool.ts
|
|
1482
1631
|
/**
|
|
1483
1632
|
* Arguments schema: an open `spec` slot. The schema must NOT reject anything
|
|
@@ -1616,7 +1765,10 @@ function createRenderUiTool() {
|
|
|
1616
1765
|
}
|
|
1617
1766
|
},
|
|
1618
1767
|
async execute(args) {
|
|
1619
|
-
const
|
|
1768
|
+
const input = specOf(args);
|
|
1769
|
+
const chartErrors = validateRenderableChartSemantics(input);
|
|
1770
|
+
if (chartErrors.length > 0) throw new Error("render_ui spec invalid: " + chartErrors.join("; "));
|
|
1771
|
+
const spec = repairGenuiSpec(input);
|
|
1620
1772
|
if (spec === null) return "render_ui:spec 无效 —— 根对象需要 \"items\" 数组(组件树白名单见系统提示词),请修正后重试。";
|
|
1621
1773
|
return `已渲染 UI「${spec.title ?? "未命名"}」(${countNodes(spec)} 个组件)。用户现在可以看到这张卡片;组件带 action 时,用户交互会以 [genui-action] 消息发回给你,届时请重新渲染更新后的界面。`;
|
|
1622
1774
|
},
|
|
@@ -1719,6 +1871,11 @@ function bracketDiagnostic(raw) {
|
|
|
1719
1871
|
return diffs.length === 0 ? "" : ` 括号计数:${diffs.join(";")}(长表格最易在收尾处错位,如把 ]]}]} 写成 ]}]}]})\n`;
|
|
1720
1872
|
}
|
|
1721
1873
|
const COMMON_CAUSES = "常见原因:① 收尾括号错位/缺失({ 与 }、[ 与 ] 数量不相等)② 字符串值内用了半角引号 \"(中文引语请用 “” 或 「」)③ 尾随逗号 ④ 字符串未闭合";
|
|
1874
|
+
/** Return the model-facing chart field failure for one parsed fence body. */
|
|
1875
|
+
function chartValidationFailure(value) {
|
|
1876
|
+
const errors = validateRenderableChartSemantics(value);
|
|
1877
|
+
return errors.length === 0 ? void 0 : `❌ chart 字段验证失败:\n- ${errors.join("\n- ")}`;
|
|
1878
|
+
}
|
|
1722
1879
|
/** Build the validate_dsh_ui tool definition (registered alongside render_ui). */
|
|
1723
1880
|
function createValidateDshUiTool() {
|
|
1724
1881
|
return {
|
|
@@ -1746,9 +1903,16 @@ function createValidateDshUiTool() {
|
|
|
1746
1903
|
} catch (error) {
|
|
1747
1904
|
const detail = error instanceof Error ? error.message : String(error);
|
|
1748
1905
|
const repaired = completeFenceJson(raw);
|
|
1749
|
-
if (repaired !== null
|
|
1906
|
+
if (repaired !== null) {
|
|
1907
|
+
const repairedValue = JSON.parse(repaired.text);
|
|
1908
|
+
const chartFailure = chartValidationFailure(repairedValue);
|
|
1909
|
+
if (chartFailure !== void 0) return chartFailure;
|
|
1910
|
+
if (repairGenuiSpec(repairedValue) !== null) return `❌ dsh-ui 围栏 JSON 解析失败:${detail}。\n${bracketDiagnostic(raw)} 已自动修复 ${repaired.repairs} 处,下面是修复后的 JSON,直接作为围栏正文发出即可(无需再验证):\n\`\`\`\n${repaired.text}\n\`\`\``;
|
|
1911
|
+
}
|
|
1750
1912
|
return `❌ dsh-ui 围栏 JSON 解析失败:${detail}。\n${bracketDiagnostic(raw)} 自动修复未能恢复(结构损坏),请按错误信息修正后重新调用本工具验证,通过后再发出围栏。\n${COMMON_CAUSES}`;
|
|
1751
1913
|
}
|
|
1914
|
+
const chartFailure = chartValidationFailure(parsed);
|
|
1915
|
+
if (chartFailure !== void 0) return chartFailure;
|
|
1752
1916
|
const spec = repairGenuiSpec(parsed);
|
|
1753
1917
|
if (spec === null) return "❌ 不是合法 GenUI spec:根对象需要 \"items\" 数组,且每个节点 type 必须在白名单内(见系统提示词)。请修正后重新验证。";
|
|
1754
1918
|
const validCount = countNodes(spec);
|
|
@@ -1843,7 +2007,7 @@ The spec is a white-listed component tree rendered inline where the fence sits.
|
|
|
1843
2007
|
|
|
1844
2008
|
- 布局: text · row · col · grid · card · divider · spacer
|
|
1845
2009
|
- 展示: badge · stat · progress · list · table · keyvalue · avatar · audio · video · timeline · file-tree · breadcrumb · callout · steps · diff · json · code · copy
|
|
1846
|
-
- 图表: chart
|
|
2010
|
+
- 图表: chart {"type":"chart","kind":"bars|line|donut","data":[{"label":"...","value":n,"color":"#hex?"}],"series":[...]?}(series 仅 bars;未知扩展字段可通过校验,但原生 chart repair/render 会忽略) · echart (preset|option) · plot (函数图)
|
|
1847
2011
|
- 交互: button · input · textarea · select · checkbox · switch · slider · radio · submit · quiz · link · tabs · accordion
|
|
1848
2012
|
- 高级: mermaid (flowchart/sequence/class/gantt/pie/er/state/journey) · diagram (编辑级架构/流程图,27 种 kind) · scene3d (3D WebGL)
|
|
1849
2013
|
|
|
@@ -1862,6 +2026,51 @@ Rules:
|
|
|
1862
2026
|
* @param ctx - cordis context.
|
|
1863
2027
|
*/
|
|
1864
2028
|
const inject = ["systemPrompt"];
|
|
2029
|
+
const BUNDLED_SKILL_RANK = 600;
|
|
2030
|
+
const BUNDLED_SKILL_PROVIDER = "dsh-genui";
|
|
2031
|
+
const BUNDLED_SKILL_DESCRIPTION = "GenUI 完整组件与字段规范,用于生成 dsh-ui 结构化交互界面。";
|
|
2032
|
+
const BUNDLED_SKILL_INVOCATION = {
|
|
2033
|
+
modelInvocable: true,
|
|
2034
|
+
userInvocable: true
|
|
2035
|
+
};
|
|
2036
|
+
/** Register through the provider path so source=bundled also gets bundled precedence. */
|
|
2037
|
+
function bundledSkillProvider() {
|
|
2038
|
+
const moduleDirectory = dirname(fileURLToPath(new URL(import.meta.url)));
|
|
2039
|
+
const path = basename(moduleDirectory) === "plugin" ? resolve(moduleDirectory, "../../SKILL.md") : resolve(moduleDirectory, "../SKILL.md");
|
|
2040
|
+
const raw = readFileSync(path, "utf8");
|
|
2041
|
+
const end = raw.indexOf("\n---\n", 4);
|
|
2042
|
+
if (!raw.startsWith("---\n") || end < 0) throw new Error("genui SKILL.md has invalid frontmatter");
|
|
2043
|
+
return {
|
|
2044
|
+
name: BUNDLED_SKILL_PROVIDER,
|
|
2045
|
+
list: () => Promise.resolve([{
|
|
2046
|
+
name: "genui",
|
|
2047
|
+
description: BUNDLED_SKILL_DESCRIPTION,
|
|
2048
|
+
invocation: BUNDLED_SKILL_INVOCATION,
|
|
2049
|
+
source: "bundled",
|
|
2050
|
+
provider: BUNDLED_SKILL_PROVIDER,
|
|
2051
|
+
path,
|
|
2052
|
+
resourceBase: {
|
|
2053
|
+
kind: "directory",
|
|
2054
|
+
path: dirname(path)
|
|
2055
|
+
},
|
|
2056
|
+
rank: BUNDLED_SKILL_RANK,
|
|
2057
|
+
locator: path
|
|
2058
|
+
}]),
|
|
2059
|
+
get: () => Promise.resolve({
|
|
2060
|
+
name: "genui",
|
|
2061
|
+
description: BUNDLED_SKILL_DESCRIPTION,
|
|
2062
|
+
invocation: BUNDLED_SKILL_INVOCATION,
|
|
2063
|
+
source: "bundled",
|
|
2064
|
+
provider: BUNDLED_SKILL_PROVIDER,
|
|
2065
|
+
path,
|
|
2066
|
+
resourceBase: {
|
|
2067
|
+
kind: "directory",
|
|
2068
|
+
path: dirname(path)
|
|
2069
|
+
},
|
|
2070
|
+
content: raw.slice(end + 5)
|
|
2071
|
+
})
|
|
2072
|
+
};
|
|
2073
|
+
}
|
|
1865
2074
|
function apply(ctx) {
|
|
1866
2075
|
ctx.systemPrompt.section({
|
|
1867
2076
|
name: "genui:fence",
|
|
@@ -1881,6 +2090,9 @@ function apply(ctx) {
|
|
|
1881
2090
|
ctx.on("internal/service", (name, value) => {
|
|
1882
2091
|
if (name === "tools") tryRegister(value);
|
|
1883
2092
|
});
|
|
2093
|
+
ctx.inject(["skills"], (skillCtx) => {
|
|
2094
|
+
skillCtx.skills.registerProvider(() => bundledSkillProvider());
|
|
2095
|
+
});
|
|
1884
2096
|
let assetsRegistered = false;
|
|
1885
2097
|
const tryRegisterAssets = (value) => {
|
|
1886
2098
|
if (assetsRegistered) return;
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { GenuiBlockProps } from './blocks/state.ts';
|
|
2
2
|
export declare const GENUI_ACTION_DEBOUNCE_MS = 300;
|
|
3
3
|
/**
|
|
4
|
-
* Render a GenUI spec as an inline block.
|
|
5
|
-
*
|
|
4
|
+
* Render a GenUI spec as an inline block. `stateKey` is also the durable
|
|
5
|
+
* component identity: changing it remounts the stateful implementation so
|
|
6
|
+
* state loaded for one block can never leak into or be saved under another
|
|
7
|
+
* key. Identity-less streaming renders deliberately share one stable
|
|
8
|
+
* volatile instance, preserving local interaction state as the spec grows.
|
|
6
9
|
*/
|
|
7
10
|
export declare const GenuiBlock: import("react").NamedExoticComponent<GenuiBlockProps>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface TemplateDrawerProps {
|
|
2
|
+
/** 试用:把模板指令插入当前输入框草稿。 */
|
|
3
|
+
onUse: (instruction: string) => void;
|
|
4
|
+
/** 当前面板(模板中心/成就页),由面板 header 按钮控制。 */
|
|
5
|
+
tab: 'templates' | 'achievements';
|
|
6
|
+
}
|
|
7
|
+
export declare function TemplateDrawer({ onUse, tab }: TemplateDrawerProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* achievement-store.ts — 成就状态存储与埋点入口(模块级单例)。
|
|
3
|
+
*
|
|
4
|
+
* 只存计数与解锁时间戳;绝不含消息/内容。localStorage 持久化,
|
|
5
|
+
* 订阅接口供 toast 与成就页消费。埋点入口:
|
|
6
|
+
* - recordFence(spec):一次「内容不重复」的界面渲染(按 spec 指纹去重,
|
|
7
|
+
* LRU 指纹表跨刷新防重放重复计数)
|
|
8
|
+
* - recordPanel():面板 dock 出现
|
|
9
|
+
* - recordInteraction():组件动作回传(去抖后)
|
|
10
|
+
* - recordTemplateUse():模板中心试用
|
|
11
|
+
*/
|
|
12
|
+
import type { GenuiSpec } from './spec.ts';
|
|
13
|
+
import { type AchieveState, type AchievementDef } from './achievements.ts';
|
|
14
|
+
/** 记录一次「内容不重复」的 fence 渲染。 */
|
|
15
|
+
export declare function recordFence(spec: GenuiSpec): void;
|
|
16
|
+
/** 记录一次面板 dock 出现(仅会话从无到有时)。 */
|
|
17
|
+
export declare function recordPanel(): void;
|
|
18
|
+
/** 记录一次组件交互动作回传。 */
|
|
19
|
+
export declare function recordInteraction(): void;
|
|
20
|
+
/** 记录一次模板试用。 */
|
|
21
|
+
export declare function recordTemplateUse(): void;
|
|
22
|
+
/** 当前状态快照(订阅者读取)。 */
|
|
23
|
+
export declare function getAchievementSnapshot(): {
|
|
24
|
+
state: AchieveState;
|
|
25
|
+
unlocked: Record<string, number>;
|
|
26
|
+
};
|
|
27
|
+
/** 取走新解锁(toast 消费;一次性)。 */
|
|
28
|
+
export declare function consumeUnlocks(): AchievementDef[];
|
|
29
|
+
/** 订阅状态变化(toast 与成就页)。 */
|
|
30
|
+
export declare function subscribeAchievements(listener: () => void): () => void;
|
|
31
|
+
/** 成就页 spec 数据源(面板内渲染用)。 */
|
|
32
|
+
export declare function achievementCount(): {
|
|
33
|
+
total: number;
|
|
34
|
+
unlocked: number;
|
|
35
|
+
};
|