Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,30 @@
# Changelog

## [0.9.0] - 2026-08-17
### 新增
- **ECharts 集成组件**:新增 `echart` 节点类型,支持模型输出完整 ECharts 图表。两种模式:
- **Preset 简写**(`preset` + `data`/`series`):与 `chart` 节点同构的数据格式,模型只需改 `type` 为 `echart` 并加 `preset`(`bar`/`line`/`area`/`pie`/`scatter`)即可升级到 ECharts 渲染;
- **Full option 逃生舱**(`option` 字段):直传 ECharts `EChartsCoreOption`,支持 dataZoom、visualMap 等高级特性。
- ECharts 引擎按需懒加载(`lib/assets/echarts.js`,~1MB),不进主 client bundle。
### 安全
- **full option XSS 防护**:`sanitizeEChartOption` 强制覆盖 `tooltip.renderMode: 'richText'`(ECharts 默认 `'html'` 模式经 `innerHTML` 写入 tooltip,是模型输出的 XSS 向量);同时过滤所有字符串中的 HTML/脚本注入模式(`<script`、`on[a-z]+=`、`javascript:`、`<img` 等)与 `url()` 外带通道。
- **full option 资源预算**:新增 `maxEChartArrayLen: 500`(单数组上限)与 `maxEChartOptionNodes: 2000`(总遍历条目预算),防止模型输出几十万数据点卡住渲染。
### 修复
- **preset `scatter` 数据映射**:xAxis 从 `type: 'value'`(非数字 label 如「一月」画不出来)改为 `type: 'category'`,data 映射从 `[label, value]` 改为 `value`。
- **懒加载期间 spec 更新丢失**:update effect 依赖加入 `status`,引擎加载完成(`loading → ready`)时重新应用最新 option,避免流式渲染中 spec 变更被旧 option 覆盖。
- **preset tooltip `renderMode`**:所有 preset 路径的 tooltip 统一设为 `richText`,与 full option 安全姿态对齐。
- **preset `series[].color` 对齐**:bar/line/area preset 现在尊重 `series[].color`(与 `chart` 节点行为一致)。
### 无障碍
- ECharts 画布容器添加 `role="img"` 与 `aria-label`(与 PlotBlock 对齐)。
### 可观测性
- ECharts 渲染失败时 `console.warn` 输出诊断信息(asset 404 / 引擎注册失败 / option 异常),遵循「静默失败必须可观测」约定。
### 构建
- `tsdown.config.ts` 的 `assetConfig` 签名放宽为 `'mermaid' | 'three' | 'echarts'`;`asset-loader.ts` 的 `@param` 注释同步三资产。
- `scripts/verify-pack.mjs` 必查列表新增 `lib/assets/echarts.js`(issue #15 发布规范)。
### 测试
- 新增 `tests/genui-echart-guard.spec.ts`:preset 白名单、height 100–800、option 深度/数组/节点预算、函数/url()/HTML 过滤、非法节点拒绝。
- 新增 `tests/genui-echart.spec.tsx`:preset 五种形态渲染容器、error fallback、option 优先于 preset、标题与高度、scatter 中文 label。

## [Unreleased]
### 兼容性
- **dsh 0.1.0-rc.8**:对齐全部宿主 peer 依赖并补齐实际使用的 conversation、input-trigger、session 直接声明;改用 ui-tool 的公开客户端入口,测试和构建不再读取本机旧源码快照。`tsc`、`tsdown`、Vitest 全通过(316 passed / 104 skipped,0 失败)。
Expand Down
20 changes: 17 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ The following is the detailed capability reference. Every behavior is constraine
- **Answer-as-UI**: components are embedded in the reply and appear as they stream — no waiting for the whole message
- **30+ components**: cards, tables, charts, forms, tabs, accordions, file trees, timelines, diffs…
- **Native media**: audio and video play inline from browser-reachable http(s) or same-origin relative URLs, with user-controlled playback, video posters/aspect ratios, and visible failure states
- **Function plots**: `plot` draws curves; parameter sliders redraw in real time, with optional auto-animation
- **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

- **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)
- **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`)
Expand Down Expand Up @@ -180,19 +180,33 @@ The model outputs this fence (written for the browser — you don't need to read

What you see: two stat cards.

### ECharts example

```dsh-ui
{"title":"Q1 Revenue","items":[
{"type":"echart","title":"Monthly Revenue","preset":"bar","data":[
{"label":"Jan","value":98},
{"label":"Feb","value":112},
{"label":"Mar","value":128}
]}
]}
```

What you see: a themed bar chart with tooltips and axis labels — rendered by ECharts, lazy-loaded on demand.

## 🔧 How it works

The model writes the interface description as JSON inside a `dsh-ui` fence; the browser-side renderer (`src/client`) claims this language through the main repo's `fence-registry` interface and renders it. Components are whitelisted — the model can't smuggle in HTML/scripts; function expressions go through a standalone parser, never `eval`.

The core render package stays light (≈110 KB min / 28 KB gzip); the mermaid and three.js engines are bundled separately as on-demand assets (loaded through the plugin's self-registered HTTP routes the first time they're used), so startup only downloads the rendering core.
The core render package stays light (≈110 KB min / 28 KB gzip); the mermaid, three.js, and echarts engines are bundled separately as on-demand assets (loaded through the plugin's self-registered HTTP routes the first time they're used), so startup only downloads the rendering core.

## ❓ FAQ

- **Rendering as a code block?** First check the browser console for `[genui] client active; fence-channel=registry|dom`. If absent, the client bundle was not activated even if its URL returns 200 — align the profile dependency, `package.json.name`, `cordis.patch.yml`, ModuleLoader id, and configured bundle name. If present, inspect the fence label/body; registry-less hosts automatically use the DOM channel.
- **Chat UI goes blank when rendering a dsh-ui fence?** Your dsh is too old — update dsh first, then reinstall the plugin.
- **`dsh: pnpm not found on PATH`?** Install pnpm, then **open a new terminal** and retry (`corepack enable` or `npm i -g pnpm`).
- **Stuck on git credentials / 404 during install?** The repo is public (`omdsh-dev/dsh-genui`) — the git URL above needs no login; a 404 for `@omdsh-dev/dsh-genui` means the npm package has not been published yet.
- **Installed but scene3d/mermaid don't render?** The engines (mermaid / three) are no longer inlined in client.js — they load on demand the first time they're used (`/plugins/@omdsh-dev/dsh-genui/assets/*.js`, hosted by the plugin's own HTTP routes). First restart dsh web + hard refresh (Cmd+Shift+R); still broken, remove and reinstall (`dsh plugin --profile web remove @omdsh-dev/dsh-genui`, then add again). Hosts without the asset routes degrade to source/load-error hints — update dsh.
- **Installed but scene3d/mermaid/echarts don't render?** The engines (mermaid / three / echarts) are no longer inlined in client.js — they load on demand the first time they're used (`/plugins/@omdsh-dev/dsh-genui/assets/*.js`, hosted by the plugin's own HTTP routes). First restart dsh web + hard refresh (Cmd+Shift+R); still broken, remove and reinstall (`dsh plugin --profile web remove @omdsh-dev/dsh-genui`, then add again). Hosts without the asset routes degrade to source/load-error hints — update dsh.
- **Model not outputting fences?** New sessions pick it up after a restart; or just say "output it with dsh-ui".
- **No lib/ after cloning?** Build it yourself: `pnpm install && pnpm run check`.

Expand Down
20 changes: 17 additions & 3 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ dsh plugin --profile web add link:$PWD
- **回答即界面**:组件嵌在回答里,边生成边出现,不用等整段写完
- **30+ 组件**:卡片、表格、图表、表单、标签页、折叠面板、文件树、时间线、diff……
- **原生音视频**:浏览器可访问的 http(s) 或同源相对地址直接嵌入回答;用户主动控制播放,视频支持封面与画面比例,失败时原位提示
- **函数图**:`plot` 画曲线,参数滑块拖动实时重绘,支持自动动画
- **ECharts 集成**:`echart` 节点渲染完整的 ECharts 图表,自动适配主题色、提示框和图例。两种模式:**预设简写**(`preset: 'bar' | 'line' | 'area' | 'pie' | 'scatter'` + `data`/`series`)可从 `chart` 节点快速升级;**完整选项**(`option` 字段)支持自定义图表类型、dataZoom、visualMap 等高级 ECharts 功能。echarts 引擎(~1 MB)按需懒加载——主包不含引擎,没有 `echart` 节点的对话不会下载它- **函数图**:`plot` 画曲线,参数滑块拖动实时重绘,支持自动动画

- **测验**:`quiz` 点选判题 + 解析 + 重试;带 `action` 时答案同时回传模型(判题仍本地即时)
- **本地判卷(交卷)**:多道选择题 = 每题的 `radio` 加 `group` + `answer`(正确答案)+ `explanation`(解析),再加一个 `submit` 交卷按钮——用户全部选完点一次,**分数、每题对错、解析当场在 UI 里出现,零模型往返**;题目随即锁定,「重新作答」本地重置(可选 `resetAction` 通知模型)。题目没带答案时才退回聚合 action(`fields` 收集所有带 `id` 的输入)
Expand Down Expand Up @@ -180,19 +180,33 @@ dsh plugin --profile web add link:$PWD

你看到的是两张统计卡片。

### ECharts 示例

```dsh-ui
{"title":"Q1 收入","items":[
{"type":"echart","title":"月度收入","preset":"bar","data":[
{"label":"1月","value":98},
{"label":"2月","value":112},
{"label":"3月","value":128}
]}
]}
```

你看到的是一张带提示框和坐标轴的主题色柱状图——由 ECharts 渲染,按需懒加载。

## 🔧 原理

模型把界面描述写成 JSON 放进 `dsh-ui` 围栏,浏览器端渲染器(`src/client`)通过主仓 `fence-registry` 接口认领这门语言并渲染。组件是白名单的,模型塞不进 HTML/脚本;函数表达式走独立解析器,不用 eval。

主渲染包保持轻量(≈110 KB min / 28 KB gzip),mermaidthree.js 引擎单独打包为按需资产(首次用到时经插件自注册的 HTTP 路由加载),启动时只下载渲染核心。
主渲染包保持轻量(≈110 KB min / 28 KB gzip),mermaidthree.js 与 echarts 引擎单独打包为按需资产(首次用到时经插件自注册的 HTTP 路由加载),启动时只下载渲染核心。

## ❓ 常见问题

- **显示成代码块?** 先在浏览器控制台找 `[genui] client active; fence-channel=registry|dom`。没有这行,即使 `client.js` 返回 200,也只是下载了文件、没有激活:请对齐网页配置依赖名、`package.json.name`、`cordis.patch.yml`、ModuleLoader id 和配置中的 bundle 名。出现这行后再查围栏标签/正文;宿主没有 registry 时会自动走 DOM 通道。
- **渲染 dsh-ui fence 时聊天界面白屏?** dsh 版本太旧——先更新 dsh 再重装插件。
- **`dsh: pnpm not found on PATH`?** 装 pnpm 后**新开终端**再试(`corepack enable` 或 `npm i -g pnpm`)。
- **安装时卡在 git 凭据/404?** 仓库是公开的(`omdsh-dev/dsh-genui`),上面的 git URL 无需登录;`@omdsh-dev/dsh-genui` 返回 404,表示 npm 包尚未发布。
- **装了但 scene3d/mermaid 不渲染?** 引擎(mermaid / three)不再内联进 client.js——它们在首次用到时按需加载(`/plugins/@omdsh-dev/dsh-genui/assets/*.js`,插件自带 HTTP 路由托管)。先重启 dsh web + 硬刷新(Cmd+Shift+R);仍不渲染就卸掉重装(`dsh plugin --profile web remove @omdsh-dev/dsh-genui` 后再 add)。旧版宿主缺少资产路由时会降级显示源码/加载失败提示,更新 dsh 即可。
- **装了但 scene3d/mermaid/echarts 不渲染?** 引擎(mermaid / three / echarts)不再内联进 client.js——它们在首次用到时按需加载(`/plugins/@omdsh-dev/dsh-genui/assets/*.js`,插件自带 HTTP 路由托管)。先重启 dsh web + 硬刷新(Cmd+Shift+R);仍不渲染就卸掉重装(`dsh plugin --profile web remove @omdsh-dev/dsh-genui` 后再 add)。旧版宿主缺少资产路由时会降级显示源码/加载失败提示,更新 dsh 即可。
- **模型不主动输出?** 重启后新会话生效;或直接说"用 dsh-ui 输出"。
- **clone 后没有 lib/?** `pnpm install && pnpm run check` 自己构建。

Expand Down
6 changes: 3 additions & 3 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ description: "Render structured interactive UI inline in your reply via the dsh-

布局:`text` `row` `col` `grid` `card` `divider` `spacer`
展示:`stat` `badge` `progress` `list` `table` `keyvalue` `avatar` `audio` `video` `timeline` `file-tree` `breadcrumb` `diff` `json` `code` `callout` `steps`
图表:`chart`(bars/line/donut,可多序列)`plot`(数学函数图)
交互:`button` `input` `select` `checkbox` `radio` `switch` `textarea` `tabs` `accordion` `copy`
图表:`chart`(bars/line/donut,可多序列)`plot`(数学函数图)`echart`(ECharts 全功能图表)交互:`button` `input` `select` `checkbox` `radio` `switch` `textarea` `tabs` `accordion` `copy`
高级:`mermaid`(流程图/时序/甘特等)`scene3d`(3D WebGL)`quiz`(点选判题 + 解析 + 重试)

### 布局
Expand Down Expand Up @@ -48,6 +47,7 @@ description: "Render structured interactive UI inline in your reply via the dsh-
### 图表
- chart: `{"type":"chart","kind":"bars|line|donut","data":[{"label":"...","value":n,"color":"#hex?"}],"series":[...]?}` — bars 默认;line 趋势;donut 占比;series 字段 = 分组柱状图;负值数据:柱高为 0 但数值标注照显、donut 负值记 0 弧长(line 正常画负区间)
- plot: `{"type":"plot","series":[{"expr":"a*sin(b*x)","label":"...","color":"#hex?","params":[{"name":"a","value":1,"min":0,"max":5,"animateTo":3,"durationMs":4000,"loop":true},{"name":"b","value":1,"min":0.5,"max":5}]}],"xMin":-6.28,"xMax":6.28,"title":"..."}` — SVG 函数图;**series 可带 `"kind":"line|area|scatter"`**(缺省 line;area 填色到基线;scatter 散点);**params 渲染成实时滑块**(拖动即时重绘,**y 轴锁定**=只变曲线不变数轴);**animateTo 参数会显示播放按钮**(自动动画演示);SVG 可拖拽平移、滚轮缩放;表达式支持 sin/cos/tan/asin/acos/atan/sqrt/cbrt/exp/log/ln/abs/floor/ceil/round/min/max/pow,常量 pi/e/tau,变量 x(其他字母=参数)
- echart: `{"type":"echart","title":"...","height":300,"preset":"bar|line|area|pie|scatter","data":[{"label":"...","value":n}],"series":[...]?}` — **ECharts 全功能图表**,视觉效果远超 `chart`(渐变、tooltip、动画、图例交互);**preset 模式**:用和 `chart` 一样的 `data`/`series` 格式,自动构建主题化的 ECharts 配置(颜色跟随宿主主题);**full option 模式**:传 `"option":{...}` 直接写 ECharts 原生配置(支持 dataZoom/visualMap/radar/gauge/heatmap 等所有图表类型),option 中的函数会被过滤(只接受数据);推荐用 echart 替代 chart 获得更好视觉效果

### 交互
**本地优先(v2.6)**:UI 自己能做的状态变化——判卷、判题、重置、展开、选中——一律本地即时完成,**零模型往返**。action 只用于必须模型参与的事(生成新内容、执行工具、下一步建议)。**交互组件必须带 action:不带 action 的按钮渲染为禁用态,用户点不了;带 action 的按钮点击后有「已触发」本地反馈。**
Expand Down Expand Up @@ -83,7 +83,7 @@ description: "Render structured interactive UI inline in your reply via the dsh-
|---|---|
| 关键结论 / 要点罗列(≥2 条) | `list`、`keyvalue`、`callout` |
| 重点强调 / 警告 / 注意事项 | `callout`(info/success/warning/error)、`badge`、`stat` |
| 数据对比 / 趋势 / 占比 | `chart`(bars/line/donut)、`table` |
| 数据对比 / 趋势 / 占比 | `chart`(bars/line/donut)、`echart`(ECharts 全功能)、`table` |
| 关键指标数字 / 进度状态 | `stat`、`progress`、`badge` |
| 流程 / 步骤 / 阶段 / 时间线 | `steps`、`timeline`、`mermaid`(flowchart/sequence/gantt) |
| 目录 / 文件结构 / 层级关系 | `file-tree`、`mermaid`、`accordion` |
Expand Down
64 changes: 64 additions & 0 deletions lib/assets/echarts.js

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions lib/client.js

Large diffs are not rendered by default.

Loading
Loading