dsh-rich-msg 0.0.0-stage → 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 kyoukisu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,255 @@
1
- # Temporary Holding Version
1
+ # dsh-rich-msg
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 一个独立、无模型行为倾向的 **DSH 富内容消息渲染引擎**(Rich Message Renderer)。
4
+
5
+ 它把结构化 JSON 协议渲染成美观、可交互、跟随宿主主题的消息内容,并提供两条互不耦合的输出通道:
6
+
7
+ | 通道 | 数据来源 | 最终展示位置 |
8
+ |---|---|---|
9
+ | **A. Markdown Fence** | 最终 Assistant 回答正文中的 ```rich-msg 代码围栏 | 原位替换该代码块 |
10
+ | **B. Tool Bridge** | 工具调用 `rich_msg_publish` 暂存的结构化内容 + 最终回答中的 `ref` 引用 | 最终回答中引用所在的位置 |
11
+
12
+ 两条通道共用同一套 Schema、校验器、React Renderer 与主题适配层。
13
+
14
+ 本项目是**纯技术渲染层**:它不注入 system prompt / developer prompt / persona / skill,不规定模型何时使用富内容,也不改变模型回答风格。是否使用、何时使用、如何组织内容,完全由使用者自己的 Persona、Prompt 或上层应用决定。
15
+
16
+ ---
17
+
18
+ ## 设计目标
19
+
20
+ 1. **协议先行**:先定义并校验数据结构,再渲染;非法数据永不执行、永不修补。
21
+ 2. **双通道、单渲染器**:Fence 与 Tool 两种入口最终都落到同一个 `RichResponse`。
22
+ 3. **只渲染最终回答**:推理过程、工具调用阶段、中间步骤中的内容不会被当成最终 UI 展示。
23
+ 4. **宿主优先的视觉**:组件保留自己的布局语言,颜色、字体、背景、边框、圆角、阴影全部取自 DSH 主题变量;明暗主题与移动端开箱即用。
24
+ 5. **安全默认**:声明式 JSON、无 HTML/JS 执行、无 eval、URL 白名单、Action 白名单、大小/深度/数量限制、Session 隔离。
25
+ 6. **可卸载、可重挂载**:所有 React root、样式、监听器、路由、缓存都有明确的生命周期。
26
+
27
+ ## 它不负责什么
28
+
29
+ - 不决定模型应该如何思考、回答、教学或组织内容。
30
+ - 不注册任何 prompt 段落、Persona、Skill 或 Agent 预设。
31
+ - 不通过工具描述夹带 UI 使用策略。
32
+ - 不替使用者选择模型、语气或教学策略。
33
+
34
+ ---
35
+
36
+ ## 架构
37
+
38
+ ```
39
+ dsh-rich-msg
40
+ ├── Host Integration(Node / Cordis 插件)
41
+ │ ├── lib/index.js 插件入口:注册工具、装配 Artifact Store、可选 HTTP 读取路由
42
+ │ ├── Artifact Store 会话作用域、带 TTL / 容量上限 / 淘汰策略的内存存储
43
+ │ └── Final Message Bridge 由「工具暂存 + 最终回答引用」构成的两段式桥接
44
+ │
45
+ ├── Protocol(框架无关)
46
+ │ ├── core/schema.ts RichSpec v1 定义与严格校验
47
+ │ ├── core/evaluator.ts 安全表达式求值(无 eval)
48
+ │ ├── core/partial.ts 流式前缀解析(只呈现已闭合且合法的部分)
49
+ │ └── shared/protocol.ts Fence 语言、ref 协议、Action 信封、DOM 标记、上限
50
+ │
51
+ ├── Renderer(React)
52
+ │ ├── ui/RichResponse.tsx 节点调度、交互状态、持久化
53
+ │ ├── ui/blocks/* Chart / Table / Metrics / Media / Layers / Code / Field
54
+ │ ├── ui/EntityList.tsx 实体列表与详情
55
+ │ └── ui/primitives/* Popover / Tooltip / Lightbox / Gallery 等基础件
56
+ │
57
+ ├── Client Integration(浏览器)
58
+ │ ├── client/dom-renderer.tsx Fence 发现、最终消息判定、挂载/回滚
59
+ │ ├── client/chat-view.ts 读取 Chat Snapshot:最终性、推理排除、Artifact 索引
60
+ │ ├── client/artifact-resolver.ts ref 解析(会话日志优先,HTTP 路由兜底)
61
+ │ ├── client/actions.ts 交互事件 → 用户消息
62
+ │ └── client/entity-tab.tsx 可选:右侧栏实体详情页
63
+ │
64
+ ├── Theme
65
+ │ └── client/host-theme.css DSH 主题变量 → 组件 token(明/暗/移动端)
66
+ │
67
+ └── Tests / scripts
68
+ ```
69
+
70
+ 详细设计见 [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)。
71
+
72
+ ---
73
+
74
+ ## 两条输出通道
75
+
76
+ ### A. Markdown Fence
77
+
78
+ 最终 Assistant 回答中:
79
+
80
+ ````markdown
81
+ ```rich-msg
82
+ {"version":1,"nodes":[{"type":"text","content":"Hello"},{"type":"stat","label":"Score","value":42}]}
83
+ ```
84
+ ````
85
+
86
+ 渲染器会:
87
+
88
+ 1. 只处理 **最终回答** 的 `assistant-step` 行,并用 Chat Snapshot 校验:该步骤是整轮对话的收尾回答,且其后没有工具调用;
89
+ 2. 通过 Snapshot 中该步骤的内容块区分 **正文** 与 **推理**:推理块中的围栏永不渲染;
90
+ 3. 校验 JSON;非法内容保持原生代码块(流式中)或显示诊断卡片(已稳定);
91
+ 4. 在原位置挂载独立的 React root 并隐藏源码块,卸载时完整还原。
92
+
93
+ `rich-ui` 作为上游兼容别名同样被接受。普通代码块、其他语言、工具行内容完全不受影响。
94
+
95
+ ### B. Tool Bridge
96
+
97
+ 工具调用负责**准备**内容,最终回答负责**决定是否呈现**:
98
+
99
+ ```
100
+ 模型调用 rich_msg_publish({document})
101
+ → Host 校验、存入会话作用域 Artifact Store
102
+ → 返回 {ok:true, id:"rm_…", kind, summary}
103
+ 模型在最终回答中写:
104
+ ```rich-msg
105
+ {"version":1,"ref":"rm_…"}
106
+ ```
107
+ → 浏览器从会话日志中的工具调用还原文档(主路径)
108
+ → 或读取 Host 的只读路由(兜底)
109
+ → 在最终回答的引用位置渲染
110
+ ```
111
+
112
+ 如果最终回答没有引用该 id,内容不会被显示;工具阶段本身不渲染任何富内容。
113
+
114
+ 详细数据流与验证方法见 [docs/TOOL-BRIDGE.md](docs/TOOL-BRIDGE.md)。
115
+
116
+ ---
117
+
118
+ ## 支持的能力
119
+
120
+ 组件与字段的完整清单见 [docs/COMPONENTS.md](docs/COMPONENTS.md),数据协议见 [docs/PROTOCOL.md](docs/PROTOCOL.md)。
121
+
122
+ - **布局**:`stack` / `row` / `grid` / `card` / `carousel` / `divider` / `meta`
123
+ - **文本**:`text`(行内 code/bold/链接/实体 chip)、`callout`、`code`
124
+ - **对象**:`entity-list` / `entity-ref` / 实体详情面板 / 图库 / 图片灯箱 / `image` / `image-group` / `gallery`
125
+ - **数据**:`stat` / `progress` / `status` / `table`(排序、筛选、状态色、进度列)/ `chart`(bar/line/area/donut/scatter)/ `computed`
126
+ - **流程**:`diagram` / `layers`
127
+ - **媒体**:`image` / `video` / `audio` / `popover` / `tooltip`
128
+ - **控件**:`input` / `select` / `slider` / `switch` / `checkbox` / `radio` / `segmented` / `tabs`
129
+ - **逻辑**:`expression` / `conditional` / 本地 Action(set/toggle/reset/open-entity/close-entity/copy/open-url/send-message)
130
+
131
+ ## 工具接口
132
+
133
+ | 工具 | 输入 | 输出 | 说明 |
134
+ |---|---|---|---|
135
+ | `rich_msg_validate` | `{document: string}` | `{valid, error?, title?, nodes?, entities?}` | 只做 Schema 校验,无副作用 |
136
+ | `rich_msg_publish` | `{document: string}` | `{ok, id?, kind?, summary?, error?}` | 校验并存入当前会话的 Artifact Store,返回稳定引用 id |
137
+
138
+ 工具描述只说明输入、输出与副作用,不包含任何"何时应该使用"的行为指导。
139
+
140
+ ## 安装
141
+
142
+ ### 方式一:本地 link(开发)
143
+
144
+ ```powershell
145
+ # 1) 构建
146
+ node scripts/build.mjs
147
+
148
+ # 2) 在 %USERPROFILE%.dshprofiles<name>package.json 中声明(示例)
149
+ # {
150
+ # "name": "dsh-profile-richmsg",
151
+ # "private": true,
152
+ # "dependencies": { "dsh-rich-msg": "link:E:/path/to/dsh-rich-msg" },
153
+ # "dsh": { "profile": { "bundles": ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app", "dsh-rich-msg"] } }
154
+ # }
155
+ # 并在该 profile 的 node_modules 下建立指向本目录的 junction/symlink。
156
+
157
+ # 3) 启动该 profile
158
+ node <dsh>/lib/bin.js --profile <name> --host 127.0.0.1 --port 58470 --no-open
159
+ ```
160
+
161
+ ### 方式二:作为依赖安装
162
+
163
+ 把本包发布/复制到可解析的位置后,在 profile 的 `package.json` 中:
164
+
165
+ ```json
166
+ {
167
+ "dependencies": { "dsh-rich-msg": "link:E:/path/to/dsh-rich-msg" },
168
+ "dsh": { "profile": { "bundles": ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app", "dsh-rich-msg"] } }
169
+ }
170
+ ```
171
+
172
+ ```powershell
173
+ dsh plugin --profile <profile> add link:E:/path/to/dsh-rich-msg
174
+ # 并把 "dsh-rich-msg" 加入 dsh.profile.bundles
175
+ ```
176
+
177
+ > 新增/替换包后需要重启宿主进程;客户端资源会按 boot revision 重新生成。
178
+
179
+ ## 主题
180
+
181
+ - 全部颜色/字体/圆角/阴影来自 DSH 的 `--dsw-*` 令牌;
182
+ - 同时支持浅色与深色主题(跟随 `data-ds-dark-theme` 与上游 `data-theme="dark"`);
183
+ - 组件不覆盖宿主消息气泡、侧边栏或其它插件的布局;
184
+ - 移动端 390px 宽度下保持可读。
185
+
186
+ 详见 [docs/THEME.md](docs/THEME.md)。
187
+
188
+ ## 安全
189
+
190
+ - 只接受声明式 JSON;
191
+ - 拒绝 `__proto__` / `constructor` / `prototype` 键,防原型污染;
192
+ - 文档大小、嵌套深度、数组长度、组件数量、字符串长度全部有上限;
193
+ - URL 仅允许 http(s),禁止内嵌凭据与危险协议;
194
+ - Action 事件白名单校验;交互只由用户触发;
195
+ - 富内容不能直接调用任意 DSH 工具;
196
+ - Artifact 按 Session 隔离,跨 Session 查询与不存在等价;
197
+ - 日志不写入文档内容或表单值。
198
+
199
+ 详见 [docs/SECURITY.md](docs/SECURITY.md)。
200
+
201
+ ## 兼容性与已验证范围
202
+
203
+ - 目标宿主:`@deepseek-ai/dsh@0.2.2-alpha.2`(本机 `E:\tools\dsh-cli`)。
204
+ - Fence 发现依赖宿主 DOM 标记(`data-chat-flow-kind`、`data-code-block-content` 等)。宿主升级后需重新验证;标记变化时渲染器**失败关闭**(保留原生代码块)。
205
+ - 最终消息判定依赖 Chat Snapshot(`uiConversation`)。该服务不可用时退化为保守的 DOM 规则(仅已稳定的 assistant 行,且不预览流式内容),并在文档中明确记录这一降级。
206
+ - **已验证**:13 个测试文件 / 210 项单元与集成测试通过;`tsc --noEmit` 0 错误;隔离 profile 下真实 DSH 宿主加载与 DOM 冒烟验证通过(插件加载、模块分发、Fence 渲染、普通代码/工具行/流式行不接管、卸载还原、零控制台错误、浅色/深色/390px 截图)。
207
+ - **未验证(待人工)**:真实模型对话下的 Fence 通道与 Tool Bridge 引用渲染。自动化 CDP 发送消息未打通,已按交付要求停止;请按 [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) 第 5.3 节清单在真实 DSH 中人工验证。
208
+
209
+ ## 上游项目与许可证
210
+
211
+ - 本项目基于 **kyoukisu/dsh-rich-ui**(MIT)的源码进行独立开发与重构:保留了其成熟的 RichSpec Schema、表达式求值器、React 组件与视觉实现,重写了宿主集成、消息判定、工具桥接、主题桥接与文档,并移除了运行时 Prompt 注入。
212
+ - 上游版权声明保留在 [LICENSE](LICENSE)(MIT, Copyright (c) 2026 kyoukisu)。
213
+ - 图标来自 [lucide-react](https://lucide.dev)(ISC),详见 [docs/THIRD-PARTY.md](docs/THIRD-PARTY.md)。
214
+ - 本项目与上游作者无隶属、合作或背书关系;名称 `dsh-rich-msg` 用于区分衍生项目。
215
+
216
+ ## 变更报告
217
+
218
+ | 模块 | 上游原有能力 | 本次修改 | 修改原因 |
219
+ |---|---|---|---|
220
+ | Prompt | 通过 `inject: ["systemPrompt"]` 注册 `rich-ui:fence` 段落,构建行为指令并注入每次请求 | **完全移除**运行时注入;删除 `prompt-rich-ui/` 运行时文件;技术内容改为离线文档 | 保持模型行为中立 |
221
+ | Validator | `rich_ui_validate` 工具,描述含"默认使用/优先"等行为指导 | 保留校验能力,改名为 `rich_msg_validate`,描述仅含参数、返回与副作用 | 工具 API 保持纯技术性 |
222
+ | Tool | 仅校验,无状态 | 新增 `rich_msg_publish` + 会话作用域 Artifact Store + 只读读取路由 | 支持 Tool 输出通道 |
223
+ | Fence | DOM MutationObserver 扫描 `rich-ui` 围栏,仅以 assistant-step 祖先判定 | 保留扫描技术;新增 Chat Snapshot 最终性判定、推理排除、流式回撤、引用解析、稳定重挂载 | 只渲染最终回答,不误渲染推理/工具阶段 |
224
+ | Renderer | RichSpec React 组件体系 | 复用全部组件;命名空间改为 `dsh-rich-msg`;保留既有可访问性修复 | 降低改造成本 |
225
+ | Theme | 自带配色 + 部分 DSH token 桥接 | 新增完整桥接层(明/暗/移动端),统一映射 `--dsw-*` | Hybrid 视觉要求 |
226
+ | Actions | `[rich-ui-action]` 用户消息 | 改为 `[rich-msg-action]`,保留白名单、大小上限、会话捕获 | 品牌统一与安全 |
227
+ | Build | Vite + tsc + 独立 demo | esbuild 双产物(Host ESM / Client ModuleLoader bundle),无需 Vite 运行时 | 简化构建、与宿主加载协议对齐 |
228
+ | Tests | vitest 单测 + Playwright 截图 | 保留并扩充为 13 文件 / 210 项单测与集成测试;新增真实宿主 DOM 冒烟脚本;真实模型 E2E 改为人工验证 | 可复现、不虚构验证 |
229
+ | Docs | 上游内部文档 + 运行时 prompt 文件 | 重写为 7 份技术文档,且明确不得成为运行时 prompt | 交付要求 |
230
+
231
+ 完整说明见 [docs/CHANGES.md](docs/CHANGES.md)。
232
+
233
+ ## 开发
234
+
235
+ ```powershell
236
+ node scripts/build.mjs # 构建 lib/index.js 与 lib/client.js
237
+ npx tsc --noEmit # 类型检查
238
+ npx vitest run # 单元 + 集成测试
239
+ node scripts/package.mjs # 打包 dist/*.zip + sha256
240
+ ```
241
+
242
+ 详见 [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md)。
243
+
244
+ ## 目录
245
+
246
+ ```
247
+ src/core 协议与求值
248
+ src/host Node 插件:工具、Artifact Store、读取路由
249
+ src/client 浏览器插件:Fence 渲染、消息判定、引用解析、交互回传
250
+ src/ui React Renderer 与样式
251
+ src/shared 双端共享协议常量
252
+ tests 单元与集成测试
253
+ scripts 构建与真实宿主验证脚本
254
+ docs 技术文档
255
+ ```
@@ -0,0 +1,8 @@
1
+ # dsh-rich-msg bundle layer.
2
+ #
3
+ # Inserts only the host plugin row. The plugin registers technical tools
4
+ # (validation and artifact publication) and an optional artifact read route.
5
+ # It registers NO prompt section, NO persona, NO skill and NO agent preset.
6
+ - insert:
7
+ - id: rich-msg
8
+ name: dsh-rich-msg
@@ -0,0 +1,126 @@
1
+ # ARCHITECTURE
2
+
3
+ 本文件说明 dsh-rich-msg 的分层、生命周期与关键设计取舍。所有结论基于本机安装的 `@deepseek-ai/dsh@0.2.2-alpha.2` 实际源码/类型与真实宿主验证,不是对 DSH 未来 API 的假设。
4
+
5
+ ## 1. 分层
6
+
7
+ ```
8
+ ┌──────────────────────────────────────────────────────────────┐
9
+ │ Host(Node ESM, lib/index.js) │
10
+ │ · tools.register(rich_msg_validate / rich_msg_publish) │
11
+ │ · ArtifactStore(会话作用域、TTL、容量、淘汰) │
12
+ │ · ctx.inject(['webServer']) → 只读 GET 路由(可选) │
13
+ └───────────────▲──────────────────────────────────────────────┘
14
+ │ session log(工具调用参数 + 工具结果)
15
+ ┌───────────────┴──────────────────────────────────────────────┐
16
+ │ Client(Browser ModuleLoader bundle, lib/client.js) │
17
+ │ · ChatView ← ctx.uiConversation.binding(session).target('chat') │
18
+ │ · FenceRenderer DOM 扫描 + 最终性判定 + React root 挂载 │
19
+ │ · ArtifactResolver 会话日志优先 / HTTP 路由兜底 │
20
+ │ · ActionRelay → ctx.sessions.scope(session).get('conversation').send │
21
+ └───────────────▲──────────────────────────────────────────────┘
22
+ │ 同一份 RichSpec / 同一套 React 组件
23
+ ┌───────────────┴──────────────────────────────────────────────┐
24
+ │ Renderer(React,框架无关于 Cordis) │
25
+ │ RichResponse → 布局/数据/媒体/流程/控件/逻辑 │
26
+ └──────────────────────────────────────────────────────────────┘
27
+ ```
28
+
29
+ 依赖方向严格单向:`host → shared ← client`,`client → ui → core`。`core` 不依赖 React,`ui` 不依赖 Cordis 或宿主服务。
30
+
31
+ ## 2. 宿主侧生命周期
32
+
33
+ - 插件形态:npm 包 + `package.json.dsh.bundle.patch` 指向 `cordis.patch.yml`,由 profile 的 `dsh.profile.bundles` 激活。
34
+ - `apply(ctx, config)` 只做三件事:
35
+ 1. `ctx.tools.register(...)` 注册两个工具;
36
+ 2. 创建 `ArtifactStore`;
37
+ 3. `ctx.inject(['webServer'], ...)` 可选注册只读路由。
38
+ - 所有资源登记在 `ctx.effect(() => disposeAll, 'dsh-rich-msg: host resources')` 中;fiber 卸载时逆序释放、清空 Artifact、移除路由。
39
+ - `apply` 同时返回 disposer,兼容把返回值当日志清理函数的宿主。
40
+
41
+ **不变量**:`apply` 不读取 `ctx.systemPrompt`、`ctx.persona`、`ctx.skill`、`ctx.agentPreset` 等任何模型行为服务。测试用 Proxy 记录属性访问并静态扫描源码双重保证(`tests/host/neutrality.test.ts`)。
42
+
43
+ ## 3. 客户端侧生命周期
44
+
45
+ `apply(ctx)`:
46
+
47
+ 1. 构造 `ChatView`(读取 Chat Snapshot)与 `ArtifactResolver`、`ActionRelay`;
48
+ 2. 通过 `ctx.inject(['uiConversation'])` 延迟绑定 Snapshot 服务(服务不存在时自动降级);
49
+ 3. 通过 `ctx.inject(['slots'])` 可选安装右侧栏实体详情页;
50
+ 4. 安装一次性样式;安装 Fence 渲染器;
51
+ 5. 全部登记进 `ctx.effect(() => dispose, 'dsh-rich-msg: browser resources')`。
52
+
53
+ 卸载时:断开 MutationObserver、取消 Snapshot 订阅、unmount 所有 React root、恢复所有被隐藏的源码块、移除 `<style>`、清理缓存。卸载路径由单测与真实宿主验证覆盖。
54
+
55
+ ## 4. Fence 渲染(通道 A)
56
+
57
+ ### 4.1 为什么是 DOM 适配器
58
+
59
+ 已核实的 DSH 客户端扩展点:
60
+
61
+ | 扩展点 | 能力 | 结论 |
62
+ |---|---|---|
63
+ | `conversation.chat.node`(keyed by ChatNodeKind) | 可整体替换 `assistant-step` 渲染器 | 会重写推理折叠、文本选择、流式稳定性等宿主行为,侵入过大 |
64
+ | `conversation.chat.reasoning.body` | 仅推理正文 | 与正文无关 |
65
+ | `slots` / `uiRenderer` | 通用 Slot 体系 | **没有** fenced-code 渲染器注册表 |
66
+ | `MarkdownText`(ui-primitives) | Markdown 渲染 | 只暴露链接/图片/文件委托,没有代码块扩展点 |
67
+ | DOM | 宿主真实输出 | 官方 genui 类插件的既有技术,可失败关闭 |
68
+
69
+ 因此沿用"只读 DOM 适配 + 相邻挂载"的技术,但用 Chat Snapshot 补齐上游缺失的**最终消息判定**。
70
+
71
+ ### 4.2 判定流程
72
+
73
+ ```
74
+ 候选代码块(.md-code-block / .code-block / [data-code-block-content])
75
+ ├─ 在 [data-rich-msg-root] 内? → 跳过
76
+ ├─ 最近的 [data-chat-flow-kind] 不是 assistant-step? → 跳过
77
+ ├─ 语言标签:显式非 rich-msg/rich-ui → 跳过
78
+ │ 通用标签(宿主把未知语言本地化)→ 仅接受严格 payload 特征
79
+ │ (version:1 + nodes 数组,或合法 ref 文档)
80
+ ├─ Chat Snapshot 可用?
81
+ │ ├─ 该行 node.kind === 'assistant-step'?
82
+ │ ├─ 围栏文本属于 text 块(不是 reasoning 块)?
83
+ │ ├─ finality:该 assistant 步骤是整轮最后一个且其后无 tool-call?
84
+ │ │ running → streaming-final(可预览)
85
+ │ │ settled → final
86
+ │ │ 否则 → not-final(不渲染)
87
+ │ └─ 若 snapshot 不可用 → 保守降级:仅已稳定行 + 不预览流式
88
+ └─ 通过 → 校验/挂载;任一环节失败 → 保留宿主原生代码块
89
+ ```
90
+
91
+ **关键点**:`assistant-step` 在 DSH 中既包含推理过程,也包含工具调用前后的中间叙述,所以"祖先匹配"不能证明它是最终回答。Snapshot 判定解决了这一点,并且当模型在后续步骤调用工具时,已挂载的流式预览会被**自动回撤**(恢复到原生代码块)。
92
+
93
+ ### 4.3 挂载与重挂载
94
+
95
+ - 每个源码块对应一个独立 `createRoot` 容器,容器插在源码块之后并设置 `hidden` 隐藏源块;
96
+ - 复用条件:raw 文本、会话、streaming 状态、行 owner、模式(spec/ref/diagnostic)全部一致,避免每个 token 重建整棵树;
97
+ - 宿主 reconcillation 可能移动节点:每次扫描校验 `source.nextSibling === container`,需要时重新插入;
98
+ - 渲染异常由 `RenderBoundary` 捕获:隐藏容器、恢复源码块,不产生无限重试;
99
+ - 卸载:逐个 unmount、移除容器、恢复 `source.hidden`。
100
+
101
+ ### 4.4 持久化
102
+
103
+ 控件状态使用 `sessionStorage`,key 由 `[插件, 会话 id, 行锚点, 块序号, ref]` 组成;只持久化 slider/switch/select/tabs 等声明式控件,输入框、日期、Action payload 一律不写。存储失败静默禁用。
104
+
105
+ ## 5. 工具桥接(通道 B)
106
+
107
+ 见 [TOOL-BRIDGE.md](TOOL-BRIDGE.md)。
108
+
109
+ ## 6. 为什么不注册 tool view
110
+
111
+ DSH 提供 `tool.call.toolview` 按工具名键控的渲染槽。刻意**不**使用它来渲染富内容:工具阶段的内容不应冒充最终回答。工具行保持宿主默认呈现(名称、参数、结果摘要),富内容只在最终回答引用处出现。
112
+
113
+ ## 7. 构建与加载
114
+
115
+ - Host:esbuild 打包为 ESM(平台 node,仅 Node 内置依赖);
116
+ - Client:esbuild 打包为 CJS 主体,由构建脚本包进
117
+ `window.__ModuleLoader__.load({ id, factory(require) { ... } })`;
118
+ - `react` / `react/jsx-runtime` / `react-dom` / `react-dom/client` 保持 external,由 Web 外壳静态模块表提供;其余(含 lucide-react)打进 bundle;
119
+ - CSS 作为文本模块打入 JS,由插件在运行时注入一个带命名空间属性的 `<style>`,卸载时移除。
120
+
121
+ ## 8. 已知取舍
122
+
123
+ 1. **DOM 契约是观测契约,不是稳定 API**:宿主升级可能改变类名/标记。渲染器在所有判定点失败关闭(保留代码块),并在升级后需要重新跑真实宿主验证。
124
+ 2. **Snapshot 不可用时无法区分推理**:降级模式只保证"不渲染流式内容 + 仅 stable 行",无法排除推理块。文档与代码都显式记录了这一限制;真实 Web 组合中 `uiConversation` 必然存在。
125
+ 3. **引用解析优先走会话日志**:这样离线、刷新、宿主重启后仍可渲染历史回答;HTTP 路由只覆盖工具结果被压缩等边缘情况。两者都失败时显示可读诊断,不会静默丢内容。
126
+ 4. **不接管宿主 Markdown**:普通代码块、LaTeX、文件链接、图片灯箱等全部保持宿主行为。
@@ -0,0 +1,57 @@
1
+ # CHANGES
2
+
3
+ 相对上游 `kyoukisu/dsh-rich-ui@0.6.2` 的完整变更报告。
4
+
5
+ ## 1. 总体
6
+
7
+ | 维度 | 上游 | dsh-rich-msg |
8
+ |---|---|---|
9
+ | 定位 | 面向单个 Agent 产品的富回答插件(含模型行为注入) | 通用、无行为倾向的富内容渲染引擎 |
10
+ | 输出通道 | Markdown fence(DOM 适配) | Fence + Tool Bridge 双通道 |
11
+ | 模型上下文 | 自动注入 systemPrompt 段落 + 提示文件 | 零注入;工具描述纯技术 |
12
+ | 最终消息判定 | 仅 `assistant-step` 祖先 | Snapshot 最终性 + 推理排除 + 流式回撤 |
13
+ | 主题 | 自有配色 + 部分桥接 | 全量 DSH token 桥接(明/暗/移动端) |
14
+ | 构建 | Vite + tsc + 独立 demo | esbuild 双产物、无 Vite 依赖 |
15
+ | 测试 | vitest + Playwright 快照 | 13 文件 / 210 项单测与集成测试通过 + 真实宿主 DOM 冒烟;真实模型 E2E 改为人工 |
16
+
17
+ ## 2. 模块级明细
18
+
19
+ | 模块 | 上游原有能力 | 本次修改 | 修改原因 |
20
+ |---|---|---|---|
21
+ | Prompt | `src/host/index.ts` 通过 `inject:["systemPrompt"]` + `systemPrompt.section(...)` 注入 `rich-ui:fence` 指令;`prompt-rich-ui/prompt.md` 为运行时加载文件 | 删除整条注入链;删除 `prompt-rich-ui/`;技术内容整理为 `docs/PROTOCOL.md` / `docs/COMPONENTS.md`(离线文档,不被运行时读取) | 模型行为必须中立 |
22
+ | 行为指导 | prompt 中包含"默认使用""集合优先卡片""validate 后发送"等大量行为指令 | 全部移除;任何文档不得自动进入模型上下文 | 同上 |
23
+ | Validator | `rich_ui_validate`(描述含行为指导) | `rich_msg_validate`:仅描述输入/输出/无副作用;保留严格校验与错误信息 | 工具 API 保持技术性 |
24
+ | Tool Bridge | 无(仅校验) | 新增 `rich_msg_publish`、`ArtifactStore`、只读 HTTP 路由、`ref` 引用协议、浏览器端解析器 | 支持 Tool 通道且不直接展示工具阶段内容 |
25
+ | Fence 发现 | DOM MutationObserver;`rich-ui` 语言;仅 `assistant-step` 祖先 | 保留扫描框架;新增语言别名 `rich-msg`、严格 payload 特征、Chat Snapshot 校验、finality 状态机、推理排除、流式回撤、引用解析、失败关闭 | 满足"只渲染最终回答" |
26
+ | 渲染器 | RichResponse + blocks + primitives + EntityList/Details + persistence | 直接复用;命名空间/存储前缀改为 `dsh-rich-msg`;保留既有可访问性实现 | 复用成熟实现 |
27
+ | 主题 | `rich-ui.css`(67KB,自有 token + 部分 `--dsw-*` 桥接) | 保留组件样式;新增 `client/host-theme.css` 完整桥接(明/暗/375–480px) | Hybrid 视觉要求 |
28
+ | Actions | `[rich-ui-action]` 信封 + 会话捕获 + 大小上限 | 前缀改为 `[rich-msg-action]`;保留白名单/上限/会话捕获/不记录 payload | 品牌统一 |
29
+ | 实体侧栏 | `entity-tab.tsx` 注册右侧栏 tab | 保留,改为可选注入;标识符改为 `dsh-rich-msg/entity` | 保留能力 |
30
+ | 构建 | Vite 配置 + TS 检查 + 两个 vite build | `scripts/build.mjs`:esbuild 打 Host ESM 与 Client CJS,再包 ModuleLoader;CSS 作为文本模块注入 | 摆脱 Vite/npm 构建依赖,与宿主加载协议一致 |
31
+ | 测试 | vitest 单测(schema/UI/host)+ Playwright 快照 | 保留 schema/evaluator/UI/persistence/entityTab 测试;新增 protocol/artifacts/tools/route/neutrality/chat-view/fence-claim/dom-renderer/artifact-resolver 测试;Playwright 快照移除,改为宿主冒烟 + 人工 E2E | 覆盖新链路、不虚构验证 |
32
+ | 文档 | README + 内部 docs + 运行时 prompt | 重写 README + 8 份技术文档 | 交付要求 |
33
+ | 包清单 | `@kyoukisu/dsh-rich-ui` | `dsh-rich-msg` | 独立项目命名 |
34
+
35
+ ## 3. 保留的上游能力
36
+
37
+ - RichSpec v1 Schema 与严格校验(`src/core/schema.ts`)
38
+ - 安全表达式求值(`src/core/evaluator.ts`)
39
+ - 流式前缀解析(`src/core/partial.ts`)
40
+ - 全部 React 组件:布局、文本、实体、图表、表格、指标、媒体、流程、控件、逻辑
41
+ - 图片灯箱、图库、popover/tooltip、实体详情面板、右侧栏实体 tab
42
+ - 控件状态持久化(命名空间调整)
43
+ - 可访问性处理(aria、键盘、读屏标签)
44
+
45
+ ## 3.1 验证状态
46
+
47
+ - 已通过:`tsc --noEmit`(0 错误)、`vitest run`(210/210)、隔离 profile 真实宿主加载与 DOM 冒烟。
48
+ - 未完成(按用户要求停止自动化,改人工):真实模型对话的 Fence 通道与 Tool Bridge 引用端到端验证。
49
+ - 未验证:Android WebView、多标签页并发、超长历史压测、宿主升级后的 DOM 契约。
50
+
51
+ ## 4. 明确不做的事
52
+
53
+ - 不注册任何 prompt / persona / skill / agent preset;
54
+ - 不通过工具描述或初始化代码影响模型行为;
55
+ - 不修改 DSH 主程序;
56
+ - 不接管宿主 Markdown 渲染;
57
+ - 不在工具行渲染富内容。
@@ -0,0 +1,132 @@
1
+ # COMPONENTS
2
+
3
+ RichSpec v1 节点字段参考。`?` 表示可选字段(不是 JSON 语法)。字段未列出者一律禁止。
4
+
5
+ ## 根与实体
6
+
7
+ **Root**:`{version:1, nodes:Node[], title?:string, entities?:Entity[], initialState?:Record<KEY,Scalar>}`
8
+
9
+ **Entity**:`{id:KEY, kind:string, title:string, subtitle?, description?, image?:URL, images?:(URL|Image)[], facts?:{label:string,value:Scalar}[], cast?:{name:string,role?,image?:URL}[], sources?:{title:string,url:URL}[], related?:KEY[], badge?:{label:string,tone?}, thumbShape?:"poster"|"square"|"landscape", galleryLayout?:"paired"|"single"}`
10
+
11
+ - `Image` = `{src:URL, alt:string, caption?:string}`
12
+ - `badge.tone`:`success | info | warning | danger | accent`
13
+ - `thumbShape` 控制行预览形状;`galleryLayout:"single"` 为单图带说明的图库
14
+ - 所有 `related`、`entity-ref`、`open-entity` 目标必须存在
15
+
16
+ ## 文本
17
+
18
+ **text**:`{type:"text", content:Expr, variant?:"body"|"title"|"subtitle"|"caption"}`
19
+
20
+ 行内仅识别:`\`code\``、`**bold**`、`[label](https://…)`、`[[entityId]]`。其他标记按字面显示;链接必须绝对 http(s)。
21
+
22
+ **callout**:`{type:"callout", content:Expr, tone?:"info"|"success"|"warning"|"error"}`
23
+
24
+ **code**:`{type:"code", code:string, language?:string, title?:string, lineNumbers?:boolean}`
25
+
26
+ **icon**:`{type:"icon", name:string, size?:number, tone?:TONE}`(8–128;Lucide 名称)
27
+
28
+ ## 布局
29
+
30
+ | 节点 | 字段 |
31
+ |---|---|
32
+ | `stack` / `row` / `grid` / `card` | `{children:Node[], gap?:0-128, title?:string}`;`grid`/`row` 支持 `columns`(1–12) |
33
+ | `carousel` | `{children:Node[]}` |
34
+ | `divider` | `{label?:string}` |
35
+ | `meta` | `{items:[{text:string,icon?:string}]}` |
36
+ | `popover` | `{label:string, children:Node[]}` |
37
+ | `tooltip` | `{label:string, text:string}` |
38
+
39
+ ## 数据
40
+
41
+ **stat**:`{label:string, value:Expr, delta?:Expr, deltaLabel?:string, hint?:string, tone?:"success"|"warning"|"danger"|"accent", icon?:string}`
42
+
43
+ **progress**:`{value:Expr, max?:number, label?:string, tone?:"success"|"warning"|"danger"|"info"|"accent", showValue?:boolean}`
44
+
45
+ **status**:`{label:string, tone?:"muted"|"success"|"warning"|"danger"|"info"|"accent"}`
46
+
47
+ **computed**:`{expr:Expr, label?:string}`
48
+
49
+ **table**:`{columns:[{key,label,align?:"left"|"right"|"center", format?:"text"|"number"|"status"|"progress", tones?:Record<string,TONE>, max?:number, tone?:TONE}], rows:Record<KEY,Expr>[], sortable?:boolean, filterable?:boolean, filterPlaceholder?:string, caption?:string, empty?:string}`
50
+
51
+ - 列 key 唯一;行对象的字段必须对应列 key;缺格显示 "—"
52
+ - `format:"number"`:千分位 + 右对齐
53
+ - `format:"status"`:按 `tones`(按显示字符串索引)渲染状态 pill,未知值用 muted
54
+ - `format:"progress"`:0–`max`(默认 100)进度条
55
+ - `sortable` / `filterable` 为本地交互
56
+
57
+ **chart**:`{kind:"bar"|"line"|"area"|"donut"|"scatter", data?:{label:string,value:Expr}[], categories?:string[], series?:{label:string,values:Expr[]}[], points?:{x:number,y:number,label?:string}[], unit?:string, xUnit?:string, caption?:string}`
58
+
59
+ - `data` 用于单系列或 donut;donut 值必须非负
60
+ - `categories` + `series` 用于 2–6 个系列,`values` 长度必须等于 `categories`
61
+ - `scatter` 使用 1–200 个 `points`
62
+
63
+ ## 对象与媒体
64
+
65
+ **entity-list**:`{entityIds:string[], layout?:"compact"|"feature"}`
66
+
67
+ **entity-ref**:`{entityId:KEY}`
68
+
69
+ **image**:`{src:URL, alt:string, caption?:string}`
70
+
71
+ **image-group**:`{alt:string, label?:string, presentation?:"button"|"gallery", src?:URL[], items?:Image[]}`(`src` 与 `items` 二选一)
72
+
73
+ **video**:`{src:URL, poster?:URL, title?:string, caption?:string}`(原生 controls,不自动播放,不支持第三方嵌入)
74
+
75
+ **audio**:`{src:URL, title?:string, caption?:string}`
76
+
77
+ **Gallery / 灯箱**:由 `images`、`image-group` 自动启用,支持键盘与读屏。
78
+
79
+ ## 流程
80
+
81
+ **diagram**:`{steps:[{id:KEY,label:string,detail?:string,tone?:TONE,connector?:string,depth?:0|1|2}], direction?:"horizontal"|"vertical", caption?:string}`(1–12 步,id 唯一)
82
+
83
+ **layers**:`{layers:[{title:string, detail?:string, icon?:string, tone?:TONE, items?:[{label,detail?,icon?,tone?}], layers?:[Layer]}], caption?:string}`
84
+
85
+ - 最多 3 层嵌套;每层最多 6 个条目
86
+ - 同一层不能同时包含 `items` 与嵌套 `layers`
87
+
88
+ ## 控件
89
+
90
+ | 节点 | 字段 |
91
+ |---|---|
92
+ | `input` | `{stateKey:KEY, label?, placeholder?, inputType?:"text"|"number"|"date"}` |
93
+ | `select` | `{stateKey:KEY, options:{label,value:Scalar}[], label?}` |
94
+ | `slider` | `{stateKey:KEY, min:number, max:number, label?, step?:number}`(min < max,step > 0) |
95
+ | `switch` | `{stateKey:KEY, label?}` |
96
+ | `checkbox` | `{stateKey:KEY, label:string}` |
97
+ | `radio` | `{stateKey:KEY, label?, layout?:"row"|"column", options:[{label,value}]}` |
98
+ | `segmented` | 同 radio,2–4 个短选项 |
99
+ | `tabs` | `{stateKey:KEY, tabs:[{id:KEY,label,icon?,children:Node[]}]}`(id 唯一) |
100
+ | `button` | `{label:string, action:Action, variant?:"primary"|"secondary"|"ghost", disabled?:Expr}` |
101
+ | `conditional` | `{when:Expr, children:Node[], elseChildren?:Node[]}` |
102
+
103
+ - 共享同一 `stateKey` 的控件必须接受同一值域;
104
+ - `input` 的日期接受 `YYYY-MM-DD` 或 `""`;文本/日期不持久化。
105
+
106
+ ## Actions
107
+
108
+ | action | 字段 | 说明 |
109
+ |---|---|---|
110
+ | `set` | `{key:KEY, value:Expr}` | 本地状态更新(必须落在 domain 内) |
111
+ | `toggle` | `{key:KEY}` | 本地布尔反转 |
112
+ | `reset` | `{}` | 恢复默认值并清除持久化 |
113
+ | `open-entity` | `{entityId:KEY}` | 本地详情面板/侧栏 |
114
+ | `close-entity` | `{}` | 关闭详情 |
115
+ | `copy` | `{text:Expr}` | 点击后写入剪贴板 |
116
+ | `open-url` | `{url:URL}` | 点击后打开外部链接 |
117
+ | `send-message` | `{text:Expr}` | 作为普通用户消息发回对话 |
118
+
119
+ 副作用(复制、打开链接、发消息)只在用户点击后发生;不会被模型数据直接触发。
120
+
121
+ ## 可访问性
122
+
123
+ - 图表提供可访问表格/文本替代;
124
+ - 进度条暴露 `aria-valuenow`;
125
+ - 状态 pill 使用语义色 token;
126
+ - 控件有可读标签;
127
+ - 图片必须有 `alt`;
128
+ - 深色/浅色主题下保持对比度。
129
+
130
+ ## 主题 token
131
+
132
+ 组件自身不写死颜色:全部通过 `--ru-*` 变量,由 `client/host-theme.css` 映射到 DSH 的 `--dsw-*` 令牌。详见 [THEME.md](THEME.md)。