@changfenhuang/dsh-genui 0.11.1 → 0.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +4 -0
  3. package/README.zh-CN.md +4 -0
  4. package/SKILL.md +4 -2
  5. package/lib/assets/standalone-runtime.js +1505 -0
  6. package/lib/client.js +163 -25
  7. package/lib/index.js +222 -18
  8. package/lib/types/client/artifact/ExportableGenuiBlock.d.ts +6 -0
  9. package/lib/types/client/artifact/create.d.ts +5 -0
  10. package/lib/types/client/artifact/download.d.ts +9 -0
  11. package/lib/types/client/artifact/html.d.ts +5 -0
  12. package/lib/types/client/artifact/index.d.ts +8 -0
  13. package/lib/types/client/artifact/json.d.ts +3 -0
  14. package/lib/types/client/artifact/parse.d.ts +3 -0
  15. package/lib/types/client/artifact/portability.d.ts +4 -0
  16. package/lib/types/client/artifact/standalone-theme.d.ts +2 -0
  17. package/lib/types/client/artifact/types.d.ts +33 -0
  18. package/lib/types/client/blocks/state.d.ts +2 -0
  19. package/lib/types/client/echarts-engine.d.ts +4 -0
  20. package/lib/types/client/echarts-lazy.d.ts +1 -3
  21. package/lib/types/client/embed.d.ts +2 -0
  22. package/lib/types/client/i18n/en.d.ts +6 -0
  23. package/lib/types/client/primitive-adapter.d.ts +4 -0
  24. package/lib/types/client/primitive-labels.d.ts +1 -1
  25. package/lib/types/client/standalone/asset-loader.d.ts +2 -0
  26. package/lib/types/client/standalone/bootstrap-types.d.ts +9 -0
  27. package/lib/types/client/standalone/katex-style.d.ts +1 -0
  28. package/lib/types/client/standalone/primitive-adapter.d.ts +54 -0
  29. package/lib/types/client/standalone/runtime.d.ts +2 -0
  30. package/lib/types/plugin/fence-feedback.d.ts +4 -1
  31. package/lib/types/plugin/index.d.ts +1 -1
  32. package/lib/types/plugin/inline-content-diagnostic.d.ts +8 -0
  33. package/package.json +3 -2
  34. package/src/client/EChartNode.tsx +4 -8
  35. package/src/client/GenuiBlock.tsx +17 -13
  36. package/src/client/action-context.ts +1 -1
  37. package/src/client/artifact/ArtifactExport.module.css +64 -0
  38. package/src/client/artifact/ExportableGenuiBlock.tsx +100 -0
  39. package/src/client/artifact/create.ts +69 -0
  40. package/src/client/artifact/download.ts +42 -0
  41. package/src/client/artifact/html.ts +157 -0
  42. package/src/client/artifact/index.ts +15 -0
  43. package/src/client/artifact/json.ts +6 -0
  44. package/src/client/artifact/parse.ts +32 -0
  45. package/src/client/artifact/portability.ts +47 -0
  46. package/src/client/artifact/standalone-theme.ts +94 -0
  47. package/src/client/artifact/types.ts +44 -0
  48. package/src/client/asset-loader.ts +8 -0
  49. package/src/client/blocks/advanced.tsx +9 -1
  50. package/src/client/blocks/charts.tsx +1 -1
  51. package/src/client/blocks/forms.tsx +8 -5
  52. package/src/client/blocks/render-node.tsx +1 -1
  53. package/src/client/blocks/state.ts +2 -0
  54. package/src/client/echarts-engine.ts +9 -0
  55. package/src/client/echarts-lazy.ts +1 -4
  56. package/src/client/embed.ts +10 -0
  57. package/src/client/fence-render.tsx +5 -13
  58. package/src/client/genui-runtime/normalize.ts +1 -1
  59. package/src/client/guard.ts +2 -1
  60. package/src/client/i18n/en.ts +8 -0
  61. package/src/client/i18n/zh.ts +8 -0
  62. package/src/client/inline.ts +26 -1
  63. package/src/client/panel.tsx +2 -2
  64. package/src/client/primitive-adapter.ts +13 -0
  65. package/src/client/primitive-labels.ts +1 -1
  66. package/src/client/standalone/asset-loader.ts +9 -0
  67. package/src/client/standalone/bootstrap-types.ts +11 -0
  68. package/src/client/standalone/katex-style.ts +1 -0
  69. package/src/client/standalone/primitive-adapter.tsx +94 -0
  70. package/src/client/standalone/runtime.tsx +27 -0
  71. package/src/client/toolview.tsx +2 -2
  72. package/src/plugin/fence-feedback.ts +25 -9
  73. package/src/plugin/index.ts +4 -3
  74. package/src/plugin/inline-content-diagnostic.ts +178 -0
  75. package/src/plugin/tool.ts +18 -3
  76. package/tsdown.config.ts +82 -1
@@ -13,7 +13,7 @@ import { Context } from '@deepseek-ai/cordis';
13
13
  * Deliberately slim: the `genui` skill carries the full component→field
14
14
  * mapping; this section keeps only the contract that must always be
15
15
  * present (fence syntax, type whitelist, and critical behavioral rules). */
16
- export declare const GENUI_SECTION_TEXT = "You can render interactive UI components INSIDE your reply \u2014 between paragraphs \u2014 by emitting a fenced block with the language tag `dsh-ui` containing a JSON spec:\n\n```dsh-ui\n{\"title\":\"<user-language text>\",\"gap\":14,\"items\":[...]}\n```\n\nAllowed `type` values; the `genui` skill, when available, carries the full content\u2192component mapping and per-component field details:\n\n- \u5E03\u5C40: text \u00B7 row \u00B7 col \u00B7 grid \u00B7 card \u00B7 divider \u00B7 spacer \u00B7 hero\uFF08\u5C01\u9762\u5757\uFF0C\u4E00\u6761\u56DE\u7B54\u6700\u591A\u4E00\u4E2A\uFF09\n- \u5C55\u793A: badge \u00B7 stat \u00B7 progress \u00B7 list \u00B7 table \u00B7 keyvalue \u00B7 timeline \u00B7 file-tree \u00B7 breadcrumb \u00B7 callout \u00B7 steps \u00B7 diff \u00B7 json \u00B7 code \u00B7 copy \u00B7 avatar \u00B7 audio \u00B7 video\n- \u56FE\u8868: chart {\"kind\":\"bars|line|donut\",\"data\":[{\"label\":\"...\",\"value\":n}],\"series\":[{\"label\":\"...\",\"data\":[...]}]?,\"horizontal\":true?,\"stacked\":true?}\uFF08series\uFF1Abars \u5206\u7EC4/\u5806\u53E0 / line \u591A\u5E8F\u5217\uFF1Bhorizontal \u6A2A\u5411\u67F1\uFF09 \u00B7 echart (preset \u540D\u6216 option \u76F4\u901A\uFF0C\u9884\u8BBE\u6E05\u5355\u89C1 skill) \u00B7 plot (\u51FD\u6570\u56FE)\n- \u4EA4\u4E92: button \u00B7 input \u00B7 textarea \u00B7 select \u00B7 checkbox \u00B7 switch \u00B7 slider \u00B7 radio \u00B7 submit \u00B7 quiz \u00B7 link \u00B7 tabs \u00B7 accordion\n- \u9AD8\u7EA7: mermaid (\u6D41\u7A0B\u56FE/\u65F6\u5E8F/\u7518\u7279/ER \u7B49\uFF0C\u5173\u952E\u5B57\u89C1 skill) \u00B7 diagram (\u67B6\u6784/\u6D41\u7A0B\u56FE\uFF0C27 \u79CD kind) \u00B7 scene3d (3D WebGL)\n\n**\u9ED8\u8BA4\u5C31\u8BE5\u51FA UI**\uFF1A\u51FA\u73B0\u4E0B\u5217\u60C5\u51B5\u81F3\u5C11\u51FA\u4E00\u4E2A\u56F4\u680F\uFF1A\n- \u22653 \u6761\u5E76\u5217\u8981\u70B9 \u2192 `list`\uFF1B\u6570\u5B57\u5BF9\u6BD4 \u2192 `table`\uFF1B\u6307\u6807/\u8FDB\u5EA6/\u72B6\u6001 \u2192 `stat`/`progress`/`badge`\n- \u6B65\u9AA4/\u65F6\u95F4\u7EBF \u2192 `steps`/`timeline`/`mermaid`\uFF1B\u67B6\u6784/\u6D41\u7A0B \u2192 `diagram` \u6216 `mermaid`\uFF1B\u98CE\u9669/\u7ED3\u8BBA \u2192 `callout`\uFF1B\u4EE3\u7801/\u6539\u52A8 \u2192 `code`/`diff`/`json`\n- \u884C\u5185\u5BCC\u6587\u672C\uFF1A`text`/`list`/\u8868\u683C\u6587\u672C\u5217/`keyvalue`/`callout` \u91CC\u53EF\u5199 $\u516C\u5F0F$\u3001$$\u5757\u516C\u5F0F$$\u3001`code`\u3001**\u52A0\u7C97**\u3001==\u9AD8\u4EAE==\u3001[\u6587\u5B57](url)\u3002\n- \u9ED8\u8BA4\u65E0\u5361 \u2260 \u5C11\u7528\u7EC4\u4EF6\uFF1A\u786C\u89E6\u53D1\u7167\u5E38\u51FA\u7EC4\u4EF6\uFF0C**\u7EC4\u4EF6\u591A\u4E0D\u662F\u95EE\u9898**\u2014\u2014\u5224\u636E\u662F\u6BCF\u4E2A\u7EC4\u4EF6\u627F\u8F7D\u4E0D\u540C\u4FE1\u606F\u3001\u6709\u7126\u70B9\u4E0E\u5C42\u6B21\u3002\u5361\u7247\u53EA\u7528\u4E8E\u5E76\u6392\u9879\u4E0E\u6570\u636E\u5BF9\u8C61\uFF1B\u5355\u6BB5\u6587\u5B57\u7528\u300C\u6807\u9898 + \u6B63\u6587 + \u95F4\u8DDD\u300D\u3002\n\n**\u53D1\u56DE\u7B54\u524D\u6700\u540E\u81EA\u68C0\u4E00\u6B21**\uFF1A\u8FD9\u6BB5\u5185\u5BB9\u91CC\u6709\u6CA1\u6709 \u22653 \u6761\u5E76\u5217\u8981\u70B9\u3001\u4EFB\u4F55\u5BF9\u6BD4\u3001\u4EFB\u4F55\u6570\u5B57/\u6307\u6807\u3001\u4EFB\u4F55\u6B65\u9AA4\u6216\u6D41\u7A0B\uFF1F\u6709\u5C31\u5148\u8F6C\u6210\u7EC4\u4EF6\u518D\u5F00\u53E3\u3002**\u72B6\u6001\u6C47\u62A5\u3001\u8FDB\u5EA6\u8BF4\u660E\u3001\u63D0\u4EA4\u4E0E\u6539\u52A8\u6E05\u5355\u540C\u6837\u7B97**\u3002\n- \u8D8B\u52BF/\u5360\u6BD4 \u2192 `chart`\uFF08\u22648 \u70B9\uFF09\u6216 `echart`\uFF08\u591A\u5E8F\u5217/\u8981\u4EA4\u4E92\u65F6\uFF09\uFF1B\u914D\u8272\u9ED8\u8BA4\u8DDF\u968F\u4E3B\u9898\uFF0C\u53EA\u6709\u8BED\u4E49\u9700\u8981\u65F6\u624D\u7528 `palette`/`card.accent`\uFF1Bgrid \u5B50\u8282\u70B9\u7528 `\"span\":2` \u8DE8\u5217\u505A\u5BBD\u7A84\u6DF7\u6392\uFF1B\u6570\u636E\u591A\u65F6\u7ED9 `table`/`chart`/`list` \u914D\u4E00\u4E2A `input`(id) + `filter` \u7ED1\u5B9A\uFF0C\u8BFB\u8005\u80FD\u5C31\u5730\u7B5B\u9009\u3002\n\n**\u5B57\u6BB5\u901F\u67E5**\uFF08\u5B8C\u6574\u89C1 genui skill\uFF09\uFF1A`stat` `{\"label\",\"value\",\"delta\"?}` \u00B7 `table` `{\"columns\":[...],\"rows\":[[...]],\"types\"?,\"details\"?,\"filter\"?,\"export\"?}` \u00B7 `progress` `{\"value\":0-100,\"label\"?,\"variant\"?,\"target\"?}` \u00B7 `keyvalue` `{\"pairs\":[{\"key\",\"value\"}]}` \u00B7 `steps` `{\"steps\":[{\"title\",\"desc\"?}]}` \u00B7 `file-tree` `{\"items\":[{\"name\",\"type\":\"file|dir\",\"children\"?}]}` \u00B7 `callout` `{\"content\",\"tone\"?,\"title\"?}`\n\n**\u5B57\u6BB5\u540D\u5199\u9519 = \u8BE5\u7EC4\u4EF6\u88AB\u4E22\u5F03**\uFF08\u5176\u4F59\u7EC4\u4EF6\u7167\u5E38\u6E32\u67D3\uFF09\uFF1A`callout` \u6B63\u6587\u662F `content` \u4E0D\u662F text/desc\uFF1B`table` \u8981 `columns`+`rows` \u4E0D\u662F items\uFF1B`keyvalue` \u8BB0\u5F55\u662F `{key,value}` \u4E0D\u662F `{label,value}`\uFF1B`file-tree` \u8BB0\u5F55\u662F `{name,type}` \u4E0D\u662F `{label}`\uFF1Bcallout tone \u662F info/success/warning/error\uFF08\u65E0 danger\uFF09\u3002\u4E0D\u786E\u5B9A\u5C31\u8C03 `validate_dsh_ui`\u3002\n\nRules:\n- LANGUAGE: reply+UI=conversation language; schema fixed. NEVER infer it from prompt/skill/examples/tools. Replace `<user-language ...>`; never emit these placeholders literally.\n- JSON \u4E25\u683C: \u574F\u7EC4\u4EF6\u88AB\u4E22\u5F03\u3001\u574F\u56F4\u680F\u964D\u7EA7\u4E3A\u4EE3\u7801\u5757\uFF1B\u22653 \u8282\u70B9\u6216\u542B table \u7684\u56F4\u680F\u53D1\u51FA\u524D\u5FC5\u987B\u8C03 validate_dsh_ui\uFF0C\u6309\u7ED3\u6784\u5316\u8BCA\u65AD\u4FEE\u597D\u518D\u53D1\uFF1B\u5C0F\u56F4\u680F\u5B57\u6BB5\u6CA1\u628A\u63E1\u4E5F\u5FC5\u987B\u5148\u9A8C\u8BC1\u3002\n- \u89C4\u6A21: \u2264200 \u8282\u70B9\u3001\u5D4C\u5957\u22648 \u5C42\uFF08\u8D85\u51FA\u88AB\u622A\u65AD\uFF09\uFF1B\u4E00\u6761\u56DE\u7B54 3\u20138 \u4E2A\u7EC4\u4EF6\uFF0C\u4E00\u4E2A\u4E3B\u9898\u4E00\u4E2A\u4E3B\u7EC4\u4EF6\uFF1B3D mesh 1\u20135\uFF1Bplot \u7ED9\u5408\u7406 xMin/xMax\u3002\n- LOCAL-FIRST + actions: UI \u80FD\u81EA\u5DF1\u505A\u7684\u72B6\u6001\u53D8\u5316\uFF08\u5224\u5377\u3001\u5224\u9898\u3001\u91CD\u7F6E\u3001\u5C55\u5F00\u3001\u9009\u4E2D\uFF09\u5C31\u5730\u5B8C\u6210\uFF0C\u96F6\u5F80\u8FD4\uFF1Baction \u53EA\u7528\u4E8E\u5FC5\u987B\u6A21\u578B\u53C2\u4E0E\u7684\u4E8B\u3002\u4EA4\u4E92\u7EC4\u4EF6\u5E26 \"action\":\"name\"\uFF0C\u4EA4\u4E92\u4EE5 [genui-action] name + \u7EC4\u4EF6\u6570\u636E\u56DE\u4F20\uFF0C\u5C4A\u65F6\u91CD\u6E32\u67D3\u66F4\u65B0 UI\uFF1B\u65E0 action \u7684\u6309\u94AE\u7981\u7528\u3002\n- Durable state: \u4EA4\u4E92\u72B6\u6001\u6309\u300C\u4F1A\u8BDD+\u5185\u5BB9\u6307\u7EB9\u300D\u6301\u4E45\u5316\u2014\u2014\u5237\u65B0/\u91CD\u653E\u6062\u590D\uFF1B\u91CD\u6E32\u67D3\u76F8\u540C\u5185\u5BB9\u4FDD\u7559\uFF0C\u65B0\u5185\u5BB9\u91CD\u7F6E\u3002\n- \u5377\u5B50\u6A21\u5F0F: \u6BCF\u9898\u4E00\u4E2A radio\uFF08group+answer+explanation\uFF09+ \u4E00\u4E2A submit\uFF08groups \u5168\u5217\uFF09\uFF0C\u672C\u5730\u5224\u5206\u3002\n- Secrets ban: \u4E0D\u7D22\u53D6\u5BC6\u7801\u3001API Key\u3001Token\u3001\u6062\u590D\u7801\uFF1B\u9700\u8981\u65F6\u62D2\u7EDD\u5E76\u89E3\u91CA\u3002\n- Tool channel: render_ui \u5DE5\u5177\u628A\u540C\u4E00 spec \u6E32\u67D3\u4E3A\u5DE5\u5177\u884C\u5361\u7247\uFF08\u4EA4\u4ED8\u7269\u578B\u754C\u9762\u7528\uFF09\uFF1B\u56F4\u680F\u7528\u4E8E\u56DE\u7B54\u5185\u8054 UI\u3002\n- Panel: \"panel\":true \u53EA\u6E32\u67D3\u8FDB\u4F1A\u8BDD\u9762\u677F dock \u5E76\u539F\u5730\u66F4\u65B0\uFF1B\"append\":true \u8FFD\u52A0\u5408\u5E76\uFF08\u540C\u6807\u7B7E tabs \u8FFD\u52A0/\u65B0\u6807\u7B7E\u52A0\u5165/\u5C3E\u90E8\u8FFD\u52A0\uFF09\uFF1B\u4E0A\u9650 200 \u8282\u70B9/200 \u6B21\u8FFD\u52A0\uFF0C\u6EE1\u4E86\u53D1 replace \u91CD\u5EFA\u3002\u9762\u677F\u7EC4\u4EF6\u6765\u7684 [genui-action] \u53EA\u56DE\u4E00\u4E2A panel:true \u56F4\u680F + \u81F3\u591A\u4E00\u884C 10 \u5B57\u5185\u786E\u8BA4\uFF0C\u4E0D\u89E3\u91CA\u3001\u4E0D\u7528\u666E\u901A\u56F4\u680F\u3002";
16
+ export declare const GENUI_SECTION_TEXT = "You can render interactive UI components INSIDE your reply \u2014 between paragraphs \u2014 by emitting a fenced block with the language tag `dsh-ui` containing a JSON spec:\n\n```dsh-ui\n{\"title\":\"<user-language text>\",\"gap\":14,\"items\":[...]}\n```\n\nAllowed `type` values; the `genui` skill, when available, carries the full content\u2192component mapping and per-component field details:\n\n- \u5E03\u5C40: text \u00B7 row \u00B7 col \u00B7 grid \u00B7 card \u00B7 divider \u00B7 spacer \u00B7 hero\uFF08\u5C01\u9762\u5757\uFF0C\u4E00\u6761\u56DE\u7B54\u6700\u591A\u4E00\u4E2A\uFF09\n- \u5C55\u793A: badge \u00B7 stat \u00B7 progress \u00B7 list \u00B7 table \u00B7 keyvalue \u00B7 timeline \u00B7 file-tree \u00B7 breadcrumb \u00B7 callout \u00B7 steps \u00B7 diff \u00B7 json \u00B7 code \u00B7 copy \u00B7 avatar \u00B7 audio \u00B7 video\n- \u56FE\u8868: chart {\"kind\":\"bars|line|donut\",\"data\":[{\"label\":\"...\",\"value\":n}],\"series\":[{\"label\":\"...\",\"data\":[...]}]?,\"horizontal\":true?,\"stacked\":true?}\uFF08series\uFF1Abars \u5206\u7EC4/\u5806\u53E0 / line \u591A\u5E8F\u5217\uFF1Bhorizontal \u6A2A\u5411\u67F1\uFF09 \u00B7 echart (preset \u540D\u6216 option \u76F4\u901A\uFF0C\u9884\u8BBE\u6E05\u5355\u89C1 skill) \u00B7 plot (\u51FD\u6570\u56FE)\n- \u4EA4\u4E92: button \u00B7 input \u00B7 textarea \u00B7 select \u00B7 checkbox \u00B7 switch \u00B7 slider \u00B7 radio \u00B7 submit \u00B7 quiz \u00B7 link \u00B7 tabs \u00B7 accordion\n- \u9AD8\u7EA7: mermaid (\u6D41\u7A0B\u56FE/\u65F6\u5E8F/\u7518\u7279/ER \u7B49\uFF0C\u5173\u952E\u5B57\u89C1 skill) \u00B7 diagram (\u67B6\u6784/\u6D41\u7A0B\u56FE\uFF0C27 \u79CD kind) \u00B7 scene3d (3D WebGL)\n\n**\u9ED8\u8BA4\u5C31\u8BE5\u51FA UI**\uFF1A\u51FA\u73B0\u4E0B\u5217\u60C5\u51B5\u81F3\u5C11\u51FA\u4E00\u4E2A\u56F4\u680F\uFF1A\n- \u22653 \u6761\u5E76\u5217\u8981\u70B9 \u2192 `list`\uFF1B\u6570\u5B57\u5BF9\u6BD4 \u2192 `table`\uFF1B\u6307\u6807/\u8FDB\u5EA6/\u72B6\u6001 \u2192 `stat`/`progress`/`badge`\n- \u6B65\u9AA4/\u65F6\u95F4\u7EBF \u2192 `steps`/`timeline`/`mermaid`\uFF1B\u67B6\u6784/\u6D41\u7A0B \u2192 `diagram` \u6216 `mermaid`\uFF1B\u98CE\u9669/\u7ED3\u8BBA \u2192 `callout`\uFF1B\u4EE3\u7801/\u6539\u52A8 \u2192 `code`/`diff`/`json`\n- \u884C\u5185\u5BCC\u6587\u672C\uFF1A\u652F\u6301\u516C\u5F0F\u3001`code`\u3001\u52A0\u7C97\u3001\u9AD8\u4EAE\u3001\u94FE\u63A5\uFF1B\u7981\u7528 Markdown table / fenced code\uFF0C\u6539\u7528 table / code / diff / json\u3002\n- \u9ED8\u8BA4\u65E0\u5361\uFF1A\u6309\u89E6\u53D1\u6761\u4EF6\u4F7F\u7528\u7EC4\u4EF6\uFF0C\u6BCF\u4E2A\u7EC4\u4EF6\u627F\u8F7D\u4E0D\u540C\u4FE1\u606F\uFF1B\u5361\u7247\u53EA\u7528\u4E8E\u5E76\u6392\u9879\u4E0E\u6570\u636E\u5BF9\u8C61\uFF0C\u5355\u6BB5\u6587\u5B57\u7528\u6807\u9898\u3001\u6B63\u6587\u4E0E\u95F4\u8DDD\u3002\n\n**\u53D1\u56DE\u7B54\u524D\u6700\u540E\u81EA\u68C0\u4E00\u6B21**\uFF1A\u8FD9\u6BB5\u5185\u5BB9\u91CC\u6709\u6CA1\u6709 \u22653 \u6761\u5E76\u5217\u8981\u70B9\u3001\u4EFB\u4F55\u5BF9\u6BD4\u3001\u4EFB\u4F55\u6570\u5B57/\u6307\u6807\u3001\u4EFB\u4F55\u6B65\u9AA4\u6216\u6D41\u7A0B\uFF1F\u6709\u5C31\u5148\u8F6C\u6210\u7EC4\u4EF6\u518D\u5F00\u53E3\u3002**\u72B6\u6001\u6C47\u62A5\u3001\u8FDB\u5EA6\u8BF4\u660E\u3001\u63D0\u4EA4\u4E0E\u6539\u52A8\u6E05\u5355\u540C\u6837\u7B97**\u3002\n- \u8D8B\u52BF/\u5360\u6BD4 \u2192 `chart`\uFF08\u22648 \u70B9\uFF09\u6216 `echart`\uFF08\u591A\u5E8F\u5217/\u8981\u4EA4\u4E92\u65F6\uFF09\uFF1B\u914D\u8272\u9ED8\u8BA4\u8DDF\u968F\u4E3B\u9898\uFF0C\u53EA\u6709\u8BED\u4E49\u9700\u8981\u65F6\u624D\u7528 `palette`/`card.accent`\uFF1Bgrid \u5B50\u8282\u70B9\u7528 `\"span\":2` \u8DE8\u5217\u505A\u5BBD\u7A84\u6DF7\u6392\uFF1B\u6570\u636E\u591A\u65F6\u7ED9 `table`/`chart`/`list` \u914D\u4E00\u4E2A `input`(id) + `filter` \u7ED1\u5B9A\uFF0C\u8BFB\u8005\u80FD\u5C31\u5730\u7B5B\u9009\u3002\n\n**\u5B57\u6BB5\u901F\u67E5**\uFF08\u5B8C\u6574\u89C1 genui skill\uFF09\uFF1A`stat` `{\"label\",\"value\",\"delta\"?}` \u00B7 `table` `{\"columns\":[...],\"rows\":[[...]],\"types\"?,\"details\"?,\"filter\"?,\"export\"?}` \u00B7 `progress` `{\"value\":0-100,\"label\"?,\"variant\"?,\"target\"?}` \u00B7 `keyvalue` `{\"pairs\":[{\"key\",\"value\"}]}` \u00B7 `steps` `{\"steps\":[{\"title\",\"desc\"?}]}` \u00B7 `file-tree` `{\"items\":[{\"name\",\"type\":\"file|dir\",\"children\"?}]}` \u00B7 `callout` `{\"content\",\"tone\"?,\"title\"?}`\n\n**\u5B57\u6BB5\u540D\u5199\u9519 = \u8BE5\u7EC4\u4EF6\u88AB\u4E22\u5F03**\uFF08\u5176\u4F59\u7EC4\u4EF6\u7167\u5E38\u6E32\u67D3\uFF09\uFF1A`callout` \u6B63\u6587\u662F `content` \u4E0D\u662F text/desc\uFF1B`table` \u8981 `columns`+`rows` \u4E0D\u662F items\uFF1B`keyvalue` \u8BB0\u5F55\u662F `{key,value}` \u4E0D\u662F `{label,value}`\uFF1B`file-tree` \u8BB0\u5F55\u662F `{name,type}` \u4E0D\u662F `{label}`\uFF1Bcallout tone \u662F info/success/warning/error\uFF08\u65E0 danger\uFF09\u3002\u4E0D\u786E\u5B9A\u5C31\u8C03 `validate_dsh_ui`\u3002\n\nRules:\n- LANGUAGE: reply+UI=conversation language; schema fixed. NEVER infer it from prompt/skill/examples/tools. Replace `<user-language ...>`; never emit these placeholders literally.\n- JSON \u4E25\u683C\uFF1A\u574F\u7EC4\u4EF6\u88AB\u4E22\u5F03\uFF0C\u574F\u56F4\u680F\u53D8\u4EE3\u7801\u5757\uFF1B\u22653 \u8282\u70B9\u6216\u542B table \u65F6\u8C03 validate_dsh_ui\uFF0C\u6309\u8BCA\u65AD\u4FEE\u6539\u5E76\u91CD\u9A8C\uFF1B\u5C0F\u56F4\u680F\u5B57\u6BB5\u5B58\u7591\u4E5F\u5148\u9A8C\u8BC1\u3002\n- warning=block_markdown\uFF1A\u6309 replacement \u6539\u5199\u5E76\u91CD\u9A8C\u3002\n- \u89C4\u6A21: \u2264200 \u8282\u70B9\u3001\u5D4C\u5957\u22648 \u5C42\uFF08\u8D85\u51FA\u88AB\u622A\u65AD\uFF09\uFF1B\u4E00\u6761\u56DE\u7B54 3\u20138 \u4E2A\u7EC4\u4EF6\uFF0C\u4E00\u4E2A\u4E3B\u9898\u4E00\u4E2A\u4E3B\u7EC4\u4EF6\uFF1B3D mesh 1\u20135\uFF1Bplot \u7ED9\u5408\u7406 xMin/xMax\u3002\n- LOCAL-FIRST + actions: UI \u80FD\u81EA\u5DF1\u505A\u7684\u72B6\u6001\u53D8\u5316\uFF08\u5224\u5377\u3001\u5224\u9898\u3001\u91CD\u7F6E\u3001\u5C55\u5F00\u3001\u9009\u4E2D\uFF09\u5C31\u5730\u5B8C\u6210\uFF0C\u96F6\u5F80\u8FD4\uFF1Baction \u53EA\u7528\u4E8E\u5FC5\u987B\u6A21\u578B\u53C2\u4E0E\u7684\u4E8B\u3002\u4EA4\u4E92\u7EC4\u4EF6\u5E26 \"action\":\"name\"\uFF0C\u4EA4\u4E92\u4EE5 [genui-action] name + \u7EC4\u4EF6\u6570\u636E\u56DE\u4F20\uFF0C\u5C4A\u65F6\u91CD\u6E32\u67D3\u66F4\u65B0 UI\uFF1B\u65E0 action \u7684\u6309\u94AE\u7981\u7528\u3002\n- Durable state: \u4EA4\u4E92\u72B6\u6001\u6309\u300C\u4F1A\u8BDD+\u5185\u5BB9\u6307\u7EB9\u300D\u6301\u4E45\u5316\u2014\u2014\u5237\u65B0/\u91CD\u653E\u6062\u590D\uFF1B\u91CD\u6E32\u67D3\u76F8\u540C\u5185\u5BB9\u4FDD\u7559\uFF0C\u65B0\u5185\u5BB9\u91CD\u7F6E\u3002\n- \u5377\u5B50\u6A21\u5F0F: \u6BCF\u9898\u4E00\u4E2A radio\uFF08group+answer+explanation\uFF09+ \u4E00\u4E2A submit\uFF08groups \u5168\u5217\uFF09\uFF0C\u672C\u5730\u5224\u5206\u3002\n- Secrets ban: \u4E0D\u7D22\u53D6\u5BC6\u7801\u3001API Key\u3001Token\u3001\u6062\u590D\u7801\uFF1B\u9700\u8981\u65F6\u62D2\u7EDD\u5E76\u89E3\u91CA\u3002\n- Tool channel: render_ui \u5DE5\u5177\u628A\u540C\u4E00 spec \u6E32\u67D3\u4E3A\u5DE5\u5177\u884C\u5361\u7247\uFF08\u4EA4\u4ED8\u7269\u578B\u754C\u9762\u7528\uFF09\uFF1B\u56F4\u680F\u7528\u4E8E\u56DE\u7B54\u5185\u8054 UI\u3002\n- Panel: \"panel\":true \u53EA\u6E32\u67D3\u8FDB\u4F1A\u8BDD\u9762\u677F dock \u5E76\u539F\u5730\u66F4\u65B0\uFF1B\"append\":true \u8FFD\u52A0\u5408\u5E76\uFF08\u540C\u6807\u7B7E tabs \u8FFD\u52A0/\u65B0\u6807\u7B7E\u52A0\u5165/\u5C3E\u90E8\u8FFD\u52A0\uFF09\uFF1B\u4E0A\u9650 200 \u8282\u70B9/200 \u6B21\u8FFD\u52A0\uFF0C\u6EE1\u4E86\u53D1 replace \u91CD\u5EFA\u3002\u9762\u677F\u7EC4\u4EF6\u6765\u7684 [genui-action] \u53EA\u56DE\u4E00\u4E2A panel:true \u56F4\u680F + \u81F3\u591A\u4E00\u884C 10 \u5B57\u5185\u786E\u8BA4\uFF0C\u4E0D\u89E3\u91CA\u3001\u4E0D\u7528\u666E\u901A\u56F4\u680F\u3002";
17
17
  /**
18
18
  * Register the GenUI output-language section and the render_ui tool.
19
19
  * @param ctx - cordis context.
@@ -0,0 +1,8 @@
1
+ import type { GenuiSpec } from '../client/spec.ts';
2
+ export interface InlineContentWarning {
3
+ path: string;
4
+ kind: 'fenced_code' | 'markdown_table';
5
+ replacement: 'code' | 'table';
6
+ }
7
+ /** 按组件结构检查需要块级内容诊断的 canonical 显示字段。 */
8
+ export declare function collectInlineContentWarnings(spec: GenuiSpec): InlineContentWarning[];
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@changfenhuang/dsh-genui",
3
3
  "description": "GenUI for DeepSeek Harness: interactive UI components rendered inline in assistant replies via the ```dsh-ui fence — layout, charts, plots, forms, quizzes, mermaid, 3D scenes, and an action event loop back to the model. Ships the fence-teaching host plugin, the browser renderer (client half), and the genui skill.",
4
- "version": "0.11.1",
4
+ "version": "0.11.2",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/types/plugin/index.d.ts",
@@ -24,6 +24,7 @@
24
24
  "./assets/three": "./src/client/asset-three.ts",
25
25
  "./assets/echarts-core": "./src/client/asset-echarts-core.ts",
26
26
  "./assets/echarts": "./src/client/asset-echarts.ts",
27
+ "./assets/standalone": "./lib/assets/standalone-runtime.js",
27
28
  "./skill": "./SKILL.md"
28
29
  },
29
30
  "files": [
@@ -175,7 +176,7 @@
175
176
  "scripts": {
176
177
  "build": "node scripts/clean.mjs && tsc -p tsconfig.json && tsdown",
177
178
  "test": "vitest run",
178
- "check": "node scripts/clean.mjs && tsc -p tsconfig.json && vitest run && tsdown",
179
+ "check": "node scripts/clean.mjs && tsc -p tsconfig.json && vitest run && tsdown && node scripts/check-standalone.mjs",
179
180
  "check:host-api": "node scripts/check-host-api.mjs",
180
181
  "prepack": "node scripts/prepack.mjs"
181
182
  },
@@ -14,16 +14,11 @@
14
14
  import { renderInline } from './inline.ts'
15
15
  import { useEffect, useRef, useState } from 'react'
16
16
  import css from './GenuiBlock.module.css'
17
- import { CORE_PRESETS, createChart as lazyCreateChart, type EChartsInstance } from './echarts-lazy.ts'
17
+ import { createChart as lazyCreateChart, type EChartsInstance } from './echarts-lazy.ts'
18
+ import { echartEngineFor } from './echarts-engine.ts'
18
19
  import { CHART_COLORS } from './blocks/charts.tsx'
19
20
  import type { GenuiEChart } from './spec.ts'
20
21
 
21
- /** Which engine bundle this node needs (progressive disclosure). */
22
- function neededEngine(node: GenuiEChart): 'core' | 'full' {
23
- if (node.option !== undefined) return 'full'
24
- return CORE_PRESETS.has(node.preset ?? 'bar') ? 'core' : 'full'
25
- }
26
-
27
22
  /**
28
23
  * Categorical fallback palette. The host defines its `--dsw-static-*` tokens on
29
24
  * `body`, not on `:root`, and ECharts renders to CANVAS (so a `var(--x)` string
@@ -401,7 +396,8 @@ export function EChartNode({ node }: { node: GenuiEChart }) {
401
396
  // Full `option` wins over preset shorthand.
402
397
  const option = node.option ?? presetOption(node, el)
403
398
 
404
- void lazyCreateChart(el, option, { height: node.height ?? 300 }, neededEngine(node)).then((inst) => {
399
+ const engine = echartEngineFor(node) === 'echarts-full' ? 'full' : 'core'
400
+ void lazyCreateChart(el, option, { height: node.height ?? 300 }, engine).then((inst) => {
405
401
  if (!alive) {
406
402
  inst.dispose()
407
403
  return
@@ -82,7 +82,7 @@ function specEquivalent(a: GenuiSpec, b: GenuiSpec): boolean {
82
82
 
83
83
  /** Stateful implementation. Streaming state adopts its first durable key
84
84
  * when the reply settles; switching an existing durable key starts fresh. */
85
- function GenuiBlockInstance({ spec, stateKey, animateEntrance = true, initialState, onStateChange }: GenuiBlockProps) {
85
+ function GenuiBlockInstance({ spec, stateKey, animateEntrance = true, initialState, onStateChange, onStateSnapshot }: GenuiBlockProps) {
86
86
  const gap = spec.gap ?? 16
87
87
  const onAction = useDebouncedAction(useGenuiAction())
88
88
  const stateChangeRef = useRef(onStateChange)
@@ -155,6 +155,18 @@ function GenuiBlockInstance({ spec, stateKey, animateEntrance = true, initialSta
155
155
  }),
156
156
  [answers, multiAnswers, fields, secretFields, meta, locked, round, setAnswer, setMultiAnswer, setField, registerSecretField, registerMeta, clear],
157
157
  )
158
+ const durableState = useMemo(() => {
159
+ const safeFields = Object.fromEntries(Object.entries(fields).filter(([id]) => !secretFields.has(id)))
160
+ return {
161
+ answers,
162
+ ...(Object.keys(multiAnswers).length > 0 ? { multiAnswers } : {}),
163
+ locked,
164
+ ...(Object.keys(safeFields).length > 0 ? { fields: safeFields } : {}),
165
+ }
166
+ }, [answers, multiAnswers, locked, fields, secretFields])
167
+ useEffect(() => {
168
+ onStateSnapshot?.(durableState)
169
+ }, [durableState, onStateSnapshot])
158
170
  // Achievement telemetry: every emitted action counts as one interaction
159
171
  // (the debounced emit fires once per real user action).
160
172
  const trackedAction = useMemo(() => {
@@ -167,23 +179,14 @@ function GenuiBlockInstance({ spec, stateKey, animateEntrance = true, initialSta
167
179
  // Durable save (debounced 300ms — typing in a field fires per keystroke).
168
180
  // Secret field values are stripped before writing: passwords never persist.
169
181
  useEffect(() => {
170
- const safeFields = Object.fromEntries(
171
- Object.entries(fields).filter(([id]) => !secretFields.has(id)),
172
- )
173
- const state = {
174
- answers,
175
- ...(Object.keys(multiAnswers).length > 0 ? { multiAnswers } : {}),
176
- locked,
177
- ...(Object.keys(safeFields).length > 0 ? { fields: safeFields } : {}),
178
- }
179
182
  if (savesExternally) {
180
- stateChangeRef.current?.(state)
183
+ stateChangeRef.current?.(durableState)
181
184
  return
182
185
  }
183
186
  if (stateKey === undefined) return
184
- const timer = setTimeout(() => saveBlockState(stateKey, state), 300)
187
+ const timer = setTimeout(() => saveBlockState(stateKey, durableState), 300)
185
188
  return () => clearTimeout(timer)
186
- }, [stateKey, answers, multiAnswers, locked, fields, secretFields, savesExternally])
189
+ }, [stateKey, durableState, savesExternally])
187
190
  // Achievement telemetry (0.9.5): the store dedupes by spec fingerprint, so
188
191
  // streaming re-renders and replays count once per distinct content.
189
192
  useEffect(() => {
@@ -235,4 +238,5 @@ export const GenuiBlock = memo(function GenuiBlock(props: GenuiBlockProps) {
235
238
  return <GenuiBlockInstance key={identity.generation} {...props} />
236
239
  }, (prev, next) => prev.stateKey === next.stateKey
237
240
  && prev.animateEntrance === next.animateEntrance && prev.onStateChange === next.onStateChange
241
+ && prev.onStateSnapshot === next.onStateSnapshot
238
242
  && specEquivalent(prev.spec, next.spec))
@@ -14,7 +14,7 @@
14
14
  */
15
15
  import { createContext, useContext } from 'react'
16
16
  import type { Context } from 'react'
17
- import * as primitives from '@deepseek-ai/dsh-client-ui-primitives'
17
+ import * as primitives from './primitive-adapter.ts'
18
18
 
19
19
  /** v2 action handler: component action + its collected data. */
20
20
  export type GenuiActionHandler = (action: string, payload: Record<string, unknown>) => void
@@ -0,0 +1,64 @@
1
+ .chrome {
2
+ position: relative;
3
+ display: flex;
4
+ align-items: center;
5
+ justify-content: flex-end;
6
+ gap: 8px;
7
+ margin: 8px 0 6px;
8
+ font: 12px/1.4 system-ui, sans-serif;
9
+ }
10
+
11
+ .trigger,
12
+ .item {
13
+ border: 1px solid var(--dsw-alias-border-l1);
14
+ border-radius: 7px;
15
+ padding: 5px 9px;
16
+ background: var(--dsw-alias-bg-layer-1);
17
+ color: var(--dsw-alias-label-secondary);
18
+ cursor: pointer;
19
+ }
20
+
21
+ .trigger:hover,
22
+ .item:hover {
23
+ background: var(--dsw-alias-fill-hover);
24
+ color: var(--dsw-alias-label-primary);
25
+ }
26
+
27
+ .menu {
28
+ position: absolute;
29
+ z-index: 20;
30
+ top: calc(100% + 4px);
31
+ right: 0;
32
+ display: grid;
33
+ min-width: 140px;
34
+ gap: 3px;
35
+ padding: 5px;
36
+ border: 1px solid var(--dsw-alias-border-l1);
37
+ border-radius: 9px;
38
+ background: var(--dsw-alias-bg-layer-1);
39
+ box-shadow: 0 8px 24px #0002;
40
+ }
41
+
42
+ .item {
43
+ border-color: transparent;
44
+ text-align: left;
45
+ }
46
+
47
+ .item:disabled {
48
+ opacity: .5;
49
+ cursor: not-allowed;
50
+ }
51
+
52
+ .menuNotice {
53
+ max-width: 260px;
54
+ padding: 2px 9px 6px;
55
+ color: var(--dsw-alias-state-error-primary, #d94848);
56
+ }
57
+
58
+ .notice {
59
+ color: var(--dsw-alias-label-tertiary);
60
+ }
61
+
62
+ .statusError {
63
+ color: var(--dsw-alias-state-error-primary, #d94848);
64
+ }
@@ -0,0 +1,100 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react'
2
+ import type { BlockInteractionState } from '../interaction-store.ts'
3
+ import { useT } from '../i18n/index.ts'
4
+ import { GenuiBlock } from '../GenuiBlock.tsx'
5
+ import type { GenuiBlockProps } from '../blocks/state.ts'
6
+ import { analyzeGenuiPortability } from './portability.ts'
7
+ import { createGenuiArtifact } from './create.ts'
8
+ import { downloadGenuiArtifactHtml, downloadGenuiArtifactJson } from './download.ts'
9
+ import { GenuiExportError } from './types.ts'
10
+ import css from './ArtifactExport.module.css'
11
+
12
+ interface ArtifactExportMenuProps {
13
+ getArtifact: () => ReturnType<typeof createGenuiArtifact>
14
+ }
15
+
16
+ /** 提供 JSON 和 HTML 的本地化导出菜单。 */
17
+ function ArtifactExportMenu({ getArtifact }: ArtifactExportMenuProps) {
18
+ const t = useT()
19
+ const rootRef = useRef<HTMLDivElement>(null)
20
+ const [open, setOpen] = useState(false)
21
+ const [errorMessage, setErrorMessage] = useState('')
22
+ const report = analyzeGenuiPortability(getArtifact().spec)
23
+
24
+ useEffect(() => {
25
+ if (!open) return
26
+ const closeOutside = (event: PointerEvent): void => {
27
+ if (event.target instanceof Node && !rootRef.current?.contains(event.target)) setOpen(false)
28
+ }
29
+ const closeEscape = (event: KeyboardEvent): void => {
30
+ if (event.key === 'Escape') setOpen(false)
31
+ }
32
+ document.addEventListener('pointerdown', closeOutside)
33
+ document.addEventListener('keydown', closeEscape)
34
+ return () => {
35
+ document.removeEventListener('pointerdown', closeOutside)
36
+ document.removeEventListener('keydown', closeEscape)
37
+ }
38
+ }, [open])
39
+
40
+ /** 执行用户选择的下载并更新辅助说明。 */
41
+ const exportAs = async (format: 'html' | 'json'): Promise<void> => {
42
+ setOpen(false)
43
+ setErrorMessage('')
44
+ try {
45
+ const artifact = getArtifact()
46
+ if (format === 'html') await downloadGenuiArtifactHtml(artifact)
47
+ else downloadGenuiArtifactJson(artifact)
48
+ } catch (error) {
49
+ if (error instanceof GenuiExportError) {
50
+ if (error.code === 'unsupported-custom-component') {
51
+ console.warn('[dsh-genui] standalone export rejected:', error.message)
52
+ setErrorMessage(t('artifact.unsupportedCustom', { types: report.customTypes.join(', ') }))
53
+ } else {
54
+ console.warn('[dsh-genui] artifact export failed:', error.code, error.message)
55
+ setErrorMessage(t('artifact.exportFailed'))
56
+ }
57
+ } else {
58
+ console.warn('[dsh-genui] artifact export failed:', error instanceof Error ? error.message : 'unknown error')
59
+ setErrorMessage(t('artifact.exportFailed'))
60
+ }
61
+ }
62
+ }
63
+
64
+ return (
65
+ <div className={css.chrome} ref={rootRef} data-genui-export>
66
+ {report.externalMedia.length > 0 && <span className={css.notice}>{t('artifact.externalMediaNotice')}</span>}
67
+ {errorMessage !== '' && <span className={css.statusError} aria-live="polite">{errorMessage}</span>}
68
+ <button type="button" className={css.trigger} aria-haspopup="menu" aria-expanded={open} onClick={() => setOpen(value => !value)}>
69
+ {t('artifact.export')}
70
+ </button>
71
+ {open && (
72
+ <div className={css.menu} role="menu">
73
+ <button type="button" role="menuitem" className={css.item} disabled={report.customTypes.length > 0} onClick={() => void exportAs('html')}>{t('artifact.exportHtml')}</button>
74
+ {report.customTypes.length > 0 && <span className={css.menuNotice}>{t('artifact.unsupportedCustom', { types: report.customTypes.join(', ') })}</span>}
75
+ <button type="button" role="menuitem" className={css.item} onClick={() => void exportAs('json')}>{t('artifact.exportJson')}</button>
76
+ </div>
77
+ )}
78
+ </div>
79
+ )
80
+ }
81
+
82
+ export interface ExportableGenuiBlockProps extends GenuiBlockProps {
83
+ exportEnabled?: boolean
84
+ }
85
+
86
+ /** 在完成态 GenUI 外层显示 artifact 导出菜单。 */
87
+ export function ExportableGenuiBlock(props: ExportableGenuiBlockProps) {
88
+ const stateRef = useRef<BlockInteractionState | undefined>(undefined)
89
+ const captureState = useCallback((state: BlockInteractionState) => {
90
+ stateRef.current = state
91
+ }, [])
92
+ const getArtifact = useCallback(() => createGenuiArtifact(props.spec, stateRef.current), [props.spec])
93
+ const { exportEnabled = true, ...blockProps } = props
94
+ return (
95
+ <div data-genui-artifact>
96
+ <GenuiBlock {...blockProps} onStateSnapshot={captureState} />
97
+ {exportEnabled && <ArtifactExportMenu getArtifact={getArtifact} />}
98
+ </div>
99
+ )
100
+ }
@@ -0,0 +1,69 @@
1
+ import { currentLocale } from '../i18n/index.ts'
2
+ import { processGenuiSpec } from '../guard.ts'
3
+ import type { BlockInteractionState } from '../interaction-store.ts'
4
+ import type { GenuiNode, GenuiSpec } from '../spec.ts'
5
+ import { GenuiExportError, GENUI_ARTIFACT_FORMAT, GENUI_ARTIFACT_VERSION, type CreateGenuiArtifactOptions, type GenuiArtifactTheme, type GenuiArtifactV1 } from './types.ts'
6
+
7
+ /** 复制 durable 状态并移除密码字段。 */
8
+ function safeState(state: BlockInteractionState | undefined, secretIds: Set<string>): BlockInteractionState | undefined {
9
+ if (state === undefined) return undefined
10
+ const fields = Object.fromEntries(Object.entries(state.fields ?? {}).filter(([id]) => !secretIds.has(id)))
11
+ return {
12
+ ...(state.answers === undefined ? {} : { answers: { ...state.answers } }),
13
+ ...(state.multiAnswers === undefined ? {} : { multiAnswers: Object.fromEntries(Object.entries(state.multiAnswers).map(([id, values]) => [id, [...values]])) }),
14
+ ...(state.locked === undefined ? {} : { locked: state.locked }),
15
+ ...(Object.keys(fields).length === 0 ? {} : { fields }),
16
+ }
17
+ }
18
+
19
+ /** 复制节点树并递归移除密码默认值。 */
20
+ function sanitizeNodes(items: GenuiNode[], secretIds: Set<string>): GenuiNode[] {
21
+ return items.map(item => {
22
+ const node = JSON.parse(JSON.stringify(item)) as GenuiNode & Record<string, unknown>
23
+ if (node.type === 'input' && node.inputType === 'password') {
24
+ if (typeof node.id === 'string') secretIds.add(node.id)
25
+ delete node.value
26
+ }
27
+ if (Array.isArray(node.items)) {
28
+ if (node.type === 'list') {
29
+ node.items = node.items.map(child => {
30
+ if (typeof child === 'object' && child !== null && 'type' in child) {
31
+ return sanitizeNodes([child as GenuiNode], secretIds)[0]!
32
+ }
33
+ return child
34
+ }) as typeof node.items
35
+ } else {
36
+ node.items = sanitizeNodes(node.items as GenuiNode[], secretIds)
37
+ }
38
+ }
39
+ if (Array.isArray(node.tabs)) {
40
+ node.tabs = node.tabs.map(tab => ({ ...tab, items: sanitizeNodes(tab.items, secretIds) }))
41
+ }
42
+ if (node.type === 'table' && Array.isArray(node.details)) {
43
+ node.details = node.details.map(row => row === null ? null : sanitizeNodes(row, secretIds))
44
+ }
45
+ return node
46
+ })
47
+ }
48
+
49
+ /** 创建规范化 artifact,并清除密码默认值及对应的持久化字段。 */
50
+ export function createGenuiArtifact(
51
+ spec: GenuiSpec,
52
+ state?: BlockInteractionState,
53
+ options: CreateGenuiArtifactOptions = {},
54
+ ): GenuiArtifactV1 {
55
+ const processed = processGenuiSpec(spec)
56
+ if (processed.spec === null) throw new GenuiExportError('artifact-invalid', 'invalid GenUI spec')
57
+ const { panel: _panel, append: _append, ...portableSpec } = processed.spec
58
+ const secretIds = new Set<string>()
59
+ const sanitizedSpec = { ...portableSpec, items: sanitizeNodes(portableSpec.items, secretIds) }
60
+ const theme: GenuiArtifactTheme = options.theme ?? (typeof document !== 'undefined' && document.body.hasAttribute('data-ds-dark-theme') ? 'dark' : 'light')
61
+ const sanitizedState = safeState(state, secretIds)
62
+ return {
63
+ format: GENUI_ARTIFACT_FORMAT,
64
+ version: GENUI_ARTIFACT_VERSION,
65
+ spec: sanitizedSpec,
66
+ ...(sanitizedState === undefined ? {} : { state: sanitizedState }),
67
+ presentation: { locale: options.locale ?? currentLocale(), theme },
68
+ }
69
+ }
@@ -0,0 +1,42 @@
1
+ import { GenuiExportError, type GenuiArtifactV1 } from './types.ts'
2
+ import { serializeGenuiArtifact } from './json.ts'
3
+ import { buildStandaloneHtml } from './html.ts'
4
+
5
+ /** 过滤文件名中的路径符号、控制字符和危险标点。 */
6
+ export function sanitizeArtifactFilename(title: string | undefined, extension: '.html' | '.genui.json'): string {
7
+ const base = (title ?? '').replace(/[\\/:*?"<>|\u0000-\u001f\u007f]/g, '').trim().replace(/\s+/g, ' ').slice(0, 120).replace(/[. ]+$/g, '')
8
+ return `${base || 'genui'}${extension}`
9
+ }
10
+
11
+ /** 触发浏览器下载并释放临时地址。 */
12
+ export function downloadBlob(blob: Blob, filename: string): void {
13
+ const url = URL.createObjectURL(blob)
14
+ const anchor = document.createElement('a')
15
+ anchor.href = url
16
+ anchor.download = filename
17
+ anchor.style.display = 'none'
18
+ document.body.appendChild(anchor)
19
+ anchor.click()
20
+ anchor.remove()
21
+ window.setTimeout(() => URL.revokeObjectURL(url), 0)
22
+ }
23
+
24
+ /** 下载规范化 GenUI JSON artifact。 */
25
+ export function downloadGenuiArtifactJson(artifact: GenuiArtifactV1): void {
26
+ try {
27
+ downloadBlob(new Blob([serializeGenuiArtifact(artifact)], { type: 'application/json;charset=utf-8' }), sanitizeArtifactFilename(artifact.spec.title, '.genui.json'))
28
+ } catch (error) {
29
+ throw new GenuiExportError('download-failed', error instanceof Error ? error.message : 'artifact download failed')
30
+ }
31
+ }
32
+
33
+ /** 生成并下载独立 HTML。 */
34
+ export async function downloadGenuiArtifactHtml(artifact: GenuiArtifactV1): Promise<void> {
35
+ try {
36
+ const html = await buildStandaloneHtml(artifact)
37
+ downloadBlob(new Blob([html], { type: 'text/html;charset=utf-8' }), sanitizeArtifactFilename(artifact.spec.title, '.html'))
38
+ } catch (error) {
39
+ if (error instanceof GenuiExportError) throw error
40
+ throw new GenuiExportError('download-failed', error instanceof Error ? error.message : 'artifact download failed')
41
+ }
42
+ }
@@ -0,0 +1,157 @@
1
+ import { assetUrl } from '../asset-loader.ts'
2
+ import { parseGenuiArtifact } from './parse.ts'
3
+ import { analyzeGenuiPortability } from './portability.ts'
4
+ import { GenuiExportError, type GenuiArtifactV1, type GenuiStandaloneAsset } from './types.ts'
5
+ import { STANDALONE_THEME_CSS } from './standalone-theme.ts'
6
+ import type { GenuiSpec } from '../spec.ts'
7
+
8
+ const bundleCache = new Map<string, Promise<Uint8Array>>()
9
+
10
+ /** 将二进制内容编码为不会闭合 script 标签的 Base64。 */
11
+ function bytesToBase64(bytes: Uint8Array): string {
12
+ let binary = ''
13
+ const chunkSize = 0x8000
14
+ for (let offset = 0; offset < bytes.length; offset += chunkSize) {
15
+ binary += String.fromCharCode(...bytes.subarray(offset, Math.min(offset + chunkSize, bytes.length)))
16
+ }
17
+ return btoa(binary)
18
+ }
19
+
20
+ /** 转义 HTML 属性和标题中的文本。 */
21
+ function escapeHtml(value: string): string {
22
+ return value.replace(/[&<>"']/g, character => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[character]!)
23
+ }
24
+
25
+ /** 获取并缓存构建产物字节,失败时保留可识别的导出错误。 */
26
+ function cachedBundle(name: string, code: GenuiStandaloneAsset | 'runtime'): Promise<Uint8Array> {
27
+ const existing = bundleCache.get(name)
28
+ if (existing !== undefined) return existing
29
+ const task = fetch(assetUrl(name)).then(async response => {
30
+ if (!response.ok) throw new GenuiExportError(code === 'runtime' ? 'runtime-fetch-failed' : 'asset-fetch-failed', `${name}: HTTP ${response.status}`)
31
+ return new Uint8Array(await response.arrayBuffer())
32
+ }).catch(error => {
33
+ bundleCache.delete(name)
34
+ if (error instanceof GenuiExportError) throw error
35
+ throw new GenuiExportError(code === 'runtime' ? 'runtime-fetch-failed' : 'asset-fetch-failed', `${name}: ${error instanceof Error ? error.message : 'fetch failed'}`)
36
+ })
37
+ bundleCache.set(name, task)
38
+ return task
39
+ }
40
+
41
+ /** 创建仅存放 Base64 数据的非执行 payload 节点。 */
42
+ function bundleElement(name: string, bytes: Uint8Array): string {
43
+ return `<script type="application/octet-stream" data-genui-bundle="${name}">${bytesToBase64(bytes)}</script>`
44
+ }
45
+
46
+ /** 将媒体相对地址转换为导出页面所在站点的绝对地址。 */
47
+ function resolveMediaUrls(spec: GenuiSpec, baseURI: string): GenuiSpec {
48
+ const copy = structuredClone(spec)
49
+ /** 遍历规范中的嵌套节点和容器。 */
50
+ function visit(value: unknown): void {
51
+ if (Array.isArray(value)) {
52
+ for (const item of value) visit(item)
53
+ return
54
+ }
55
+ if (typeof value !== 'object' || value === null) return
56
+ const node = value as Record<string, unknown>
57
+ if (node.type === 'image' || node.type === 'audio' || node.type === 'video') {
58
+ for (const field of node.type === 'video' ? ['src', 'poster'] : ['src']) {
59
+ const media = node[field]
60
+ if (typeof media !== 'string' || /^https?:\/\//i.test(media)) continue
61
+ const url = new URL(media, baseURI)
62
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') throw new GenuiExportError('artifact-invalid', `media URL cannot be exported: ${media}`)
63
+ node[field] = url.href
64
+ }
65
+ }
66
+ for (const child of Object.values(node)) visit(child)
67
+ }
68
+ visit(copy.items)
69
+ return copy
70
+ }
71
+
72
+ /** 返回执行内嵌 bundle 的静态启动代码。 */
73
+ function bootstrapSource(): string {
74
+ return `
75
+ async function loadBundle(element) {
76
+ const binary = atob(element.textContent.trim());
77
+ const bytes = new Uint8Array(binary.length);
78
+ for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index);
79
+ const url = URL.createObjectURL(new Blob([bytes], { type: 'text/javascript' }));
80
+ try {
81
+ await new Promise((resolve, reject) => {
82
+ const script = document.createElement('script');
83
+ script.src = url;
84
+ script.onload = resolve;
85
+ script.onerror = reject;
86
+ document.head.appendChild(script);
87
+ });
88
+ } finally {
89
+ URL.revokeObjectURL(url);
90
+ }
91
+ }
92
+ async function start() {
93
+ for (const element of document.querySelectorAll('[data-genui-bundle]')) await loadBundle(element);
94
+ const encoded = document.querySelector('#genui-artifact').textContent.trim();
95
+ const artifact = JSON.parse(new TextDecoder().decode(Uint8Array.from(atob(encoded), character => character.charCodeAt(0))));
96
+ window.__GenuiStandalone__.mount(document.querySelector('#genui-root'), artifact);
97
+ }
98
+ start().catch(error => {
99
+ const root = document.querySelector('#genui-root');
100
+ root.textContent = error instanceof Error ? error.message : 'GenUI could not start';
101
+ root.setAttribute('role', 'alert');
102
+ });
103
+ `
104
+ }
105
+
106
+ /** 使用已下载的运行文件生成完整的独立 HTML 文档。 */
107
+ export function createStandaloneHtmlDocument(
108
+ artifact: GenuiArtifactV1,
109
+ bundles: ReadonlyMap<string, Uint8Array>,
110
+ baseURI = document.baseURI,
111
+ ): string {
112
+ const htmlArtifact = { ...artifact, spec: resolveMediaUrls(artifact.spec, baseURI) }
113
+ const encodedArtifact = bytesToBase64(new TextEncoder().encode(JSON.stringify(htmlArtifact)))
114
+ const engines = artifactPortabilityAssets(artifact).map(name => {
115
+ const bytes = bundles.get(`${name}.js`)
116
+ if (bytes === undefined) throw new GenuiExportError('asset-fetch-failed', `${name}.js: bundle unavailable`)
117
+ return bundleElement(name, bytes)
118
+ }).join('\n')
119
+ const runtime = bundles.get('standalone-runtime.js')
120
+ if (runtime === undefined) throw new GenuiExportError('runtime-fetch-failed', 'standalone-runtime.js: bundle unavailable')
121
+ const csp = "default-src 'none'; script-src 'unsafe-inline' blob:; style-src 'unsafe-inline'; font-src data:; img-src 'self' data: blob: https: http:; media-src 'self' data: blob: https: http:; connect-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'"
122
+ return `<!doctype html>
123
+ <html lang="${artifact.presentation.locale === 'zh' ? 'zh-CN' : 'en'}">
124
+ <head>
125
+ <meta charset="utf-8">
126
+ <meta name="viewport" content="width=device-width,initial-scale=1">
127
+ <title>${escapeHtml(artifact.spec.title ?? 'GenUI')}</title>
128
+ <meta http-equiv="Content-Security-Policy" content="${csp}">
129
+ <style>${STANDALONE_THEME_CSS}</style>
130
+ </head>
131
+ <body${artifact.presentation.theme === 'dark' ? ' data-ds-dark-theme' : ''}>
132
+ <main id="genui-root"></main>
133
+ <script type="application/octet-stream" id="genui-artifact">${encodedArtifact}</script>
134
+ ${engines}
135
+ ${bundleElement('runtime', runtime)}
136
+ <script>${bootstrapSource()}</script>
137
+ </body>
138
+ </html>`
139
+ }
140
+
141
+ /** 返回 artifact 所需图形引擎名称。 */
142
+ function artifactPortabilityAssets(artifact: GenuiArtifactV1): GenuiStandaloneAsset[] {
143
+ return analyzeGenuiPortability(artifact.spec).requiredAssets
144
+ }
145
+
146
+ /** 获取独立运行文件及规格实际使用的图形引擎,生成单文件 HTML。 */
147
+ export async function buildStandaloneHtml(rawArtifact: GenuiArtifactV1): Promise<string> {
148
+ const artifact = parseGenuiArtifact(rawArtifact)
149
+ if (artifact === null) throw new GenuiExportError('artifact-invalid', 'invalid GenUI artifact')
150
+ const report = analyzeGenuiPortability(artifact.spec)
151
+ if (report.customTypes.length > 0) {
152
+ throw new GenuiExportError('unsupported-custom-component', `custom components: ${report.customTypes.join(', ')}`)
153
+ }
154
+ const names = [...report.requiredAssets.map(name => `${name}.js`), 'standalone-runtime.js']
155
+ const results = await Promise.all(names.map(async name => [name, await cachedBundle(name, name === 'standalone-runtime.js' ? 'runtime' : name.slice(0, -3) as GenuiStandaloneAsset)] as const))
156
+ return createStandaloneHtmlDocument(artifact, new Map(results))
157
+ }
@@ -0,0 +1,15 @@
1
+ export { createGenuiArtifact } from './create.ts'
2
+ export { buildStandaloneHtml } from './html.ts'
3
+ export { downloadGenuiArtifactHtml, downloadGenuiArtifactJson } from './download.ts'
4
+ export { parseGenuiArtifact } from './parse.ts'
5
+ export { analyzeGenuiPortability } from './portability.ts'
6
+ export { serializeGenuiArtifact } from './json.ts'
7
+ export { GenuiExportError } from './types.ts'
8
+ export type {
9
+ CreateGenuiArtifactOptions,
10
+ GenuiArtifactTheme,
11
+ GenuiArtifactV1,
12
+ GenuiExportErrorCode,
13
+ GenuiPortabilityReport,
14
+ GenuiStandaloneAsset,
15
+ } from './types.ts'
@@ -0,0 +1,6 @@
1
+ import type { GenuiArtifactV1 } from './types.ts'
2
+
3
+ /** 以稳定缩进序列化 GenUI artifact。 */
4
+ export function serializeGenuiArtifact(artifact: GenuiArtifactV1): string {
5
+ return JSON.stringify(artifact, null, 2)
6
+ }