@changfenhuang/dsh-genui 0.11.3 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +21 -6
  3. package/README.zh-CN.md +21 -6
  4. package/SKILL.md +18 -6
  5. package/lib/assets/mermaid.js +325 -325
  6. package/lib/assets/standalone-runtime.js +10 -9
  7. package/lib/client.js +31 -33
  8. package/lib/index.js +1060 -371
  9. package/lib/types/client/blocks/forms.d.ts +5 -6
  10. package/lib/types/client/blocks/state.d.ts +2 -11
  11. package/lib/types/client/dom-fence.d.ts +2 -1
  12. package/lib/types/client/file-address.d.ts +4 -0
  13. package/lib/types/client/file-link-context.d.ts +41 -0
  14. package/lib/types/client/file-link.d.ts +17 -0
  15. package/lib/types/client/i18n/en.d.ts +5 -1
  16. package/lib/types/client/inline.d.ts +1 -1
  17. package/lib/types/client/mermaid-colors.d.ts +2 -0
  18. package/lib/types/client/panel.d.ts +6 -1
  19. package/lib/types/client/spec.d.ts +8 -11
  20. package/lib/types/client/submission-registry.d.ts +51 -0
  21. package/lib/types/client/table-details.d.ts +20 -0
  22. package/lib/types/client/walk-spec.d.ts +3 -0
  23. package/lib/types/plugin/fence-feedback.d.ts +55 -12
  24. package/lib/types/plugin/index.d.ts +7 -3
  25. package/lib/types/shared/fence-repair.d.ts +19 -7
  26. package/package.json +1 -12
  27. package/src/client/GenuiBlock.module.css +100 -4
  28. package/src/client/GenuiBlock.tsx +7 -14
  29. package/src/client/artifact/ArtifactExport.module.css +34 -3
  30. package/src/client/artifact/ExportableGenuiBlock.tsx +23 -2
  31. package/src/client/blocks/charts.tsx +63 -14
  32. package/src/client/blocks/forms.tsx +26 -48
  33. package/src/client/blocks/render-node.tsx +11 -8
  34. package/src/client/blocks/state.ts +2 -12
  35. package/src/client/dom-fence.tsx +190 -50
  36. package/src/client/file-address.ts +37 -0
  37. package/src/client/file-link-context.tsx +114 -0
  38. package/src/client/file-link.ts +46 -0
  39. package/src/client/genui-runtime/diagnostics.ts +20 -0
  40. package/src/client/genui-runtime/normalize.ts +28 -9
  41. package/src/client/guard.ts +98 -153
  42. package/src/client/i18n/en.ts +5 -1
  43. package/src/client/i18n/zh.ts +5 -1
  44. package/src/client/index.tsx +23 -5
  45. package/src/client/inline.ts +28 -10
  46. package/src/client/mermaid-colors.ts +39 -0
  47. package/src/client/mermaid-core.ts +7 -3
  48. package/src/client/panel.tsx +18 -5
  49. package/src/client/spec.ts +8 -11
  50. package/src/client/submission-registry.ts +150 -0
  51. package/src/client/table-details.ts +151 -0
  52. package/src/client/walk-spec.ts +43 -0
  53. package/src/plugin/fence-feedback.ts +384 -53
  54. package/src/plugin/index.ts +20 -9
  55. package/src/plugin/tool.ts +8 -2
  56. package/src/shared/fence-repair.ts +345 -128
  57. package/tsdown.config.ts +3 -7
  58. package/lib/invariant.js +0 -19
  59. package/lib/types/plugin/invariant.d.ts +0 -15
  60. package/src/plugin/invariant.ts +0 -30
package/CHANGELOG.md CHANGED
@@ -2,6 +2,50 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.12.0] - 2026-10-13
6
+
7
+ ### 新增
8
+
9
+ - GenUI 正文、表格、工具卡片和会话面板支持本地文件链接,点击后在宿主右侧栏打开文件并定位到指定行;支持工作区相对路径和 Windows 路径(#273、PR #278)。
10
+ - 增加默认开启的 `renderUiTool` 配置。设为 `false` 时只关闭 `render_ui` 工具及相关模型指引,发前校验、正文围栏和会话面板继续可用(#276、PR #281)。
11
+
12
+ ### 修复
13
+
14
+ - JSON 对象中无值的字段名紧接正常字段时,围栏修复只移除无值字段,保留后续内容;浏览器渲染与发前校验共用这一修复,避免将两个字段名拼接后丢失卡片正文(#290、#291)。
15
+ - 宿主隐藏 `dsh-ui` 语言、同时改变代码块包装类名时,DOM 通道现可通过宿主的代码内容语义标记定位包装节点,继续从原始消息识别围栏;不再因漏扫描而留下裸 JSON,正文和明确声明为普通代码的围栏保持原样(#275)。
16
+ - 缺少结束标记的 `dsh-ui` 围栏若正文已经可以渲染(包括尾部工具残片或可修复的 JSON),不再要求模型重发同一张卡片;一条回复中只有无法渲染的围栏会进入纠错(#288)。
17
+ - 修复单行表格中的行内代码导致文字越过列边界的问题,同时保留连续空格;需要更宽空间的表格通过容器横向滚动显示(#280)。
18
+ - 表格页脚单元格中的行内代码同样沿用「表格内 chip 不重新开启断行」的规则;浏览器回归夹具补上宿主 `.markdown` 级联(`overflow-wrap: anywhere` 与 `:not(pre) > code` 的 `inline-flex`),让跨列溢出在夹具中可复现,并把重叠检查从首列相邻对扩展到所有相邻列对(#285)。
19
+ - 发前校验遇到原生组件的未知字段时,返回结构化字段路径并要求修改后重验,不再指示直接发送围栏;JSON 已修复的路径同样执行这一判定(#275 追加反馈)。
20
+ - 图表主题色先解析为实际 RGB/RGBA,修复第三方皮肤中 `calc()` 等颜色表达式导致 Mermaid 渲染失败的问题(PR #279)。
21
+ - DOM 围栏扫描遇到已失效的 Cordis Context 时,会保留已挂载的流式围栏和组件状态,并在 Context 恢复后继续解析(#280)。
22
+
23
+ ### 兼容性
24
+
25
+ - 将 DSH 当前宿主与下一测试版的接口、打包安装和发布验证目标更新为 `0.2.0-rc.2` 与 `0.2.1-alpha.1`。
26
+ - 移除 DSH 已废弃的运行时诊断配套入口 `./invariant`、对应构建产物与 `@deepseek-ai/dsh-invariants` 直接依赖。
27
+ - 对应宿主版本的公开依赖检查加入技能注册、插件重载和持久化纠错恢复测试;会话样本补齐新版要求的消息记录字段。
28
+
29
+ ### 变更
30
+
31
+ - 完成态 GenUI 的导出入口改为无边框图标,沿用既有图标按钮的悬浮与聚焦反馈;保留“导出”提示和 HTML / GenUI JSON 菜单,并补齐第三方主题中的悬浮反馈(#274、PR #277)。
32
+
33
+ ## [0.11.4] - 2026-10-08
34
+
35
+ ### 修复
36
+
37
+ - DSH Desktop 0.11.0 将 `dsh-ui` 围栏呈现为 `<dsh-ui>…</dsh-ui>` 纯文本标签时,DOM 通道现可识别助手消息中的完整标签文本,渲染卡片并在卸载时恢复原文(#268)。
38
+ - `list` 项支持 `label`/`name` 标题字段与 `description`/`content`/`text`/`body`/`detail` 正文字段;只有正文的记录也能显示,无法渲染的记录字段会给出位置明确的警告(#265)。
39
+ - **GenUI reasoning-only 回合进入宿主重试**:当前回合调用 `validate_dsh_ui` 后,普通顶层会话若以 `stop` 结束且完整内容块只有 reasoning,插件会将终止结果改写为 `EMPTY_RESPONSE`,交由宿主现有重试策略处理;普通会话、子代理、辅助请求及含正文或工具调用的结果保持原行为。`fenceFeedback: false` 仍只关闭同回合围栏修正(#259)。
40
+ - **完善 GenUI 交付判定与 fence-feedback 重载恢复**:`render_ui` 只有明确返回 `status=rendered` 才计为正式交付,正常返回的 `status=invalid` 会继续进入零交付修正;plugin reload 时主动读取历史会话,恢复当前 turn 的 render fingerprint、delivery turn 和已消费的 correction budget,避免重复发送 delivery reminder 或重新获得修正次数;render fingerprint 去重仅限当前 turn,新 turn 可重新修正相同错误。每 turn 最多两条 correction 的上限由 render failure 与 nothing-delivered reminder 共同使用(#263、#267)。
41
+ - **非英文宿主 locale 下围栏永远不渲染**:通用代码块的判定依赖硬编码标题白名单(`Code` / `Code block` / `代码块`),宿主 locale 为其它语言时(如俄语包的 «Код»),本地化标题被当成真实语言,同一份 `dsh-ui` 围栏既不走带标签路径也不进内容识别——界面永远停在代码块。现在判定反转:banner 标签命中**已知真实语言闭集**(语言 id 有限、由插件维护)才视为带语言,空标签与任何本地化通用词一律视为无语言、交给内容校验把关——同一份正文的渲染结果不再因宿主语言而不同(#258)。
42
+ - **多行文本/代码的复制不再丢换行**:行内渲染曾把每个 `\n` 变成 `<br>`,而 `<br>` 对 `textContent` 与 `Selection.toString()` **零贡献**——用户选中表格单元格复制出来的是一整行,`python - <<'PY' … PY` 这类 heredoc 结构被毁、必须手工拼回。现在换行在 DOM 里保持为**真实换行符**(选中/复制原样),由容器的 `white-space` 负责呈现:代码形单元格 `.tdCode` 用 `pre-wrap`(换行 + 行首缩进都保留),散文单元格 `.tdMultiline` 用 `pre-line`(换行呈现、空格照常折叠),单行单元格仍是 `nowrap`;`calloutBody`/`liTitle`/`liDesc`/`kvValue`/`tlDesc`/`detailBody`/`accBody` 同步声明 `pre-line`,因为它们此前正是靠 `<br>` 硬断行(#233)。
43
+ - **多行表格单元格的缩进**:`.table td` 的 `nowrap`(表格的数据语气)会折叠单元格里的空白,模型写进单元格的代码缩进会消失;行内 `` `code` `` 同步改为 `pre-wrap`,不再吞掉自身空格(#233)。
44
+ - **「未转义引号 + 尾部杂字符」叠在一起时围栏不再整条失守**:模型常在 JSON 根值之后追加 `</p>` 或一句解释(真实会话 539 条围栏里 240 条有尾随 `</p>`),这类正文本来靠 `parsePartialGenuiSpec` 的平衡前缀就能渲染;但一旦**同时**还有字符串内未转义的半角引号,tier-1 因为「必须整体 parse 通过」而放弃、tier-2 同样放弃——两个缺陷叠加就把整条围栏变成代码块 + 红横幅。现在 tier-2 在整体 parse 失败时回退到**平衡前缀**(根值结束处)并采用它;tier-1 保持严格(它也在流式期运行,采用前缀可能发布半截正文)。真实语料 37 条不可渲染 → 22 条(救回 15 条,占失败数 40%)。
45
+ - SKILL.md 使用规则第 3 条补充:`}` 之后不要再写任何字符(常见错误是追加 `</p>` 或解释)。
46
+ - **通用代码块的内容识别接受标点级修复**:宿主会隐去它不认识的语言(高亮器不支持 `dsh-ui`),此时同一份围栏在 DOM 里是一个「代码块 / Code block / Code」标签的普通代码块;当 ChatSnapshot 的语言来源在那一行上不可用时,内容识别是唯一出路。此前它要求 `JSON.parse(raw)` **直接**通过,于是「只差一个未转义引号(tier-1 可修)」的围栏永远不会被接管——真实会话(seq 40530)里模型终于把围栏写进正文、正文经 tier-1 修 14 处后可正常渲染,界面却始终停在代码块。现在内容识别跑与带标签路径**相同的 tier-1 修复**,并且在整体 parse 失败时**裁到第一个平衡根值**(`trimToBalancedRoot`,只裁剪、不补全结构)——真实样本里模型把工具调用模板泄漏在 JSON 之后且围栏没闭合,正是这一种;结构级 tier-2 刻意不参与(兜底不该接管只是"长得像 JSON"的普通代码),canonical 规范 / 未知字段 / 归一化等价的检查保持不变。
47
+ - **回合状态的两处漏补救**:① `render_ui` 是否交付由 `tool/result` 决定,只有无内部错误、结果块非 `isError` 且协议明确返回 `status=rendered` 才算成功;结果未到达时回合边界保持静默,不与晚到结果赛跑。② 同回合的合成上下文消息(`agent.inject()` 通知、成员消息等)不会清掉本回合的验证/交付状态;重置只发生在正式的 `turn/start` 边界与直接用户提示(`source.kind === 'user'`)上(#236、#263 review)。
48
+
5
49
  ## [0.11.3] - 2026-09-29
6
50
 
7
51
  ### 兼容性
package/README.md CHANGED
@@ -96,7 +96,7 @@ The repository ships both renderer channels, the host plugin, and the built brow
96
96
 
97
97
  Prerequisites — all required:
98
98
 
99
- 1. **dsh `^0.1.2-rc.1 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || >=0.2.0-rc.1 <0.3.0-0`** (DSH `0.2.0-rc.1` is currently a prerelease; verified host roles: minimum `dsh-v0.1.2-rc.1`, current `dsh-v0.1.7-rc.2`, next `dsh-v0.2.0-rc.1`; users on DSH `<=0.1.1-rc.x` should use dsh-genui `0.9.8`)
99
+ 1. **dsh `^0.1.2-rc.1 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || >=0.2.0-rc.1 <0.3.0-0`** (DSH `0.2.1-alpha.1` is currently a prerelease; verified host roles: minimum `dsh-v0.1.2-rc.1`, current `dsh-v0.2.0-rc.2`, next `dsh-v0.2.1-alpha.1`; users on DSH `<=0.1.1-rc.x` should use dsh-genui `0.9.8`)
100
100
  2. **`pnpm` on your PATH**: the `dsh plugin` command depends on it. If missing: `corepack enable` (or `npm i -g pnpm`), then **open a new terminal** and confirm `pnpm -v` prints a version
101
101
 
102
102
  Install and activate in DSH (one command, all dependencies included):
@@ -116,6 +116,12 @@ npm install @changfenhuang/dsh-genui
116
116
 
117
117
  > ⚠️ **Don't use `link:` on a freshly cloned directory** — `link:` does not install the plugin's dependencies (mermaid / three / react), so the renderer will break. Use the npm command above for normal installation; reserve `link:` for local development iteration (see below).
118
118
 
119
+ ### Package name and version matter
120
+
121
+ - **The scope matters**: npm also hosts a same-named, unscoped [`dsh-genui`](https://www.npmjs.com/package/dsh-genui) (a Vue/OpenTiny implementation by a different maintainer, unrelated to this repo). If an install listing describes "interactive charts, forms, calculators, dashboards, and mini apps", that is the other project — the host will reject it over incompatible peers. This plugin is always **`@changfenhuang/dsh-genui`**.
122
+ - **When the host rejects the install**: the supported host range is declared in peerDependencies and enforced by the host — upgrade to the `latest` plugin version on npm and reinstall; no other config changes are needed.
123
+ - **When the resolved version is older than `latest`**: this is usually pnpm's release-age policy (versions published within the last 24 hours are silently skipped). Wait a day and retry, or add the target version to `minimumReleaseAgeExclude` in the profile's `pnpm-workspace.yaml`; if the profile exact-pins an old version, update the declaration first, then reinstall.
124
+
119
125
  ### Migrating from the old `@omdsh-dev` package name
120
126
 
121
127
  If you installed from `github:omdsh-dev/dsh-genui` before v0.9.2, pnpm may keep the dependency under the old `@omdsh-dev/dsh-genui` key even though the repository now declares `@changfenhuang/dsh-genui`. The loader resolves plugins from the profile's dependency keys, so a later reinstall can then fail with `Cannot find package '@changfenhuang/dsh-genui'`. Re-add the plugin under its current package name:
@@ -163,16 +169,25 @@ The following is the detailed capability reference. Every behavior is constraine
163
169
  - **ECharts integration**: the `echart` node renders full ECharts charts with theme-aware colors, tooltips, and legends. Two modes: **preset shorthand** (`preset: 'bar' | 'line' | 'area' | 'pie' | 'scatter'` + `data`/`series`) for quick upgrade from the `chart` node, or **full option** (`option` field) for custom chart types, dataZoom, visualMap, and other advanced ECharts features. The echarts engine (~1 MB) is lazy-loaded on demand — the main bundle never carries it, and conversations without `echart` nodes never download it- **Function plots**: `plot` draws curves; parameter sliders redraw in real time, with optional auto-animation
164
170
 
165
171
  - **Quiz**: `quiz` grades on click with explanation and retry; with `action`, the answer is also sent back to the model (grading stays local and instant)
166
- - **Local grading (submit)**: a multiple-choice set = one `radio` per question with `group` + `answer` (correct answer) + `explanation`, plus one `submit` button — after the user answers everything and clicks once, **the score, per-question right/wrong, and explanations appear right in the UI with zero model round-trips**; the quiz then locks, and "retake" resets locally (optional `resetAction` notifies the model). Questions without an answer fall back to an aggregated action (`fields` collects every input with an `id`)
172
+ - **Local grading (submit)**: a multiple-choice set uses one `radio` per question with `group` + `answer` + `explanation` and a `submit` button. Once the required radio members are answered, grading appears in the UI when no other collected block state needs delivery. An aggregated action sends `answers`, optional `fields`, `total`, and `answered` in other cases.
173
+ - **Submit groups**: `groups` names submission member keys from `radio.group`, `checkbox.group`, `input.id`, `textarea.id`, `select.id`, and `slider.id`. A radio needs a selection, a checkbox group needs at least one selection, and a field needs a value that remains after trimming. `groups` controls required members and progress; the payload still collects filled form state across the block. Without `groups`, any answered member enables submission.
167
174
  - **State persistence**: answers, submission locks, and input values are saved per "session + content fingerprint" — refresh or reopen restores everything; re-rendering identical content keeps user state; new content starts fresh; LRU cap of 200 blocks
168
175
  - **Form semantics**: `input` Enter / `textarea` Ctrl+Enter submits immediately (`submit:true`), no blur needed; fields with an `id` are collected into the submit's `fields`
169
176
  - **Secrets ban**: GenUI must never ask for passwords, API keys, access tokens, recovery codes, or other secrets; even if a password input appears, it stays masked, is never persisted, and never enters form collection
170
177
  - **Local-first principle**: state changes the UI can do itself (grading, quiz checking, resets, expand/collapse, selection) always happen locally and instantly; actions are reserved for things that genuinely need the model (generating new content, running tools, next-step suggestions)
171
178
  - **Honest interactions**: interactive components must carry `action`; buttons without one render disabled (kills the "looks clickable, does nothing" fake button); buttons with `action` show instant "triggered" local feedback (proof the local event fired, not that the model received it)
172
179
  - **Event loop**: buttons, checkboxes, radio buttons, switches, selects, inputs, textareas, submits, and quizzes send one event immediately per gesture; slider drags keep trailing-edge debounce, sending only the final value per slider, with different `id`s handled independently
173
- - **Tool channel**: the `render_ui` tool renders the same spec as a card in the tool row (deliverable-style UI goes through the tool, answer-style UI through the fence)
180
+ - **Tool channel**: `render_ui` is registered by default and renders the same spec as a card in the tool row; `validate_dsh_ui` remains available. Set `renderUiTool: false` in this plugin's config to stop registering `render_ui`. The `dsh-ui` fence, `panel: true`, `/panel`, and historical `render_ui` results in the client tool view continue to work. Reload the host plugin after changing this setting.
181
+ - **Disable the `render_ui` tool**: add `renderUiTool: false` under this plugin's `config:` in your profile:
182
+
183
+ ```yaml
184
+ - id: genui
185
+ name: '@changfenhuang/dsh-genui'
186
+ config:
187
+ renderUiTool: false
188
+ ```
174
189
  - **Session panel**: a persistent dock above the composer; `render_ui` / `panel: true` fences update the same surface in place; `/panel` opens it from the client (`/panel <instruction>` customizes via the model, `/panel clear` clears); the top border is draggable to resize; `append: true` merges incrementally — same-named tabs append content, new tabs get added; the whole panel caps at 200 nodes / 200 appends, after which the model should send `replace` to rebuild
175
- - **Fence auto-repair**: enabled by default; set `fenceFeedback: false` in this plugin's config (under the plugin entry's `config:` in your profile's cordis.patch.yml) to disable it. When a reply's final dsh-ui fence fails to render, the plugin steers the SAME turn with the per-node diagnosis so the model can resend a fixed fence; at most one correction per turn and per fence, never in subagents, so it cannot loop.
190
+ - **Fence auto-repair**: enabled by default; set `fenceFeedback: false` in this plugin's config (under the plugin entry's `config:` in your profile's cordis.patch.yml) to disable same-turn fence corrections. A validated GenUI turn that ends with reasoning only still becomes `EMPTY_RESPONSE` and uses the host's retry policy. When a reply's final dsh-ui fence fails to render, the plugin steers the SAME turn with the per-node diagnosis so the model can resend a fixed fence. At most two corrections are sent per turn, shared by render failures and validated turns with no formal delivery; each fence fingerprint is corrected once per turn, and a nothing-delivered reminder is sent once per turn. A new turn can correct the same failed fence again. Plugin reload restores the current turn's fence fingerprints and correction count, along with delivery reminders, from persisted correction messages. Corrections are never sent in subagent sessions.
176
191
  - **Self-healing & limits**: every fence passes a spec guard — bad nodes are silently dropped (the surviving siblings keep rendering: one bad component no longer degrades the whole fence), numbers clamped, strings truncated; the whole tree is capped at 200 nodes / 8 nesting levels; pathological specs never crash the UI
177
192
  - **Canonical component protocol**: native field aliases such as `card.label` → `title`, `table.data`/`table.items` → `rows`, `callout.kind`/`callout.desc` → `tone`/`content` (tone value `danger` → `error`), `steps.items` → `steps`, `keyvalue.items` → `pairs` (record `label` → `key`), and `file-tree.nodes` → `items` (record `label` → `name`, `type` defaults to `dir` when children exist) are normalized deterministically before validation and rendering. A root-level component array is adopted as `items`, and a double-encoded JSON string is decoded once. `validate_dsh_ui` reports these normalizations and warns about unknown native fields without blocking custom renderer nodes.
178
193
  - **Chart error self-healing**: mermaid failures auto-retry with repairs (strip backticks, quote Chinese/space labels, remove `<br/>`) before degrading to source; a broken chart never hits the screen
@@ -239,9 +254,9 @@ pnpm run check # type check + full tests + build
239
254
 
240
255
  With the locked dependencies installed, the check script (`pnpm run check` or `npm run check`) uses the pinned DSH `0.1.2-rc.1` release packages.
241
256
 
242
- The verified host roles are minimum `dsh-v0.1.2-rc.1`, current `dsh-v0.1.7-rc.2`, and next `dsh-v0.2.0-rc.1`. DSH `0.2.0-rc.1` is currently a prerelease. CI keeps four primary lanes: Node 22 + current, and Node 24 + minimum/current/next. Replace these pinned tags as DSH publishes newer versions.
257
+ The verified host roles are minimum `dsh-v0.1.2-rc.1`, current `dsh-v0.2.0-rc.2`, and next `dsh-v0.2.1-alpha.1`. DSH `0.2.1-alpha.1` is currently a prerelease. CI keeps four primary lanes: Node 22 + current, and Node 24 + minimum/current/next. Replace these pinned tags as DSH publishes newer versions.
243
258
 
244
- `pnpm run check:host-api dsh-v0.1.7-rc.2` and `pnpm run check:host-api dsh-v0.2.0-rc.1` install the corresponding published DSH packages and run TypeScript typecheck plus tsdown build. Each CI host lane then installs the generated tarball in its pinned DSH host and runs the packed smoke.
259
+ `pnpm run check:host-api dsh-v0.2.0-rc.2` and `pnpm run check:host-api dsh-v0.2.1-alpha.1` install the corresponding published DSH packages and run TypeScript typecheck, plugin lifecycle tests (including skill registration and persisted feedback recovery), and tsdown build. Each CI host lane then installs the generated tarball in its pinned DSH host and runs the packed smoke.
245
260
 
246
261
  Run `node scripts/verify-pack.mjs --keep` to retain the verified tarball for inspection or e2e use. The default `node scripts/verify-pack.mjs` removes its temporary directory after verification.
247
262
 
package/README.zh-CN.md CHANGED
@@ -96,7 +96,7 @@ CI 的 packed host smoke 会把实际生成的 npm tarball 安装到真实 DSH
96
96
 
97
97
  前置条件,缺一不可:
98
98
 
99
- 1. **dsh `^0.1.2-rc.1 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || >=0.2.0-rc.1 <0.3.0-0`**(DSH `0.2.0-rc.1` 当前属于预发布版本;已验证宿主角色:minimum `dsh-v0.1.2-rc.1`、current `dsh-v0.1.7-rc.2`、next `dsh-v0.2.0-rc.1`;使用 DSH `<=0.1.1-rc.x` 的用户请使用 dsh-genui `0.9.8`)
99
+ 1. **dsh `^0.1.2-rc.1 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || >=0.2.0-rc.1 <0.3.0-0`**(DSH `0.2.1-alpha.1` 当前属于预发布版本;已验证宿主角色:minimum `dsh-v0.1.2-rc.1`、current `dsh-v0.2.0-rc.2`、next `dsh-v0.2.1-alpha.1`;使用 DSH `<=0.1.1-rc.x` 的用户请使用 dsh-genui `0.9.8`)
100
100
  2. **`pnpm` 在 PATH 上**:`dsh plugin` 命令依赖它。没有就 `corepack enable`(或 `npm i -g pnpm`),然后**新开一个终端**,确认 `pnpm -v` 有输出
101
101
 
102
102
  安装并在 DSH 中激活(一行命令,自动带上全部依赖):
@@ -116,6 +116,12 @@ npm install @changfenhuang/dsh-genui
116
116
 
117
117
  > ⚠️ **别用 `link:` 装一个刚 clone 的目录**——`link:` 不会安装插件的依赖(mermaid / three / react),装完渲染器会挂。正常安装请使用上面的 npm 命令;只有本地开发迭代才用 `link:`(见下文)。
118
118
 
119
+ ### 认准包名与版本
120
+
121
+ - **包名必须带 scope**:npm 上另有一个同名但不带 scope 的 [`dsh-genui`](https://www.npmjs.com/package/dsh-genui)(Vue/OpenTiny 实现,与本仓库无关,维护者不同)。如果安装列表里描述是 "interactive charts, forms, calculators, dashboards, and mini apps",那是那个项目——装上会因 peer 不兼容被宿主拒绝。本插件始终是 **`@changfenhuang/dsh-genui`**。
122
+ - **宿主拒绝安装时**:支持的宿主范围由 peerDependencies 声明、宿主据此检查,升级到 npm 上 `latest` 的插件版本重装即可,无需改其他配置。
123
+ - **解析到的版本低于 `latest`**:多半是 pnpm 的发布冷静期在起作用(默认跳过发布不满 24 小时的版本,且不提示)。等一天重试,或在 profile 的 `pnpm-workspace.yaml` 的 `minimumReleaseAgeExclude` 里加入目标版本;profile 里精确 pin 过旧版本的,先改依赖声明再重装。
124
+
119
125
  ### 从旧 `@omdsh-dev` 包名迁移
120
126
 
121
127
  如果你在 v0.9.2 之前通过 `github:omdsh-dev/dsh-genui` 安装过,pnpm 可能仍把依赖保存在旧的 `@omdsh-dev/dsh-genui` 键下,但仓库当前声明的包名已经是 `@changfenhuang/dsh-genui`。加载器按 profile 的依赖键解析插件,后续重装时就可能报 `Cannot find package '@changfenhuang/dsh-genui'`。请用当前包名重新添加一次:
@@ -163,16 +169,25 @@ dsh plugin --profile web add link:$PWD
163
169
  - **ECharts 集成**:`echart` 节点渲染完整的 ECharts 图表,自动适配主题色、提示框和图例。两种模式:**预设简写**(`preset: 'bar' | 'line' | 'area' | 'pie' | 'scatter'` + `data`/`series`)可从 `chart` 节点快速升级;**完整选项**(`option` 字段)支持自定义图表类型、dataZoom、visualMap 等高级 ECharts 功能。echarts 引擎(~1 MB)按需懒加载——主包不含引擎,没有 `echart` 节点的对话不会下载它- **函数图**:`plot` 画曲线,参数滑块拖动实时重绘,支持自动动画
164
170
 
165
171
  - **测验**:`quiz` 点选判题 + 解析 + 重试;带 `action` 时答案同时回传模型(判题仍本地即时)
166
- - **本地判卷(交卷)**:多道选择题 = 每题的 `radio` 加 `group` + `answer`(正确答案)+ `explanation`(解析),再加一个 `submit` 交卷按钮——用户全部选完点一次,**分数、每题对错、解析当场在 UI 里出现,零模型往返**;题目随即锁定,「重新作答」本地重置(可选 `resetAction` 通知模型)。题目没带答案时才退回聚合 action(`fields` 收集所有带 `id` 的输入)
172
+ - **本地判卷(交卷)**:多道选择题的每个 `radio` 设置 `group`、`answer` 和 `explanation`,再添加 `submit` 按钮。所需 radio 均已选择且当前 block 没有其他需要发送的表单状态时,分数、每题对错和解析直接显示在界面中;其他情况通过聚合 action 发送 `answers`、可选的 `fields`、`total` 和 `answered`。
173
+ - **提交分组**:`groups` 引用当前 block 中的 submission member key,来源为 `radio.group`、`checkbox.group`、`input.id`、`textarea.id`、`select.id`、`slider.id`。radio 需已选择,checkbox 组需至少选择一项,普通字段需在 trim 后非空。`groups` 控制提交所需成员与完成进度;payload 继续收集当前 block 中已填写的表单状态。未设置 `groups` 时,至少有一个已完成成员即可提交。
167
174
  - **状态持久化**:答案、交卷锁定、输入值按「会话 + 内容指纹」自动保存——刷新页面/重开会话原样恢复,重渲染相同内容保留用户状态,新内容自动从头开始;上限 200 块 LRU 淘汰
168
175
  - **表单语义**:`input` 回车 / `textarea` Ctrl+Enter 即时提交(`submit:true`),不用等失焦;带 `id` 的字段值进 submit 的 `fields` 收集
169
176
  - **秘密禁令**:GenUI 不得索取密码、API Key、访问令牌、恢复码或其他秘密;密码输入即使出现也保持打码、不持久化、不进表单收集
170
177
  - **本地优先原则**:UI 自己能完成的状态变化(判卷、判题、重置、展开、选中)一律本地即时完成;action 只用于必须模型参与的事(生成新内容、执行工具、下一步建议)
171
178
  - **诚实交互**:交互组件必须带 `action`;不带 `action` 的按钮渲染为禁用态(消灭"看着能点、点了没反应"的假按钮);带 `action` 的按钮点击后立即显示「已触发」本地反馈(只证明本地事件已触发,不代表模型已收到)
172
179
  - **事件循环**:按钮、复选框、单选、开关、下拉、输入、文本域、提交和测验等离散交互按一次手势一次事件立即回传模型;`slider` 连续拖动保留尾沿防抖,同一滑块只回传最终值,不同 `id` 的滑块互不合并。
173
- - **工具通道**:`render_ui` 工具把同一份 spec 渲染成工具行卡片(交付物型 UI 走工具、回答型 UI 走围栏)
180
+ - **工具通道**:默认注册 `render_ui`,并将同一份 spec 渲染成工具行卡片;`validate_dsh_ui` 始终可用。在本插件的 `config:` 中设置 `renderUiTool: false` 后,宿主不再注册 `render_ui`。正文 `dsh-ui` 围栏、`panel: true`、`/panel` 和客户端工具视图中的历史 `render_ui` 结果仍可使用。修改此设置后需要让宿主重新加载插件。
181
+ - **关闭 `render_ui` 工具**:在用户 profile 中本插件条目的 `config:` 下添加:
182
+
183
+ ```yaml
184
+ - id: genui
185
+ name: '@changfenhuang/dsh-genui'
186
+ config:
187
+ renderUiTool: false
188
+ ```
174
189
  - **会话面板**:composer 上方常驻 dock,`render_ui` / `panel: true` 围栏原地更新同一块界面;`/panel` 命令客户端直开(`/panel <指令>` 转模型定制、`/panel clear` 清空);顶边框可拖拽调高;`append: true` 增量合并——同名标签页追加内容、新标签页新增;整面板默认最多 200 节点 / 200 条追加,达到上限后模型应发送 `replace` 重建
175
- - **围栏自修**:默认开启;在 profile 的 cordis.patch.yml 中本插件条目的 `config:` 下设置 `fenceFeedback: false` 可以关闭。回答最终的 dsh-ui 围栏无法渲染时,插件借宿主的轮内转向(steer)把逐节点诊断送回**同一轮**,模型重发修好的围栏;每轮至多一次、每个围栏至多一次、子代理不触发,不会循环。
190
+ - **围栏自修**:默认开启;在 profile 的 cordis.patch.yml 中本插件条目的 `config:` 下设置 `fenceFeedback: false` 可以关闭同回合围栏修正。已调用 `validate_dsh_ui` 的 GenUI 回合若只输出 reasoning 并以 `stop` 结束,仍会转换为 `EMPTY_RESPONSE`,交给宿主重试策略处理。回答最终的 dsh-ui 围栏无法渲染时,插件借宿主的轮内转向(steer)把逐节点诊断送回**同一轮**,模型重发修好的围栏。每个 turn 最多发送两条 correction,渲染失败与“已验证但没有正式交付”共用该上限;同一 turn 内相同围栏 fingerprint 只修正一次,新 turn 可以重新修正相同错误;同一 turn 的零交付提醒只发送一次。plugin reload 后会从持久化的 correction message 恢复当前 turn 的围栏去重记录、已消费的 correction 次数以及零交付提醒记录。子代理回合不会收到围栏修正。
176
191
  - **自愈与上限**:每个围栏过规格守卫——坏节点静默丢弃(同围栏其余组件照常渲染,单个坏组件不再拖垮整条围栏)、数值钳位、字符串截断,整树 ≤200 节点 / 8 层嵌套,病态 spec 不会拖垮界面
177
192
  - **统一组件协议**:`card.label` → `title`、`table.data`/`table.items` → `rows`、`callout.kind`/`callout.desc` → `tone`/`content`(tone 值 `danger` → `error`)、`steps.items` → `steps`、`keyvalue.items` → `pairs`(记录内 `label` → `key`)、`file-tree.nodes` → `items`(记录内 `label` → `name`、有 children 时缺省 `dir`)等原生字段别名会在校验和渲染前确定性归一化;根级组件数组视为 `items`、双重编码的 JSON 字符串解一层;`validate_dsh_ui` 会报告归一化结果,并对原生组件未知字段给出警告,同时保持自定义 renderer 节点的透明兼容。
178
193
  - **图错误自愈**:mermaid 渲染失败自动修复重试(剥反引号、引号化中文/空格标签、去 `<br/>`),仍失败才降级源码;错误图永不直接上屏
@@ -253,9 +268,9 @@ pnpm run check # 类型检查 + 全量测试 + 构建
253
268
 
254
269
  安装锁定依赖后,检查脚本(`pnpm run check` 或 `npm run check`)使用固定的 DSH `0.1.2-rc.1` 发布包。
255
270
 
256
- 已验证宿主角色为 minimum `dsh-v0.1.2-rc.1`、current `dsh-v0.1.7-rc.2`、next `dsh-v0.2.0-rc.1`。DSH `0.2.0-rc.1` 当前属于预发布版本。CI 保持四条主要 lane:Node 22 + current,以及 Node 24 + minimum/current/next。DSH 发布新版本后,替换对应固定标签。
271
+ 已验证宿主角色为 minimum `dsh-v0.1.2-rc.1`、current `dsh-v0.2.0-rc.2`、next `dsh-v0.2.1-alpha.1`。DSH `0.2.1-alpha.1` 当前属于预发布版本。CI 保持四条主要 lane:Node 22 + current,以及 Node 24 + minimum/current/next。DSH 发布新版本后,替换对应固定标签。
257
272
 
258
- `pnpm run check:host-api dsh-v0.1.7-rc.2` 与 `pnpm run check:host-api dsh-v0.2.0-rc.1` 会安装对应 DSH 发布包,运行 TypeScript 类型检查与 tsdown 构建。每条 CI 宿主 lane 随后会把生成的 tarball 安装到固定版本的 DSH 宿主并运行 packed smoke。
273
+ `pnpm run check:host-api dsh-v0.2.0-rc.2` 与 `pnpm run check:host-api dsh-v0.2.1-alpha.1` 会安装对应 DSH 发布包,运行 TypeScript 类型检查、插件生命周期测试(包括技能注册与持久化纠错恢复)和 tsdown 构建。每条 CI 宿主 lane 随后会把生成的 tarball 安装到固定版本的 DSH 宿主并运行 packed smoke。
259
274
 
260
275
  运行 `node scripts/verify-pack.mjs --keep` 可保留已验收的 tarball,便于检查或运行 e2e;默认的 `node scripts/verify-pack.mjs` 会在验收后清理临时目录。
261
276
 
package/SKILL.md CHANGED
@@ -40,7 +40,7 @@ description: "Render structured interactive UI inline through the dsh-ui fence.
40
40
  - image: `{"type":"image","src":"/mmx-files/result.png","alt":"<user-language description>"}` — 展示浏览器可访问的 http(s) 或同源相对图片地址;懒加载;不支持 `file:`/`data:` 等本地或主动协议
41
41
  - audio: `{"type":"audio","src":"/mmx-files/result.mp3","alt":"<user-language description>","loop":true?}` — 原生控制条;用户主动播放,不自动播放;仅 http(s) 或同源相对地址
42
42
  - video: `{"type":"video","src":"/mmx-files/result.mp4","alt":"<user-language description>","poster":"/mmx-files/poster.jpg"?,"loop":true?,"muted":true?,"aspectRatio":"16:9|4:3|1:1|9:16"?}` — 原生播放/音量/全屏控制;不自动播放
43
- - list: `{"type":"list","items":["..."] 或 [{"title":"...","desc":"..."}] 或嵌套节点(如 {"type":"badge","label":"TS"})}` — 行内可嵌节点(计入节点预算)
43
+ - list: `{"type":"list","items":["..."] 或 [{"title":"...","desc":"..."}] 或嵌套节点(如 {"type":"badge","label":"TS"})}` — 行内可嵌节点(计入节点预算);记录中的 `label`/`name` 归一化为 `title`,`description`/`content`/`text`/`body`/`detail` 归一化为 `desc`;只有正文时将正文用作标题,未知字段会收到警告
44
44
  - table: `{"type":"table","columns":["..."],"rows":[["...","..."]],"types":["text|num|delta|bar|badge"]?,"details":[[...]]?,"total":true?}` — 表头点击本地排序(升/降/还原,零往返);数值感知:千分位(`1,234`)、`k/m/b`、`万/亿`、`%`、货币符号都能按真实数值比较,纯数值列自动右对齐;**带符号单元格自动着色**(`+12.4%` 绿、`-3` 红,无需额外字段);`types` 可按列指定 `bar`(0-100 内联进度条)、`ring`(0-100 小环)、`spark`(单元格写 `"3,5,4,8"` 画微趋势线)、`badge`(胶囊标签)、`delta`(强制涨跌色)、`num`(强制右对齐)、`index`(行号)、`group`(首列当分组标题:该行只有第一格有内容时渲染成跨列小标题);`"total":true` 追加合计行(数值列自动求和);**`"export":true`**:表格上方出现「复制 Markdown / 复制 CSV」两个小按钮(纯本地剪贴板,不发请求);**`"filter":"输入框id"`**:把表格和某个 input/select 绑定,读者输入即时过滤(`filterColumn` 可限定列)——数据多时**默认就该配一个**;**`"sortField":"下拉id"`** 用下拉的值(列名)排序;**`"details"` 与 rows 同序**,第 i 项是该行展开后的内容(可放任意组件,`null` = 该行不可展开)——首列出现 chevron,点开在整行下方展开明细,适合「主表 + 明细」
45
45
  - keyvalue: `{"type":"keyvalue","pairs":[{"key":"...","value":"..."}]}`
46
46
  - timeline: `{"type":"timeline","items":[{"title":"...","desc":"...","time":"..."}]}`
@@ -67,9 +67,9 @@ description: "Render structured interactive UI inline through the dsh-ui fence.
67
67
  - select: `{"type":"select","label":"...","options":["...","..."],"selected":下标?,"action":"pick"?,"id":"field-id"?}` — `selected` 预选某选项(缺省显示「请选择…」占位,不静默预选第一项);带 `id` 的选择跨刷新保留并进 submit 的 `fields`
68
68
  - checkbox: `{"type":"checkbox","label":"<user-language option>","checked":true?,"action":"toggle"?,"group":"group-id"?}` — 默认保持逐次 `action` 行为;**加 `group` 进入多选聚合模式**:同组 checkbox 可反复勾选/取消,变化只在本地记录、不发逐次 action,兄弟 `submit` 一次性把该组已选 label 作为字符串数组放进 `answers`(例如 `{"styles":["<user-language option>","<user-language option>"]}`)
69
69
  - slider: `{"type":"slider","label":"...","min":0,"max":100,"step":1,"value":n?,"action":"name"?,"id":"field-id"?}` — 数值表单滑块:实时显示数值;带 `id` 跨刷新保留并进 submit 的 `fields`(拖拽经防抖合并成一次 action)
70
- - radio: `{"type":"radio","label":"<user-language label>","options":["<user-language option>","<user-language option>"],"selected":n?,"action":"pick"?}` — 单选;**加 `"group":"group-id"` 进入聚合模式**:选择只本地记录、不发往返;**加 `"answer":正确下标或标签` + `"explanation":"<user-language explanation>"` 后,交卷在本地判卷**
70
+ - radio: `{"type":"radio","label":"<user-language label>","options":["<user-language option>","<user-language option>"],"selected":n?,"action":"pick"?}` — 单选;**加 `"group":"group-id"` 进入聚合模式**:选择只本地记录、不发往返;可设置 `"answer":正确下标或标签` 和 `"explanation":"<user-language explanation>"`,在纯 radio 且没有其他待发送表单状态时本地判卷
71
71
  - link: `{"type":"link","label":"...","href":"https://..."?}` — 仅 http(s)/mailto 协议被接受;无 `href` 时渲染为纯文本样式(不会假装可点)
72
- - submit: `{"type":"submit","label":"<user-language action>","action":"grade","groups":["q1","styles"],"resetAction":"redo"?}` — 聚合按钮:纯 radio 且题目带 `answer` 时仍本地立即判卷(得分 + 每题 ✓/✗ + 解析,零往返);其余聚合场景一次发送 `[genui-action]`,payload 为 `{answers:{q1:"<user-language option>",styles:["<user-language option>","<user-language option>"]},fields:{id:"<user-language value>"},total,answered}`。`groups` 中每个 radio 必须已选择、每个 checkbox 组必须至少勾选一项才可提交
72
+ - submit: `{"type":"submit","label":"<user-language action>","action":"grade","groups":["q1","styles"],"resetAction":"redo"?}` — 聚合按钮:`groups` 引用当前 block 中的 submission member key,来源为 `radio.group`、`checkbox.group`、`input.id`、`textarea.id`、`select.id`、`slider.id`;radio 需已选择,checkbox 组需至少选择一项,普通 field 的值需在 trim 后非空。`groups` 控制提交所需成员与完成进度;未设置时,至少有一个已完成成员即可提交。纯 radio 范围含 `answer` 且本次没有范围外 payload 时可本地判卷;其余场景一次发送 `[genui-action]`,payload 继续收集当前 block 中已填写的表单状态,字段名称保持 `answers`、`fields`、`total`、`answered`
73
73
  - switch: `{"type":"switch","label":"...","checked":true?,"action":"toggle"?}`
74
74
  - textarea: `{"type":"textarea","label":"...","placeholder":"...","rows":n?,"value":"...","action":"save"?,"id":"field-id"?}` — action 在失焦和 **Ctrl/Cmd+Enter** 时触发;blur 仅值有变化才发送;带 `id` 的值刷新后保留
75
75
  - tabs: `{"type":"tabs","tabs":[{"label":"...","items":[...]}]}` — 空 tab 可以省略 `items`,会按空数组处理
@@ -131,10 +131,10 @@ description: "Render structured interactive UI inline through the dsh-ui fence.
131
131
  | `` `code` `` | 行内代码胶囊 |
132
132
  | `**加粗**` | 强调(不换行、不成块) |
133
133
  | `==高亮==` | 极淡底色标记 |
134
- | `[文字](https://…)` | 行内链接(http/https/mailto;非法目标退化为纯文字) |
134
+ | `[文字](https://…)` | 行内链接(http/https/mailto;围栏会话中的本地文件链接支持右侧栏打开与行号定位;无效目标及独立 HTML artifact 中的本地文件链接显示为普通文字) |
135
135
  | JSON `"\n"`(真实换行符) | 换行——**多段文字写同一个字段**,不要为换行拆成多个节点 |
136
136
 
137
- 不嵌套、不解析 HTML(每个标记生成 React 元素,不走 innerHTML;`<br>` 字面显示,换行用 `"\n"`);标记没闭合时原样显示。数值列 / badge / spark 单元格不解析(数字没什么可强调的)。
137
+ 不嵌套、不解析 HTML(每个标记生成 React 元素,不走 innerHTML;`<br>` 字面显示,换行用 `"\n"`);标记没闭合时原样显示。表格文本、`badge`、`delta` 单元格只识别本地文件链接,外部链接与无效目标显示为普通文字;`bar`、`spark`、`ring`、`index` 等专用数值单元格保持原有展示。
138
138
 
139
139
  文字字段只支持行内富文本。`text.content`、`callout.content`、`list` 项、`keyvalue` 值、`table` 单元格等字段中不要嵌入 Markdown 表格或连续三个及以上反引号、波浪号组成的代码围栏;表格使用 `table`,代码使用 `code`,代码改动使用 `diff`,结构化 JSON 使用 `json`。误写入文字字段的代码围栏连同内部标记保持原文,未闭合时从围栏标记开始保持原文。`validate_dsh_ui` 返回 `warning=block_markdown` 时,按照 `replacement` 改写结构节点并重新验证。
140
140
 
@@ -237,7 +237,7 @@ description: "Render structured interactive UI inline through the dsh-ui fence.
237
237
 
238
238
  1. **围栏放哪,组件就出现在哪** —— 文字在前后自然流动,不要用工具、不要解释"这是一个围栏"。**围栏一闭合就立即渲染**(不等整条回答结束),所以可以边写文字边出组件
239
239
  2. **组合优先**:复杂界面用 `grid`+`card`+`stat`+`table` 拼,不要追求单一巨型组件
240
- 3. **JSON 必须严格合法,发出前完成 4 步自检**:插件**只**修标点级小错(字符串内半角引号、尾随逗号);**缺括号/错括号等结构错误一律不修**,直接红横幅退化成代码块——写错就重发,别指望兜底。**最容易犯的错:字符串值里用了半角引号 `"`**——中文引语一律写 `“”` 或 `「」`。发出围栏前自检 4 条:① 括号配对:`{` 与 `}`、`[` 与 `]` 数量相等,**收尾序列逐个核对**(长表格最易在最后几行错位:把 `]]}]}` 写成 `]}]}]}`)② 无尾随逗号 ③ 值内引号用中文引号 ④ 最后一个字符必须是 `}`。不要在 JSON 字符串里放 markdown;超长表格/列表拆成多个组件分开发,宁短勿长
240
+ 3. **JSON 必须严格合法,发出前完成 4 步自检**:插件**只**修标点级小错(字符串内半角引号、尾随逗号);**缺括号/错括号等结构错误一律不修**,直接红横幅退化成代码块——写错就重发,别指望兜底。**最容易犯的错:字符串值里用了半角引号 `"`**——中文引语一律写 `“”` 或 `「」`。发出围栏前自检 4 条:① 括号配对:`{` 与 `}`、`[` 与 `]` 数量相等,**收尾序列逐个核对**(长表格最易在最后几行错位:把 `]]}]}` 写成 `]}]}]}`)② 无尾随逗号 ③ 值内引号用中文引号 ④ 最后一个字符必须是 `}`(**`}` 之后不要再写任何字符**——实测常见错误是习惯性追加 `</p>` 或一句解释,整条围栏就解析不了)。不要在 JSON 字符串里放 markdown;超长表格/列表拆成多个组件分开发,宁短勿长
241
241
  3.5. **字段名逐个核对(写错一个 = 整条围栏降级为代码块)**:某个组件的必填字段写错 / 缺失 → 该组件被丢弃 → 整份 spec 判定不可渲染 → 用户只看到一段裸 JSON。高频误写:`callout` 正文是 `content`(不是 text/body);`table` 要 `columns` + `rows`(不是 data,只给二维 rows 时首行会当表头);`keyvalue` 要 `pairs:[{key,value}]`(不是 items);`diff` 是 `diffs`;图片/音视频是 `src`;`code` 是 `code`、`copy` 是 `text`。拿不准就调 `validate_dsh_ui`,不要凭直觉命名
242
242
  4. **不要嵌套围栏**:dsh-ui 里不要再包 ``` 代码围栏
243
243
  5. **深色主题友好**:配色选深底亮色;UI 主题跟随应用
@@ -246,4 +246,16 @@ description: "Render structured interactive UI inline through the dsh-ui fence.
246
246
  8. **规格要紧凑**:整棵组件树 ≤200 节点、≤8 层嵌套(超出部分会被渲染器裁掉),避免巨型 spec
247
247
  9. **一个主题选一个主组件**:命中映射表后选**一种**组件承载,同一信息不要用两种组件重复表达(同一批数据又画 bars 又画 donut = 冗余)
248
248
  10. **数量纪律**:一条回答 3–8 个组件为宜,宁缺毋滥。反例:该用 `table` 对比时写三段 `text`;一个 `stat` 能说清的事套 `card`+`grid`;与内容无关的 `scene3d` 炫技——3D 只在内容本身就是几何/空间时才用
249
+ 12. **给命令就给能直接粘的**:多行 python 一律写成 heredoc 包装的**一整段** shell 代码块(`python - <<'PY'` … `PY`),不要用 `python -c "…"` 配反斜杠续行——续行在复制/粘贴里最容易碎成多行,用户还得自己拼回 heredoc。单行表达式才用 `-c`,且必须真的在一行内写完。示例:
250
+
251
+ ````text
252
+ cd /path/to/repo
253
+ python - <<'PY'
254
+ import json
255
+ d = json.load(open('report.json'))
256
+ print(d.get('status'))
257
+ PY
258
+ ````
259
+
260
+ 同理:需要用户执行的脚本放进 `code` 节点(`lang`: `bash`/`python`)或正文围栏,并保证**从第一行到最后一行一次粘进终端就能跑**。
249
261
  11. **先验后发(复杂 UI)**:发出 ```dsh-ui 围栏前,若 spec ≥3 个组件或含 `table`(长表格最易括号错位),先调用 `validate_dsh_ui` 工具(参数 `spec` 传围栏内的 JSON 文本)验证;返回 `next=fix_and_revalidate` 就按诊断修正后重新验证;返回 `next=emit_fence` 再发出;返回 `next=emit_repaired_fence` 时直接照抄 `repaired_json` 发出,无需再次验证;简单 UI(≤2 个组件)不必验证,渲染器会自动修复大部分标点/括号错误