dsh-rich-msg 0.0.0-stage → 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/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,24 @@
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
+ DeepSeek Harness(DSH)的富内容消息渲染插件:把声明式 JSON 渲染成跟随宿主主题、可交互的消息内容。
4
+
5
+ ## 功能
6
+
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)。
15
+
16
+ ## 许可证
17
+
18
+ MIT。
19
+
20
+ 本项目基于 [kyoukisu/dsh-rich-ui](https://github.com/kyoukisu/dsh-rich-ui)(MIT,Copyright (c) 2026 kyoukisu)的源码进行独立开发与重构,上游版权声明保留在 [LICENSE](LICENSE);与上游作者无隶属、合作或背书关系。
21
+
22
+ 图标来自 [lucide](https://lucide.dev)(ISC)。
23
+
24
+ 技术文档见 `docs/`。
@@ -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,134 @@
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
+ **调度策略(0.1.1 起)**:完全事件驱动。
94
+
95
+ - 触发器只有三个:宿主 DOM 的 `MutationObserver` 批次、Chat Snapshot 订阅、`visibilitychange`;
96
+ - 同一事件循环内的多次触发合并到一个微任务,**没有定时器、没有轮询、没有 rAF 循环**;
97
+ - 每次只评估事件命中的行;行签名未变时零解析、零重绘;
98
+ - **默认只在行已稳定后渲染**(不渲染流式预览)。回答完成前的围栏保持宿主原生代码块,避免每个 token 重建 React 树;
99
+ - 渲染异常由 `RenderBoundary` 捕获:隐藏容器、恢复源码块,不重试同一失败树。
100
+
101
+ ### 4.3 挂载与重挂载
102
+
103
+ - 每个源码块对应一个独立 `createRoot` 容器,容器插在源码块之后并设置 `hidden` 隐藏源块;
104
+ - 同一围栏内容变化时**原地更新**(复用 root、按渲染 key 去重),不销毁重建;
105
+ - 宿主 reconciler 可能移除外来节点:校验 `source.nextSibling === container` 并按需重插,**最多 3 次**;超限则放弃该行并恢复原生代码块,避免插入/删除拉锯;
106
+ - **每个 document scope 只允许一个渲染器实例**(HMR/重复安装会替换旧实例,而不是双挂载);
107
+ - 卸载:逐个 unmount、移除容器、恢复 `source.hidden`、断开观察者与订阅。
108
+
109
+ ### 4.4 持久化
110
+
111
+ 控件状态使用 `sessionStorage`,key 由 `[插件, 会话 id, 行锚点, 块序号, ref]` 组成;只持久化 slider/switch/select/tabs 等声明式控件,输入框、日期、Action payload 一律不写。存储失败静默禁用。
112
+
113
+ ## 5. 工具桥接(通道 B)
114
+
115
+ 见 [TOOL-BRIDGE.md](TOOL-BRIDGE.md)。
116
+
117
+ ## 6. 为什么不注册 tool view
118
+
119
+ DSH 提供 `tool.call.toolview` 按工具名键控的渲染槽。刻意**不**使用它来渲染富内容:工具阶段的内容不应冒充最终回答。工具行保持宿主默认呈现(名称、参数、结果摘要),富内容只在最终回答引用处出现。
120
+
121
+ ## 7. 构建与加载
122
+
123
+ - Host:esbuild 打包为 ESM(平台 node,仅 Node 内置依赖);
124
+ - Client:esbuild 打包为 CJS 主体,由构建脚本包进
125
+ `window.__ModuleLoader__.load({ id, factory(require) { ... } })`;
126
+ - `react` / `react/jsx-runtime` / `react-dom` / `react-dom/client` 保持 external,由 Web 外壳静态模块表提供;其余(含 lucide-react)打进 bundle;
127
+ - CSS 作为文本模块打入 JS,由插件在运行时注入一个带命名空间属性的 `<style>`,卸载时移除。
128
+
129
+ ## 8. 已知取舍
130
+
131
+ 1. **DOM 契约是观测契约,不是稳定 API**:宿主升级可能改变类名/标记。渲染器在所有判定点失败关闭(保留代码块),并在升级后需要重新跑真实宿主验证。
132
+ 2. **Snapshot 不可用时无法区分推理**:降级模式只保证"不渲染流式内容 + 仅 stable 行",无法排除推理块。文档与代码都显式记录了这一限制;真实 Web 组合中 `uiConversation` 必然存在。
133
+ 3. **引用解析优先走会话日志**:这样离线、刷新、宿主重启后仍可渲染历史回答;HTTP 路由只覆盖工具结果被压缩等边缘情况。两者都失败时显示可读诊断,不会静默丢内容。
134
+ 4. **不接管宿主 Markdown**:普通代码块、LaTeX、文件链接、图片灯箱等全部保持宿主行为。
@@ -0,0 +1,78 @@
1
+ # CHANGES
2
+
3
+ 相对上游 `kyoukisu/dsh-rich-ui@0.6.2` 的完整变更报告。
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
+
26
+ ## 1. 总体
27
+
28
+ | 维度 | 上游 | dsh-rich-msg |
29
+ |---|---|---|
30
+ | 定位 | 面向单个 Agent 产品的富回答插件(含模型行为注入) | 通用、无行为倾向的富内容渲染引擎 |
31
+ | 输出通道 | Markdown fence(DOM 适配) | Fence + Tool Bridge 双通道 |
32
+ | 模型上下文 | 自动注入 systemPrompt 段落 + 提示文件 | 零注入;工具描述纯技术 |
33
+ | 最终消息判定 | 仅 `assistant-step` 祖先 | Snapshot 最终性 + 推理排除 + 流式回撤 |
34
+ | 主题 | 自有配色 + 部分桥接 | 全量 DSH token 桥接(明/暗/移动端) |
35
+ | 构建 | Vite + tsc + 独立 demo | esbuild 双产物、无 Vite 依赖 |
36
+ | 测试 | vitest + Playwright 快照 | 15 文件 / 226 项单测与集成测试通过 + 真实宿主 DOM 冒烟;真实模型 E2E 改为人工 |
37
+
38
+ ## 2. 模块级明细
39
+
40
+ | 模块 | 上游原有能力 | 本次修改 | 修改原因 |
41
+ |---|---|---|---|
42
+ | 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`(离线文档,不被运行时读取) | 模型行为必须中立 |
43
+ | 行为指导 | prompt 中包含"默认使用""集合优先卡片""validate 后发送"等大量行为指令 | 全部移除;任何文档不得自动进入模型上下文 | 同上 |
44
+ | Validator | `rich_ui_validate`(描述含行为指导) | `rich_msg_validate`:仅描述输入/输出/无副作用;保留严格校验与错误信息 | 工具 API 保持技术性 |
45
+ | Tool Bridge | 无(仅校验) | 新增 `rich_msg_publish`、`ArtifactStore`、只读 HTTP 路由、`ref` 引用协议、浏览器端解析器 | 支持 Tool 通道且不直接展示工具阶段内容 |
46
+ | Fence 发现 | DOM MutationObserver;`rich-ui` 语言;仅 `assistant-step` 祖先 | 保留扫描框架;新增语言别名 `rich-msg`、严格 payload 特征、Chat Snapshot 校验、finality 状态机、推理排除、流式回撤、引用解析、失败关闭 | 满足"只渲染最终回答" |
47
+ | 渲染器 | RichResponse + blocks + primitives + EntityList/Details + persistence | 直接复用;命名空间/存储前缀改为 `dsh-rich-msg`;保留既有可访问性实现 | 复用成熟实现 |
48
+ | 主题 | `rich-ui.css`(67KB,自有 token + 部分 `--dsw-*` 桥接) | 保留组件样式;新增 `client/host-theme.css` 完整桥接(明/暗/375–480px) | Hybrid 视觉要求 |
49
+ | Actions | `[rich-ui-action]` 信封 + 会话捕获 + 大小上限 | 前缀改为 `[rich-msg-action]`;保留白名单/上限/会话捕获/不记录 payload | 品牌统一 |
50
+ | 实体侧栏 | `entity-tab.tsx` 注册右侧栏 tab | 保留,改为可选注入;标识符改为 `dsh-rich-msg/entity` | 保留能力 |
51
+ | 构建 | Vite 配置 + TS 检查 + 两个 vite build | `scripts/build.mjs`:esbuild 打 Host ESM 与 Client CJS,再包 ModuleLoader;CSS 作为文本模块注入 | 摆脱 Vite/npm 构建依赖,与宿主加载协议一致 |
52
+ | 测试 | 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 | 覆盖新链路、不虚构验证 |
53
+ | 文档 | README + 内部 docs + 运行时 prompt | 重写 README + 8 份技术文档 | 交付要求 |
54
+ | 包清单 | `@kyoukisu/dsh-rich-ui` | `dsh-rich-msg` | 独立项目命名 |
55
+
56
+ ## 3. 保留的上游能力
57
+
58
+ - RichSpec v1 Schema 与严格校验(`src/core/schema.ts`)
59
+ - 安全表达式求值(`src/core/evaluator.ts`)
60
+ - 流式前缀解析(`src/core/partial.ts`)
61
+ - 全部 React 组件:布局、文本、实体、图表、表格、指标、媒体、流程、控件、逻辑
62
+ - 图片灯箱、图库、popover/tooltip、实体详情面板、右侧栏实体 tab
63
+ - 控件状态持久化(命名空间调整)
64
+ - 可访问性处理(aria、键盘、读屏标签)
65
+
66
+ ## 3.1 验证状态
67
+
68
+ - 已通过:`tsc --noEmit`(0 错误)、`vitest run`(15 文件 / 226 项)、隔离 profile 真实宿主加载与 DOM 冒烟、0.1.1 事件驱动压力验证。
69
+ - 未完成(按用户要求停止自动化,改人工):真实模型对话的 Fence 通道与 Tool Bridge 引用端到端验证。
70
+ - 未验证:Android WebView、多标签页并发、超长历史压测、宿主升级后的 DOM 契约。
71
+
72
+ ## 4. 明确不做的事
73
+
74
+ - 不注册任何 prompt / persona / skill / agent preset;
75
+ - 不通过工具描述或初始化代码影响模型行为;
76
+ - 不修改 DSH 主程序;
77
+ - 不接管宿主 Markdown 渲染;
78
+ - 不在工具行渲染富内容。
@@ -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)。
@@ -0,0 +1,199 @@
1
+ # DEVELOPMENT
2
+
3
+ 开发、构建、打包、发布与人工验证手册。
4
+
5
+ ## 1. 环境
6
+
7
+ | 项 | 要求 |
8
+ |---|---|
9
+ | Node.js | ≥ 22.19(本机 24.21.0) |
10
+ | esbuild | 本机 `E:/tools/esbuild`,可用 `DSH_RICH_MSG_ESBUILD_DIR` 覆盖 |
11
+ | DSH | `@deepseek-ai/dsh@0.2.2-alpha.2`(本机 `E:\tools\dsh-cli`) |
12
+ | 依赖 | `npm install`(lucide-react 进入产物,其余为开发依赖) |
13
+
14
+ 本项目不使用 Vite;`tsc --noEmit` 仅做类型检查,构建由 esbuild 完成。
15
+
16
+ ## 2. 目录
17
+
18
+ ```
19
+ src/core RichSpec Schema / 表达式求值 / 流式前缀解析
20
+ src/host Node 插件:工具、Artifact Store、只读路由
21
+ src/client 浏览器插件:Fence 渲染、Snapshot 判定、引用解析、Action
22
+ src/ui React Renderer + rich-ui.css + primitives
23
+ src/shared 双端共享协议常量与文档解析
24
+ tests 单元/集成测试(vitest + jsdom)
25
+ scripts build.mjs / package.mjs
26
+ docs 技术文档
27
+ ```
28
+
29
+ ## 3. 构建
30
+
31
+ ```powershell
32
+ npm install
33
+ node scripts/build.mjs
34
+ ```
35
+
36
+ | 产物 | 说明 |
37
+ |---|---|
38
+ | `lib/index.js` | Host ESM 插件(esbuild bundle,platform=node,仅 Node 内置依赖) |
39
+ | `lib/client.js` | 浏览器 bundle,包装为 `window.__ModuleLoader__.load({id, factory(require){…}})` |
40
+
41
+ - React / react-dom / jsx-runtime 为 external,由 DSH Web 外壳静态模块表提供;
42
+ - lucide-react 与 CSS 文本一起打进 `lib/client.js`;
43
+ - `package.json` 的 `prepare` 会在 `npm install`(包括 git 依赖)时自动构建。
44
+
45
+ 最近一次构建结果:
46
+
47
+ ```
48
+ lib/index.js 48,876 bytes
49
+ lib/client.js 346,449 bytes (0.1.1)
50
+ host esbuild warnings: 0
51
+ tsc --noEmit: 0 errors
52
+ ```
53
+
54
+ ## 4. 测试(已通过,结果保留)
55
+
56
+ ```powershell
57
+ npx tsc --noEmit
58
+ npx vitest run
59
+ ```
60
+
61
+ ```
62
+ Test Files 15 passed (15)
63
+ Tests 226 passed (226)
64
+ ```
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
+
68
+ 覆盖范围:
69
+
70
+ | 测试文件 | 覆盖 |
71
+ |---|---|
72
+ | `tests/core/schema.test.ts` | RichSpec 全字段校验、非法输入、上限、实体/引用/图表/表格/控件 |
73
+ | `tests/core/evaluator.test.ts` | 表达式求值、算子树、状态、边界 |
74
+ | `tests/shared/protocol.test.ts` | ref 协议、Action 信封、id 语法、大小上限 |
75
+ | `tests/host/artifacts.test.ts` | 会话隔离、TTL、容量、淘汰、清理 |
76
+ | `tests/host/tools.test.ts` | 工具 Schema、中性描述、校验/发布行为、取消信号 |
77
+ | `tests/host/route.test.ts` | 只读路由:200/404/405/HEAD、跨会话拒绝 |
78
+ | `tests/host/neutrality.test.ts` | 无 prompt/persona/skill 注入、无运行时文件读取 |
79
+ | `tests/client/chat-view.test.ts` | 最终性判定、reasoning/text 区分、Artifact 索引 |
80
+ | `tests/client/fence-claim.test.ts` | 语言门控、通用标签 payload 特征、非法文档 |
81
+ | `tests/client/dom-renderer.test.tsx` | 挂载/还原、中间步骤/推理/工具行不渲染、诊断、回撤 |
82
+ | `tests/client/artifact-resolver.test.ts` | 会话日志解析、路由兜底、缺失处理 |
83
+ | `tests/ui/*` | 上游 RichResponse / primitives / persistence / entityTab 回归 |
84
+
85
+ > 按交付要求,自动化 CDP 模型端到端测试不再继续,真实模型验证改为人工执行(见第 5 节)。
86
+
87
+ ## 5. 在真实 DSH 中人工验证
88
+
89
+ ### 5.1 安装到独立 profile
90
+
91
+ ```powershell
92
+ # 1) 构建
93
+ node scripts/build.mjs
94
+
95
+ # 2) 新建 profile(不修改在用的 web profile)
96
+ $profile = "$env:USERPROFILE\.dsh\profiles\richmsg-test"
97
+ New-Item -ItemType Directory -Force "$profile\node_modules" | Out-Null
98
+
99
+ # profile/package.json
100
+ @'
101
+ {
102
+ "name": "dsh-profile-richmsg-test",
103
+ "private": true,
104
+ "dependencies": { "dsh-rich-msg": "link:E:/DSH_WORKSPACE/dsh-rich-msg" },
105
+ "dsh": { "profile": { "bundles": [
106
+ "@deepseek-ai/dsh-base",
107
+ "@deepseek-ai/dsh-web-app",
108
+ "dsh-rich-msg"
109
+ ], "patchReload": "live" } }
110
+ }
111
+ '@ | Set-Content "$profile\package.json" -Encoding utf8
112
+
113
+ # 3) 建立包链接
114
+ New-Item -ItemType Junction -Path "$profile\node_modules\dsh-rich-msg" -Target "E:\DSH_WORKSPACE\dsh-rich-msg"
115
+
116
+ # 4) 启动(日志会打印带 token 的 URL)
117
+ node <dsh>/lib/bin.js --profile richmsg-test --host 127.0.0.1 --port 58470 --no-open
118
+ ```
119
+
120
+ > 开发期已用同样的独立 profile 验证过:插件出现在 boot 图、模块经 `/plugins/…` 正常分发、浏览器半生效、Fence 渲染/工具行/普通代码/流式行行为符合预期、卸载还原、零控制台错误;并留下浅色/深色/390px 截图(本地开发证据,未纳入仓库)。
121
+
122
+ ### 5.2 人工验收清单
123
+
124
+ 1. **Fence 通道**:让模型只输出一个 `rich-msg` 代码块(例如 `{"version":1,"nodes":[{"type":"text","content":"Hello"},{"type":"stat","label":"Score","value":42}]}`),确认最终回答中原位置出现组件、源码块隐藏。
125
+ 2. **推理排除**:使用会输出 reasoning 的模型,让推理中出现 `rich-msg` 围栏,确认推理区域不渲染富内容。
126
+ 3. **工具阶段排除**:调用 `rich_msg_publish` 后,确认工具行只显示普通工具卡片,没有富内容。
127
+ 4. **引用渲染**:最终回答写 `{"version":1,"ref":"<publish 返回的 id>"}`,确认渲染位置正确。
128
+ 5. **未引用不展示**:publish 后最终回答不写引用,确认页面不出现该 artifact。
129
+ 6. **流式**:观察生成过程中只出现已闭合的合法内容,并在该步骤转为中间步骤时回撤。
130
+ 7. **非法 JSON**:流式中保持代码块;稳定后显示诊断卡片(含错误与源码复制)。
131
+ 8. **普通代码块/其它语言**:完全不受影响。
132
+ 9. **刷新/历史重放**:刷新后历史回答仍渲染;控件状态按会话/行恢复。
133
+ 10. **主题与窄屏**:浅色/深色、约 390px 宽度。
134
+ 11. **卸载**:禁用插件后页面恢复原生渲染,无残留 DOM/样式。
135
+
136
+ ## 6. 打包
137
+
138
+ ```powershell
139
+ node scripts/package.mjs
140
+ ```
141
+
142
+ ```
143
+ dist/dsh-rich-msg-<version>.zip # 可直接解压进 profile 的 node_modules
144
+ dist/dsh-rich-msg-<version>.zip.sha256
145
+ ```
146
+
147
+ ZIP 内容:`package.json`、`cordis.patch.yml`、`lib/index.js`、`lib/client.js`、`README.md`、`LICENSE`、`docs/*.md`。
148
+ 打包脚本会自动先执行构建。
149
+
150
+ 最近一次打包结果:`dsh-rich-msg-0.1.0.zip`(123,812 bytes)。
151
+
152
+ ## 7. 发布到 GitHub(不使用 force push)
153
+
154
+ 目标仓库:`https://github.com/Htao-uu/dsh-rich-msg`(上游 fork)。
155
+
156
+ 安全规则:
157
+
158
+ 1. **绝不 force push**;
159
+ 2. 新提交的父提交必须是远端 `main` 当前提交,保证 fast-forward;
160
+ 3. 远端已有文件(图片等)不因本地缺失而被误删;
161
+ 4. 推送前后用 `git ls-remote origin refs/heads/main` 核对。
162
+
163
+ 流程:
164
+
165
+ ```powershell
166
+ git remote add origin https://github.com/Htao-uu/dsh-rich-msg.git
167
+ git fetch --depth 1 --filter=blob:none origin main # 只取提交与树,不下载二进制
168
+ git reset --mixed FETCH_HEAD # index = 远端树
169
+ git add -A # 应用本次重构(含删除)
170
+ git commit -F commit-message.txt
171
+ git push origin HEAD:main # 普通 push
172
+ git ls-remote origin refs/heads/main # 与本地 HEAD 一致
173
+ ```
174
+
175
+ 若直连 Git 不可用,可改用 GitHub REST API(`api.github.com`):以远端 main SHA 为父提交创建 blob/tree/commit 并更新 ref,同样不使用 force。
176
+
177
+ ## 8. 宿主升级后的复核
178
+
179
+ 1. 重新检查宿主类型:`dsh-client-ui-chat`(slot 契约)、`dsh-client-ui-conversation`(snapshot)、`dsh-host-webserver`(路由)、`dsh-tools`(工具契约);
180
+ 2. `npx tsc --noEmit` 与 `npx vitest run`;
181
+ 3. 在独立 profile 中按第 5 节人工复验;
182
+ 4. 重点复核 DOM 标记:`data-chat-flow-kind` / `data-chat-node-key` / `data-code-block-content` / `data-ds-dark-theme`;
183
+ 5. DOM 变化时渲染器失败关闭(保留原生代码块),需更新 `src/client/dom-renderer.tsx` 选择器后复验。
184
+
185
+ ## 9. 未验证 / 已知限制
186
+
187
+ - **真实模型 Fence / Tool 双通道端到端**:未由自动化验证(按交付要求改为人工,见 5.2);判定与解析逻辑由单测覆盖。
188
+ - 宿主若改变工具结果/快照结构,引用解析需要重新验证。
189
+ - 多标签页、Android WebView、超长历史压测:未验证。
190
+ - 进程内 Artifact Store 不跨进程共享;历史引用仍依赖会话日志路径。
191
+ - `sessionStorage` 持久化仅标签内有效。
192
+
193
+ ## 10. 代码约定
194
+
195
+ - 双端共享代码放 `src/shared` 或 `src/core`,禁止引入 Node/浏览器专属 API;
196
+ - React 组件不得直接依赖 Cordis 服务;
197
+ - 所有交互副作用必须来自用户点击;
198
+ - 新增节点类型必须同时更新 `src/core/schema.ts` 与 `docs/COMPONENTS.md`;
199
+ - 不要重新引入 prompt/persona/skill 注入,由 `tests/host/neutrality.test.ts` 守护。