dsh-rich-msg 0.1.0 → 0.1.1
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/README.md +15 -246
- package/docs/ARCHITECTURE.md +13 -5
- package/docs/CHANGES.md +23 -2
- package/docs/DEVELOPMENT.md +5 -4
- package/lib/client.js +237 -85
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,255 +1,24 @@
|
|
|
1
1
|
# dsh-rich-msg
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
DeepSeek Harness(DSH)的富内容消息渲染插件:把声明式 JSON 渲染成跟随宿主主题、可交互的消息内容。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## 功能
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
- **双输出通道**:Markdown fence(```rich-msg,兼容上游 `rich-ui`)与 Tool Bridge(`rich_msg_publish` 暂存 + 最终回答 `ref` 引用);两条通道共用同一渲染器。
|
|
8
|
+
- **只渲染最终回答**:推理过程、工具调用阶段、中间步骤中的内容不渲染;工具暂存内容只有被最终回答明确引用后才显示。
|
|
9
|
+
- **组件**:布局(stack/row/grid/card/carousel/divider/meta)、文本与 callout/code、实体列表与详情、图库与图片灯箱、卡片指标/进度/状态/表格(排序筛选)/图表(bar/line/area/donut/scatter)、流程 diagram、分层 layers、图片/视频/音频、popover/tooltip、输入/选择/滑块/开关/复选/单选/分段/tabs、条件渲染与安全表达式。
|
|
10
|
+
- **交互**:本地 set/toggle/reset、复制、打开链接、发消息回传;控件状态按会话持久化。
|
|
11
|
+
- **主题**:颜色/字体/圆角/阴影取自 DSH 主题变量,支持浅色、深色与 390px 移动端。
|
|
12
|
+
- **安全**:严格 Schema 校验、无 HTML/JS 执行、无 eval、URL 白名单、大小/深度/数量上限、会话隔离;交互只由用户触发。
|
|
13
|
+
- **模型中立**:不注入 system prompt、persona 或 skill,不规定模型何时/如何使用富内容。
|
|
14
|
+
- **工具**:`rich_msg_validate`(Schema 校验)、`rich_msg_publish`(校验并存入会话 Artifact,返回引用 id)。
|
|
11
15
|
|
|
12
|
-
|
|
16
|
+
## 许可证
|
|
13
17
|
|
|
14
|
-
|
|
18
|
+
MIT。
|
|
15
19
|
|
|
16
|
-
|
|
20
|
+
本项目基于 [kyoukisu/dsh-rich-ui](https://github.com/kyoukisu/dsh-rich-ui)(MIT,Copyright (c) 2026 kyoukisu)的源码进行独立开发与重构,上游版权声明保留在 [LICENSE](LICENSE);与上游作者无隶属、合作或背书关系。
|
|
17
21
|
|
|
18
|
-
|
|
22
|
+
图标来自 [lucide](https://lucide.dev)(ISC)。
|
|
19
23
|
|
|
20
|
-
|
|
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
|
-
```
|
|
24
|
+
技术文档见 `docs/`。
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -88,15 +88,23 @@
|
|
|
88
88
|
└─ 通过 → 校验/挂载;任一环节失败 → 保留宿主原生代码块
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
-
**关键点**:`assistant-step` 在 DSH 中既包含推理过程,也包含工具调用前后的中间叙述,所以"祖先匹配"不能证明它是最终回答。Snapshot
|
|
91
|
+
**关键点**:`assistant-step` 在 DSH 中既包含推理过程,也包含工具调用前后的中间叙述,所以"祖先匹配"不能证明它是最终回答。Snapshot 判定解决了这一点。
|
|
92
|
+
|
|
93
|
+
**调度策略(0.1.1 起)**:完全事件驱动。
|
|
94
|
+
|
|
95
|
+
- 触发器只有三个:宿主 DOM 的 `MutationObserver` 批次、Chat Snapshot 订阅、`visibilitychange`;
|
|
96
|
+
- 同一事件循环内的多次触发合并到一个微任务,**没有定时器、没有轮询、没有 rAF 循环**;
|
|
97
|
+
- 每次只评估事件命中的行;行签名未变时零解析、零重绘;
|
|
98
|
+
- **默认只在行已稳定后渲染**(不渲染流式预览)。回答完成前的围栏保持宿主原生代码块,避免每个 token 重建 React 树;
|
|
99
|
+
- 渲染异常由 `RenderBoundary` 捕获:隐藏容器、恢复源码块,不重试同一失败树。
|
|
92
100
|
|
|
93
101
|
### 4.3 挂载与重挂载
|
|
94
102
|
|
|
95
103
|
- 每个源码块对应一个独立 `createRoot` 容器,容器插在源码块之后并设置 `hidden` 隐藏源块;
|
|
96
|
-
-
|
|
97
|
-
- 宿主
|
|
98
|
-
-
|
|
99
|
-
- 卸载:逐个 unmount、移除容器、恢复 `source.hidden
|
|
104
|
+
- 同一围栏内容变化时**原地更新**(复用 root、按渲染 key 去重),不销毁重建;
|
|
105
|
+
- 宿主 reconciler 可能移除外来节点:校验 `source.nextSibling === container` 并按需重插,**最多 3 次**;超限则放弃该行并恢复原生代码块,避免插入/删除拉锯;
|
|
106
|
+
- **每个 document scope 只允许一个渲染器实例**(HMR/重复安装会替换旧实例,而不是双挂载);
|
|
107
|
+
- 卸载:逐个 unmount、移除容器、恢复 `source.hidden`、断开观察者与订阅。
|
|
100
108
|
|
|
101
109
|
### 4.4 持久化
|
|
102
110
|
|
package/docs/CHANGES.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
相对上游 `kyoukisu/dsh-rich-ui@0.6.2` 的完整变更报告。
|
|
4
4
|
|
|
5
|
+
## 0.1.1(稳定性修复)
|
|
6
|
+
|
|
7
|
+
0.1.0 在长会话中会出现页面无响应("此页面没有响应")。根因是渲染调度而非数据协议:
|
|
8
|
+
|
|
9
|
+
1. 流式期间每来一个 token 就销毁并重建一个 React root;
|
|
10
|
+
2. 每次 DOM 变更全量扫描整个页面,长会话 / 长历史下开销失控;
|
|
11
|
+
3. 复用分支把已渲染容器反向设为 `hidden`,与宿主 reconciler 反复拉扯。
|
|
12
|
+
|
|
13
|
+
0.1.1 修复:
|
|
14
|
+
|
|
15
|
+
- 渲染调度改为**纯事件驱动**(DOM MutationObserver 批次 / Chat Snapshot 订阅 / `visibilitychange`,微任务合并;无定时器、无轮询、无 rAF 循环);
|
|
16
|
+
- **默认只在稳定的最终回答渲染**(流式预览改为可选且默认关闭),消除 per-token React 重建;
|
|
17
|
+
- 按事件命中的行**局部评估**,行签名缓存使重复扫描零解析、零重绘;
|
|
18
|
+
- 同一围栏**原地更新**,不重建 root;
|
|
19
|
+
- **每个 document scope 只允许一个渲染器实例**;
|
|
20
|
+
- 宿主移除外来容器时最多重插 3 次,超限放弃并恢复原生代码块;
|
|
21
|
+
- 修复复用分支的 `hidden` 反转 bug;
|
|
22
|
+
- 删除对已移除的 `prompt-rich-ui` 目录的残留测试引用。
|
|
23
|
+
|
|
24
|
+
验证:`tsc --noEmit` 0 错误;15 个测试文件 / 226 项测试通过;隔离宿主压力验证(201 行模拟历史 + 150 次流式形状 DOM 变更)0 long task、最大帧间隔 19.5ms、单 root、卸载归零、无控制台错误。
|
|
25
|
+
|
|
5
26
|
## 1. 总体
|
|
6
27
|
|
|
7
28
|
| 维度 | 上游 | dsh-rich-msg |
|
|
@@ -12,7 +33,7 @@
|
|
|
12
33
|
| 最终消息判定 | 仅 `assistant-step` 祖先 | Snapshot 最终性 + 推理排除 + 流式回撤 |
|
|
13
34
|
| 主题 | 自有配色 + 部分桥接 | 全量 DSH token 桥接(明/暗/移动端) |
|
|
14
35
|
| 构建 | Vite + tsc + 独立 demo | esbuild 双产物、无 Vite 依赖 |
|
|
15
|
-
| 测试 | vitest + Playwright 快照 |
|
|
36
|
+
| 测试 | vitest + Playwright 快照 | 15 文件 / 226 项单测与集成测试通过 + 真实宿主 DOM 冒烟;真实模型 E2E 改为人工 |
|
|
16
37
|
|
|
17
38
|
## 2. 模块级明细
|
|
18
39
|
|
|
@@ -44,7 +65,7 @@
|
|
|
44
65
|
|
|
45
66
|
## 3.1 验证状态
|
|
46
67
|
|
|
47
|
-
- 已通过:`tsc --noEmit`(0 错误)、`vitest run`(
|
|
68
|
+
- 已通过:`tsc --noEmit`(0 错误)、`vitest run`(15 文件 / 226 项)、隔离 profile 真实宿主加载与 DOM 冒烟、0.1.1 事件驱动压力验证。
|
|
48
69
|
- 未完成(按用户要求停止自动化,改人工):真实模型对话的 Fence 通道与 Tool Bridge 引用端到端验证。
|
|
49
70
|
- 未验证:Android WebView、多标签页并发、超长历史压测、宿主升级后的 DOM 契约。
|
|
50
71
|
|
package/docs/DEVELOPMENT.md
CHANGED
|
@@ -46,7 +46,7 @@ node scripts/build.mjs
|
|
|
46
46
|
|
|
47
47
|
```
|
|
48
48
|
lib/index.js 48,876 bytes
|
|
49
|
-
lib/client.js
|
|
49
|
+
lib/client.js 346,449 bytes (0.1.1)
|
|
50
50
|
host esbuild warnings: 0
|
|
51
51
|
tsc --noEmit: 0 errors
|
|
52
52
|
```
|
|
@@ -59,17 +59,18 @@ npx vitest run
|
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
```
|
|
62
|
-
Test Files
|
|
63
|
-
Tests
|
|
62
|
+
Test Files 15 passed (15)
|
|
63
|
+
Tests 226 passed (226)
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
+
> 0.1.1 修复记录:0.1.0 在长会话中会出现页面无响应——根因是流式期间每 token 重建 React root、每次 DOM 变更全量扫描整页、宿主 reconcider 与容器隐藏互相拉扯。0.1.1 改为纯事件驱动、默认只在稳定回答渲染、按行局部评估、原地更新、单 scope 单实例、重插上限。上述 226 项测试与隔离宿主压力验证(201 行模拟历史 + 150 次流式形状 DOM 变更:0 long task、最大帧间隔 19.5ms、单 root、卸载归零)均在 0.1.1 上通过。
|
|
67
|
+
|
|
66
68
|
覆盖范围:
|
|
67
69
|
|
|
68
70
|
| 测试文件 | 覆盖 |
|
|
69
71
|
|---|---|
|
|
70
72
|
| `tests/core/schema.test.ts` | RichSpec 全字段校验、非法输入、上限、实体/引用/图表/表格/控件 |
|
|
71
73
|
| `tests/core/evaluator.test.ts` | 表达式求值、算子树、状态、边界 |
|
|
72
|
-
| `tests/core/references.test.ts` | 引用目标收集 |
|
|
73
74
|
| `tests/shared/protocol.test.ts` | ref 协议、Action 信封、id 语法、大小上限 |
|
|
74
75
|
| `tests/host/artifacts.test.ts` | 会话隔离、TTL、容量、淘汰、清理 |
|
|
75
76
|
| `tests/host/tools.test.ts` | 工具 Schema、中性描述、校验/发布行为、取消信号 |
|
package/lib/client.js
CHANGED
|
@@ -9783,7 +9783,9 @@ function looksLikeDocument(raw) {
|
|
|
9783
9783
|
}
|
|
9784
9784
|
function claimFence(source, body, raw) {
|
|
9785
9785
|
if (raw.length > MAX_SOURCE_LENGTH) {
|
|
9786
|
-
if (isOurs(labelToken(fenceLabel(source, body))))
|
|
9786
|
+
if (isOurs(labelToken(fenceLabel(source, body)))) {
|
|
9787
|
+
return { kind: "invalid", error: "Document is longer than " + MAX_SOURCE_LENGTH + " characters" };
|
|
9788
|
+
}
|
|
9787
9789
|
return null;
|
|
9788
9790
|
}
|
|
9789
9791
|
const label = fenceLabel(source, body);
|
|
@@ -9819,14 +9821,39 @@ function rowOf(source) {
|
|
|
9819
9821
|
function isStreamingDom(source) {
|
|
9820
9822
|
return source.closest("[data-streaming]") !== null;
|
|
9821
9823
|
}
|
|
9822
|
-
function
|
|
9823
|
-
|
|
9824
|
-
|
|
9824
|
+
function surfacesOf(row) {
|
|
9825
|
+
return [...row.querySelectorAll(SURFACES)].filter(
|
|
9826
|
+
(surface) => !surface.closest(OWN) && !surface.parentElement?.closest(SURFACES)
|
|
9827
|
+
);
|
|
9828
|
+
}
|
|
9829
|
+
function rowSignature(row) {
|
|
9830
|
+
const key2 = row.getAttribute("data-chat-node-key") ?? row.getAttribute("data-chat-anchor-key") ?? "";
|
|
9831
|
+
const streaming = isStreamingDom(row) ? "1" : "0";
|
|
9832
|
+
const surfaces = surfacesOf(row);
|
|
9833
|
+
const lengths = surfaces.map((surface) => {
|
|
9834
|
+
const body = surface.querySelector("pre") ?? surface.querySelector("[data-code-block-content]");
|
|
9835
|
+
return body?.textContent?.length ?? 0;
|
|
9836
|
+
});
|
|
9837
|
+
return key2 + "|" + streaming + "|" + surfaces.length + "|" + lengths.join(",");
|
|
9825
9838
|
}
|
|
9826
|
-
function installFenceRenderer(deps, scope = document.body) {
|
|
9839
|
+
function installFenceRenderer(deps, scope = document.body, options = {}) {
|
|
9840
|
+
if (activeScopeRenderer !== void 0 && activeScopeRenderer.scope === scope) {
|
|
9841
|
+
try {
|
|
9842
|
+
activeScopeRenderer.dispose();
|
|
9843
|
+
} catch {
|
|
9844
|
+
}
|
|
9845
|
+
}
|
|
9846
|
+
const previewAllowed = options.previewDuringStreaming === true;
|
|
9847
|
+
const maxMounts = typeof options.maxMounts === "number" && options.maxMounts > 0 ? options.maxMounts : 40;
|
|
9827
9848
|
const mounts = /* @__PURE__ */ new Map();
|
|
9849
|
+
const knownRows = /* @__PURE__ */ new Set();
|
|
9850
|
+
const unstableRows = /* @__PURE__ */ new WeakSet();
|
|
9851
|
+
const rowSignatures = /* @__PURE__ */ new Map();
|
|
9828
9852
|
let disposed = false;
|
|
9829
|
-
let
|
|
9853
|
+
let flushPending = false;
|
|
9854
|
+
let pendingRows = /* @__PURE__ */ new Set();
|
|
9855
|
+
let fullRescan = true;
|
|
9856
|
+
let sessionId;
|
|
9830
9857
|
const remove = (source, mount) => {
|
|
9831
9858
|
try {
|
|
9832
9859
|
mount.root.unmount();
|
|
@@ -9837,9 +9864,9 @@ function installFenceRenderer(deps, scope = document.body) {
|
|
|
9837
9864
|
mounts.delete(source);
|
|
9838
9865
|
};
|
|
9839
9866
|
const paint = (mount, facts) => {
|
|
9840
|
-
const
|
|
9867
|
+
const session = mount.sessionId;
|
|
9841
9868
|
const onAction = (action2, payload) => {
|
|
9842
|
-
deps.sendAction(
|
|
9869
|
+
deps.sendAction(session, action2, payload);
|
|
9843
9870
|
};
|
|
9844
9871
|
const onOpenEntity = (entity, entities) => {
|
|
9845
9872
|
if (deps.openEntity === void 0) return false;
|
|
@@ -9872,63 +9899,87 @@ function installFenceRenderer(deps, scope = document.body) {
|
|
|
9872
9899
|
const error = mount.mode === "resolving" ? "Resolving reference..." : mount.error ?? "Invalid document";
|
|
9873
9900
|
mount.root.render((0, import_react16.createElement)(Diagnostic, { key: error + ":" + mount.raw, error, source: mount.raw }));
|
|
9874
9901
|
};
|
|
9875
|
-
const
|
|
9876
|
-
const
|
|
9877
|
-
|
|
9878
|
-
|
|
9879
|
-
|
|
9880
|
-
if (facts.fenceLocation === "reasoning") return { claim: null, facts, streaming: false };
|
|
9881
|
-
if (facts.finality === "not-final") return { claim: null, facts, streaming: false };
|
|
9882
|
-
if (facts.finality === "streaming-final" && facts.fenceLocation !== "text") return { claim: null, facts, streaming: false };
|
|
9883
|
-
return { claim, facts, streaming: facts.finality === "streaming-final" };
|
|
9884
|
-
}
|
|
9885
|
-
if (isStreamingDom(source)) return { claim: null, facts, streaming: false };
|
|
9886
|
-
return { claim, facts, streaming: false };
|
|
9902
|
+
const paintIfNeeded = (mount, facts) => {
|
|
9903
|
+
const key2 = mount.mode + "|" + mount.raw + "|" + (mount.error ?? "") + "|" + (mount.ref ?? "") + "|" + (facts?.finality ?? "");
|
|
9904
|
+
if (mount.renderedKey === key2) return;
|
|
9905
|
+
mount.renderedKey = key2;
|
|
9906
|
+
paint(mount, facts);
|
|
9887
9907
|
};
|
|
9888
|
-
const
|
|
9889
|
-
|
|
9890
|
-
|
|
9891
|
-
|
|
9892
|
-
if (
|
|
9908
|
+
const ensurePlacement = (source, mount) => {
|
|
9909
|
+
if (source.nextSibling !== mount.container) {
|
|
9910
|
+
if (!source.isConnected) return false;
|
|
9911
|
+
mount.reinserts += 1;
|
|
9912
|
+
if (mount.reinserts > 3) {
|
|
9913
|
+
const row = rowOf(source);
|
|
9914
|
+
if (row !== null) unstableRows.add(row);
|
|
9915
|
+
return false;
|
|
9916
|
+
}
|
|
9917
|
+
source.after(mount.container);
|
|
9893
9918
|
}
|
|
9894
|
-
|
|
9895
|
-
|
|
9896
|
-
|
|
9919
|
+
if (!source.hidden) source.hidden = true;
|
|
9920
|
+
if (mount.container.hidden) mount.container.hidden = false;
|
|
9921
|
+
return true;
|
|
9922
|
+
};
|
|
9923
|
+
const evaluateRow = (row, force = false) => {
|
|
9924
|
+
if (disposed || !row.isConnected) return;
|
|
9925
|
+
if (unstableRows.has(row)) return;
|
|
9926
|
+
if (row.getAttribute("data-chat-flow-kind") !== ANSWER_ROW) {
|
|
9927
|
+
for (const source of surfacesOf(row)) {
|
|
9928
|
+
const mount = mounts.get(source);
|
|
9929
|
+
if (mount !== void 0) remove(source, mount);
|
|
9930
|
+
}
|
|
9931
|
+
knownRows.delete(row);
|
|
9932
|
+
return;
|
|
9933
|
+
}
|
|
9934
|
+
const surfaces = surfacesOf(row);
|
|
9935
|
+
if (surfaces.length === 0) {
|
|
9936
|
+
knownRows.delete(row);
|
|
9937
|
+
return;
|
|
9938
|
+
}
|
|
9939
|
+
knownRows.add(row);
|
|
9940
|
+
const signature = rowSignature(row);
|
|
9941
|
+
if (!force && rowSignatures.get(row) === signature) {
|
|
9942
|
+
for (const source of surfaces) {
|
|
9943
|
+
const mount = mounts.get(source);
|
|
9944
|
+
if (mount !== void 0 && !ensurePlacement(source, mount)) remove(source, mount);
|
|
9945
|
+
}
|
|
9946
|
+
return;
|
|
9897
9947
|
}
|
|
9898
|
-
|
|
9899
|
-
for (const source of
|
|
9900
|
-
if (source.closest(OWN)) continue;
|
|
9901
|
-
const row = rowOf(source);
|
|
9948
|
+
rowSignatures.set(row, signature);
|
|
9949
|
+
for (const source of surfaces) {
|
|
9902
9950
|
const body = source.querySelector("pre") ?? source.querySelector("[data-code-block-content]");
|
|
9903
|
-
if (
|
|
9904
|
-
const
|
|
9905
|
-
if (
|
|
9951
|
+
if (body === null) {
|
|
9952
|
+
const stale = mounts.get(source);
|
|
9953
|
+
if (stale !== void 0) remove(source, stale);
|
|
9906
9954
|
continue;
|
|
9907
9955
|
}
|
|
9908
9956
|
const raw = body.textContent ?? "";
|
|
9909
|
-
const
|
|
9957
|
+
const facts = deps.chat.factsForRow(row, raw);
|
|
9958
|
+
const claim = claimFence(source, body, raw);
|
|
9910
9959
|
const mount = mounts.get(source);
|
|
9911
|
-
|
|
9960
|
+
const streaming = facts !== void 0 ? facts.finality === "streaming-final" : isStreamingDom(source);
|
|
9961
|
+
const claimable = claim !== null && !(facts !== void 0 && facts.fenceLocation === "reasoning") && !(facts !== void 0 && facts.finality === "not-final") && !(facts !== void 0 && facts.finality === "streaming-final" && facts.fenceLocation !== "text") && !(streaming && !previewAllowed);
|
|
9962
|
+
if (!claimable || claim === null) {
|
|
9912
9963
|
if (mount !== void 0) remove(source, mount);
|
|
9913
9964
|
continue;
|
|
9914
9965
|
}
|
|
9915
|
-
if (mount !== void 0 && mount.sessionId !== sessionId) {
|
|
9916
|
-
remove(source, mount);
|
|
9917
|
-
}
|
|
9918
|
-
const existing = mounts.get(source);
|
|
9919
9966
|
const owner = fenceOwnerKey(source, row);
|
|
9920
|
-
const
|
|
9921
|
-
const
|
|
9922
|
-
if (
|
|
9923
|
-
|
|
9924
|
-
|
|
9925
|
-
|
|
9926
|
-
|
|
9967
|
+
const nextMode = claim.kind === "spec" ? "rich" : claim.kind === "ref" ? "resolving" : "diagnostic";
|
|
9968
|
+
const nextRef = claim.kind === "ref" ? claim.ref : void 0;
|
|
9969
|
+
if (mount !== void 0 && !mount.failed && mount.sessionId === sessionId && mount.owner === owner && mount.ref === nextRef && mount.mode !== "resolving") {
|
|
9970
|
+
mount.raw = raw;
|
|
9971
|
+
mount.streaming = streaming;
|
|
9972
|
+
mount.error = claim.kind === "invalid" ? claim.error : void 0;
|
|
9973
|
+
mount.spec = claim.kind === "spec" ? claim.spec : mount.spec;
|
|
9974
|
+
if (!ensurePlacement(source, mount)) {
|
|
9975
|
+
remove(source, mount);
|
|
9976
|
+
continue;
|
|
9927
9977
|
}
|
|
9978
|
+
paintIfNeeded(mount, facts);
|
|
9928
9979
|
continue;
|
|
9929
9980
|
}
|
|
9930
|
-
if (
|
|
9931
|
-
if (
|
|
9981
|
+
if (mount !== void 0) remove(source, mount);
|
|
9982
|
+
if (mounts.size >= maxMounts) continue;
|
|
9932
9983
|
const container = document.createElement("div");
|
|
9933
9984
|
container.setAttribute(DOM_MARKERS.root, "");
|
|
9934
9985
|
const root = (0, import_client.createRoot)(container);
|
|
@@ -9938,23 +9989,20 @@ function installFenceRenderer(deps, scope = document.body) {
|
|
|
9938
9989
|
root,
|
|
9939
9990
|
raw,
|
|
9940
9991
|
sourceHidden: source.hidden,
|
|
9941
|
-
streaming
|
|
9942
|
-
mode,
|
|
9943
|
-
ref,
|
|
9944
|
-
spec:
|
|
9945
|
-
error:
|
|
9992
|
+
streaming,
|
|
9993
|
+
mode: nextMode,
|
|
9994
|
+
ref: nextRef,
|
|
9995
|
+
spec: claim.kind === "spec" ? claim.spec : void 0,
|
|
9996
|
+
error: claim.kind === "invalid" ? claim.error : void 0,
|
|
9946
9997
|
sessionId,
|
|
9947
|
-
owner
|
|
9998
|
+
owner,
|
|
9999
|
+
reinserts: 0
|
|
9948
10000
|
};
|
|
9949
10001
|
mounts.set(source, next);
|
|
9950
10002
|
source.after(container);
|
|
9951
10003
|
source.hidden = true;
|
|
9952
|
-
if (
|
|
9953
|
-
|
|
9954
|
-
continue;
|
|
9955
|
-
}
|
|
9956
|
-
if (mode === "diagnostic") {
|
|
9957
|
-
paint(next, evaluated.facts);
|
|
10004
|
+
if (nextMode === "rich" || nextMode === "diagnostic") {
|
|
10005
|
+
paintIfNeeded(next, facts);
|
|
9958
10006
|
continue;
|
|
9959
10007
|
}
|
|
9960
10008
|
const referenceId = next.ref ?? "";
|
|
@@ -9963,7 +10011,7 @@ function installFenceRenderer(deps, scope = document.body) {
|
|
|
9963
10011
|
applyResolution(next, immediate);
|
|
9964
10012
|
continue;
|
|
9965
10013
|
}
|
|
9966
|
-
|
|
10014
|
+
paintIfNeeded(next, facts);
|
|
9967
10015
|
void deps.artifacts.resolve(sessionId, referenceId).then((resolved) => {
|
|
9968
10016
|
if (disposed || mounts.get(source) !== next) return;
|
|
9969
10017
|
applyResolution(next, resolved);
|
|
@@ -9975,52 +10023,156 @@ function installFenceRenderer(deps, scope = document.body) {
|
|
|
9975
10023
|
mount.mode = "rich";
|
|
9976
10024
|
mount.spec = resolved.spec;
|
|
9977
10025
|
mount.container.hidden = false;
|
|
9978
|
-
|
|
10026
|
+
mount.renderedKey = void 0;
|
|
10027
|
+
paintIfNeeded(mount, void 0);
|
|
9979
10028
|
return;
|
|
9980
10029
|
}
|
|
9981
10030
|
mount.mode = "diagnostic";
|
|
9982
10031
|
mount.error = "Unresolved rich-msg reference: " + (resolved.id || "missing id");
|
|
9983
10032
|
mount.container.hidden = false;
|
|
9984
|
-
|
|
10033
|
+
mount.renderedKey = void 0;
|
|
10034
|
+
paintIfNeeded(mount, void 0);
|
|
10035
|
+
};
|
|
10036
|
+
const rowsFromRecords = (records) => {
|
|
10037
|
+
const rows = /* @__PURE__ */ new Set();
|
|
10038
|
+
let full = false;
|
|
10039
|
+
for (const record of records) {
|
|
10040
|
+
const target = record.target instanceof Element ? record.target : record.target.parentElement;
|
|
10041
|
+
if (target === null) {
|
|
10042
|
+
full = true;
|
|
10043
|
+
continue;
|
|
10044
|
+
}
|
|
10045
|
+
if (target.closest(OWN) !== null) continue;
|
|
10046
|
+
const row = target.closest("[data-chat-flow-kind]");
|
|
10047
|
+
if (row !== null) rows.add(row);
|
|
10048
|
+
for (const node of [...record.addedNodes, ...record.removedNodes]) {
|
|
10049
|
+
if (!(node instanceof Element)) continue;
|
|
10050
|
+
if (node.matches(OWN) || node.closest(OWN) !== null) continue;
|
|
10051
|
+
const addedRow = node.closest?.("[data-chat-flow-kind]");
|
|
10052
|
+
if (addedRow !== null && addedRow !== void 0) rows.add(addedRow);
|
|
10053
|
+
for (const nested of node.querySelectorAll?.("[data-chat-flow-kind]") ?? []) {
|
|
10054
|
+
if (nested instanceof HTMLElement) rows.add(nested);
|
|
10055
|
+
}
|
|
10056
|
+
}
|
|
10057
|
+
if (record.type === "childList" || record.type === "characterData") {
|
|
10058
|
+
const owner = target.closest("[data-chat-flow-kind]");
|
|
10059
|
+
if (owner !== null) rows.add(owner);
|
|
10060
|
+
}
|
|
10061
|
+
}
|
|
10062
|
+
return { rows, full };
|
|
10063
|
+
};
|
|
10064
|
+
const schedule = (rows, full = false) => {
|
|
10065
|
+
if (disposed) return;
|
|
10066
|
+
if (full) fullRescan = true;
|
|
10067
|
+
if (rows !== void 0) for (const row of rows) pendingRows.add(row);
|
|
10068
|
+
if (flushPending) return;
|
|
10069
|
+
flushPending = true;
|
|
10070
|
+
queueMicrotask(() => {
|
|
10071
|
+
flushPending = false;
|
|
10072
|
+
if (disposed) return;
|
|
10073
|
+
flush();
|
|
10074
|
+
});
|
|
9985
10075
|
};
|
|
9986
|
-
const
|
|
9987
|
-
if (
|
|
9988
|
-
|
|
9989
|
-
if (
|
|
9990
|
-
|
|
9991
|
-
|
|
9992
|
-
|
|
10076
|
+
const flush = () => {
|
|
10077
|
+
if (disposed) return;
|
|
10078
|
+
const nextSession = deps.chat.sessionId();
|
|
10079
|
+
if (nextSession !== sessionId) {
|
|
10080
|
+
sessionId = nextSession;
|
|
10081
|
+
fullRescan = true;
|
|
10082
|
+
}
|
|
10083
|
+
if (fullRescan) {
|
|
10084
|
+
fullRescan = false;
|
|
10085
|
+
for (const [source, mount] of mounts) remove(source, mount);
|
|
10086
|
+
knownRows.clear();
|
|
10087
|
+
rowSignatures.clear();
|
|
10088
|
+
const rows = /* @__PURE__ */ new Set();
|
|
10089
|
+
for (const surface of scope.querySelectorAll(SURFACES)) {
|
|
10090
|
+
if (surface.closest(OWN) !== null) continue;
|
|
10091
|
+
const row = rowOf(surface);
|
|
10092
|
+
if (row !== null) rows.add(row);
|
|
10093
|
+
}
|
|
10094
|
+
for (const row of rows) evaluateRow(row, true);
|
|
10095
|
+
return;
|
|
10096
|
+
}
|
|
10097
|
+
for (const row of pendingRows) {
|
|
10098
|
+
if (!row.isConnected) {
|
|
10099
|
+
knownRows.delete(row);
|
|
10100
|
+
continue;
|
|
10101
|
+
}
|
|
10102
|
+
evaluateRow(row);
|
|
10103
|
+
}
|
|
10104
|
+
pendingRows = /* @__PURE__ */ new Set();
|
|
10105
|
+
for (const [source, mount] of mounts) {
|
|
10106
|
+
if (!source.isConnected || !mount.container.isConnected) remove(source, mount);
|
|
9993
10107
|
}
|
|
10108
|
+
void knownRows;
|
|
9994
10109
|
};
|
|
9995
|
-
const
|
|
9996
|
-
if (
|
|
9997
|
-
|
|
9998
|
-
|
|
9999
|
-
|
|
10110
|
+
const onChatChange = () => {
|
|
10111
|
+
if (disposed) return;
|
|
10112
|
+
const nextSession = deps.chat.sessionId();
|
|
10113
|
+
const rows = /* @__PURE__ */ new Set();
|
|
10114
|
+
for (const row of knownRows) {
|
|
10115
|
+
if (row.isConnected) rows.add(row);
|
|
10116
|
+
}
|
|
10117
|
+
if (nextSession !== sessionId) {
|
|
10118
|
+
schedule(rows, true);
|
|
10000
10119
|
return;
|
|
10001
10120
|
}
|
|
10002
|
-
|
|
10121
|
+
if (rows.size === 0) return;
|
|
10122
|
+
if (flushPending) return;
|
|
10123
|
+
flushPending = true;
|
|
10124
|
+
queueMicrotask(() => {
|
|
10125
|
+
flushPending = false;
|
|
10126
|
+
if (disposed) return;
|
|
10127
|
+
for (const row of rows) if (row.isConnected) evaluateRow(row, true);
|
|
10128
|
+
for (const [source, mount] of mounts) {
|
|
10129
|
+
if (!source.isConnected || !mount.container.isConnected) remove(source, mount);
|
|
10130
|
+
}
|
|
10131
|
+
});
|
|
10132
|
+
};
|
|
10133
|
+
const observer = new MutationObserver((records) => {
|
|
10134
|
+
if (disposed) return;
|
|
10135
|
+
const { rows, full } = rowsFromRecords(records);
|
|
10136
|
+
if (full || rows.size > 0) schedule(rows, full);
|
|
10003
10137
|
});
|
|
10004
10138
|
observer.observe(scope, {
|
|
10005
10139
|
childList: true,
|
|
10006
10140
|
subtree: true,
|
|
10007
10141
|
characterData: true,
|
|
10008
10142
|
attributes: true,
|
|
10009
|
-
attributeFilter: ["data-streaming", "
|
|
10143
|
+
attributeFilter: ["data-streaming", "data-chat-node-key", "data-chat-anchor-key", "data-chat-flow-kind"]
|
|
10010
10144
|
});
|
|
10011
|
-
const
|
|
10012
|
-
|
|
10013
|
-
|
|
10145
|
+
const onVisibility = () => {
|
|
10146
|
+
if (disposed) return;
|
|
10147
|
+
if (typeof document !== "undefined" && document.hidden) return;
|
|
10148
|
+
schedule(void 0, true);
|
|
10149
|
+
};
|
|
10150
|
+
if (typeof document !== "undefined") document.addEventListener("visibilitychange", onVisibility);
|
|
10151
|
+
const unsubscribeChat = deps.chat.subscribe(onChatChange);
|
|
10152
|
+
sessionId = deps.chat.sessionId();
|
|
10153
|
+
fullRescan = true;
|
|
10154
|
+
flush();
|
|
10155
|
+
const dispose = () => {
|
|
10014
10156
|
if (disposed) return;
|
|
10015
10157
|
disposed = true;
|
|
10016
|
-
|
|
10158
|
+
unsubscribeChat();
|
|
10017
10159
|
observer.disconnect();
|
|
10160
|
+
if (typeof document !== "undefined") document.removeEventListener("visibilitychange", onVisibility);
|
|
10018
10161
|
for (const [source, mount] of mounts) remove(source, mount);
|
|
10162
|
+
knownRows.clear();
|
|
10163
|
+
rowSignatures.clear();
|
|
10164
|
+
pendingRows = /* @__PURE__ */ new Set();
|
|
10165
|
+
if (activeScopeRenderer !== void 0 && activeScopeRenderer.dispose === dispose) {
|
|
10166
|
+
activeScopeRenderer = void 0;
|
|
10167
|
+
}
|
|
10019
10168
|
};
|
|
10169
|
+
activeScopeRenderer = { scope, dispose };
|
|
10170
|
+
return dispose;
|
|
10020
10171
|
}
|
|
10172
|
+
var activeScopeRenderer;
|
|
10021
10173
|
function persistenceKeyOf(mount, facts) {
|
|
10022
|
-
if (mount.sessionId === void 0) return
|
|
10023
|
-
const tag =
|
|
10174
|
+
if (mount.sessionId === void 0) return JSON.stringify(["dsh-rich-msg", mount.owner ?? "", mount.ref ?? ""]);
|
|
10175
|
+
const tag = facts === void 0 ? "no-facts" : facts.finality + "|" + facts.fenceLocation;
|
|
10024
10176
|
return JSON.stringify(["dsh-rich-msg", mount.sessionId, mount.owner ?? "", tag, mount.ref ?? ""]);
|
|
10025
10177
|
}
|
|
10026
10178
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-rich-msg",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Rich content message renderer for DeepSeek Harness: declarative JSON rendered as themed, interactive message UI, with fence and tool-bridge channels.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|