# dsh-markstream [English](README.md) | 中文 DSH 网页端插件:用 [markstream-react](https://www.npmjs.com/package/markstream-react)(Markstream 家族的 React 渲染器)渲染对话流中的助手消息。 Markstream 是一个面向 AI 聊天流式输出的 Markdown 渲染器家族,仓库为 [Simon-He95/markstream-vue](https://github.com/Simon-He95/markstream-vue),按前端框架分发多个 npm 包(`markstream-vue` / `markstream-react` / Svelte / Angular 等)。DSH 的网页客户端是 React,所以本插件使用正确的 **`markstream-react`** 包(^2.0.1,`react >= 18`,与 DSH 的 react 18.2 匹配)。 ## 它做什么 - 将 `conversation.chat.node` 键控槽中的 **`assistant-step`** 渲染器替换为基于 markstream 的实现,注册时带 `priority: -10`(槽位优先级升序、最低者渲染)以盖过出厂实现默认优先级 0 的同键条目——同键 + 同优先级注册会直接抛错: - 文本块通过 `` 渲染,流式增量低抖动(low-jitter),支持不完整 Markdown 的中间态渲染; - 支持流式代码块(可选 `stream-diffs`)、Mermaid(可选 `mermaid`)、KaTeX(当前构建已随依赖树内联 `katex`)、安全 HTML; - 思考(reasoning)块保持 Think 折叠行、图片组保持附件画廊、未知块保持 JSON 回退、中断消息保持“已停止”标记——与出厂渲染器呈现一致; - 轮次尾巴节点(`turn-tail`,含 IconActions / 产物文件行)与工具行渲染仍是产品自带实现。 - 渲染器的 `t` 语言座由槽位声明绑定到 `conversation` 命名空间,因此直接复用 ui-conversation 的字典(`row.running` / `message.stopped` / `message.unknownBlock` / `json.truncated`),不注册新命名空间。 ## 页面配置(悬浮齿轮) 配置参照 [演示站 markstream-react.pages.dev](https://markstream-react.pages.dev/) 的设置(CODE THEME / DARK MODE 等)、[组件文档 markstream.simonhe.me/zh/guide/components.html](https://markstream.simonhe.me/zh/guide/components.html) 与 [代码块推荐配置 markstream.simonhe.me/zh/guide/code-blocks.html](https://markstream.simonhe.me/zh/guide/code-blocks.html#stream-diffs-surface-%E6%8E%A8%E8%8D%90) 设计,落地为双层: 1. **composition 层**:插件行的 `config`(见 [cordis.patch.yml](cordis.patch.yml) 内完整模板)——部署给出的默认值,经 `Config` schema 校验并作为 settings 命名空间的 `base`。 2. **用户层**:**页面右下角悬浮齿轮**(`shell.overlay` 条目 `dsh-markstream-config`)→ 打开**非模态悬浮面板**(演示站同款:右侧抽屉、不遮挡、页面其余部分可继续操作);面板头部 × 关闭,关闭即回到齿轮形态。逐字段覆盖并持久化到 `$DSH_HOME/settings.yaml`;字段旁"重置"清除用户覆盖并回退 composition 值(用户层字段**存在性**即"已覆盖"标记)。字段经 revision-fenced 的 settings scope 写入,改动即时生效(渲染器订阅配置变化后自动重装)。 > 设置 → 插件 → 配置 中不再提供 Markstream 卡片;配置入口统一为右下角齿轮面板。 字段(扁平键,面板分区顺序): | 字段 | 默认 | 说明 | | --- | --- | --- | | `enabled` | `true` | **启用状态**:`false` 时恢复出厂 Markdown 渲染(齿轮面板仍可重新打开) | | `theme` | `auto` | `auto` 跟随 DSH 主题(`body[data-ds-dark-theme]`)/ `light` / `dark`(对应 MarkdownRender `isDark`) | | `fade` | `true` | 流式非代码节点的淡入动画 | | `typewriter` | `false` | 流式内容的光标动画 | 代码块(`codeBlockProps` 头部开关 + `codeBlockOptions`,对齐 [stream-diffs surface 推荐](https://markstream.simonhe.me/zh/guide/code-blocks.html#stream-diffs-surface-%E6%8E%A8%E8%8D%90);生效需 `stream-diffs`,未安装时回退普通 `
`):

| 字段 | 默认 | 说明 |
| --- | --- | --- |
| `codeBlockShowHeader` | `true` | 显示代码块头部 |
| `codeBlockShowTooltips` | `true` | 显示提示气泡 |
| `codeBlockShowFontSizeButtons` | `true` | 显示字号按钮 |
| `codeBlockShowCollapseButton` | `false` | 显示折叠按钮 |
| `codeBlockDiffStyle` | `unified` | `unified` / `split` |
| `codeBlockOverflow` | `wrap` | `wrap` / `scroll` |
| `codeBlockExpandUnchanged` | `false` | 折叠未变更的 diff 区域 |
| `codeBlockEnableLineSelection` | `true` | 允许行选择 |
| `codeBlockDisableLineNumbers` | `false` | 隐藏行号 |
| `codeBlockFontSize` | `13` | 代码字号(px,推荐 13) |
| `codeBlockTabSize` | `2` | Tab 宽度 |
| `codeBlockPadding` | `12` | 上下对称内边距(px) |
| `codeBlockMaxHeight` | `480` | 最大高度(px,`0` = 不限) |
| `codeBlockLightTheme` | `vitesse-light` | 亮色代码主题(Shiki 注册名;默认对见文档) |
| `codeBlockDarkTheme` | `vitesse-dark` | 暗色代码主题 |

图表(需对应可选依赖,未安装自动回退源码展示):

| 字段 | 默认 | 说明 |
| --- | --- | --- |
| `mermaidEnabled` | `false` | 转发 `mermaidProps`(需 `mermaid`) |
| `mermaidIsStrict` | `true` | `mermaidProps.isStrict` |
| `mermaidMaxHeight` | `480` | `mermaidProps.maxHeight`(px,`0` = 不限) |
| `d2Enabled` | `false` | 转发 `d2Props`(需 `@terrastruct/d2`) |
| `d2MaxHeight` | `480` | `d2Props.maxHeight`(px,`0` = 不限) |

## 目录结构

```
dsh-markstream/
├── package.json                  # dsh.client manifest + dsh.bundle.patch 声明
├── cordis.patch.yml              # 插件行 insert + composition config 模板(bundle 通道)
├── tsconfig.json                 # typecheck(@deepseek-ai/* 类型来自 npm peer 0.1.0-rc.8)
├── tsconfig.build.json           # tsc 全量发射到 lib/types(JS + d.ts)
├── tsdown.config.ts              # 产出 lib/client.js(浏览器半区)
├── scripts/stage-node.mjs        # 把 Node 半区文件从 lib/types 落到 lib/
└── src/
    ├── index.ts                  # Node 半区:注册 dsh-markstream settings 命名空间(base = 行 config)
    ├── schema.ts                 # 扁平 schema(schemastery z,行 Config = settings 命名空间 schema)
    ├── config.ts                 # 共享字段定义 / 默认值 / 选项表
    ├── invariant.ts              # invariant 伴随文件
    └── client/
        ├── index.ts              # 浏览器半区:shell.overlay 悬浮面板 + assistant-step 渲染器
        ├── card-store.ts         # 快照 store(settings scope 绑定 + set/unset)
        ├── ConfigSurface.tsx     # 右下角齿轮 + 非模态悬浮面板(分区字段行)
        ├── ConfigSurface.module.css
        ├── ConfigFields.tsx      # 共享字段行(演示站风格:分区标题 + 行分隔线)
        ├── ConfigFields.module.css
        ├── AssistantMarkstream.tsx   # 配置驱动的 assistant-step 渲染器(markstream-react)
        ├── AssistantMarkstream.module.css
        ├── locales.ts            # dsh-markstream 字典(zh/en)
        └── css-modules.d.ts
```

## 构建

```bash
pnpm install
pnpm build        # tsc -p tsconfig.build.json && node scripts/stage-node.mjs && tsdown
```

产物:

- `lib/client.js` —— 浏览器半区,DSH 客户端束格式(`window.__ModuleLoader__.load({ id, factory })`):`markstream-react` / `markstream-core` / `stream-markdown-parser` / `@floating-ui/*` / `katex` 全部内联为单个文件(`inlineDynamicImports`,模块加载器只服务 `/plugins//client.js`);`react` / `react-dom` / `react/jsx-runtime` / `@deepseek-ai/dsh-client-ui-primitives` 保持模块表外部依赖;`markstream-react/index.css` 编译后以 `data-plugin-css` 样式标签注入。
- `lib/index.js` / `lib/invariant.js` —— Node 半区(连同 `config.js` / `schema.js` 由 stage-node 落到 lib/,Node 直接解析 `.js` 相对导入;`@deepseek-ai/dsh-settings` / `@deepseek-ai/schemastery` 保持外部依赖)。
- `lib/types/**/*.d.ts` —— 声明文件。

`@deepseek-ai/*` 类型来自自动安装的 npm peer(0.1.0-rc.8,与运行中的 GUI 同版本);运行时由 DSH 客户端模块表提供。

## 安装到 DSH profile(web)

本包声明了 `dsh.bundle.patch`,可用官方 CLI 通道安装:

```bash
dsh plugin --profile web add dsh-markstream
```

本地未发布的包,手动接线(本机 `~/.dsh/profiles/web`):

1. 把包加入 profile 依赖并安装(heal node_modules)。**本地开发请用 `link:`**:`file:` 会被 pnpm 打包拷贝,每次改插件都要重装;`link:` 是软链接,`pnpm build` 重建 `lib/client.js` 后服务直接读到新文件(client-hmr 轮询到变化会自动热更,无需重启):

   ```bash
   # 编辑 ~/.dsh/profiles/web/package.json
   #   "dependencies": { "dsh-markstream": "link:D:/l78z/xx_test/dsh-markstream" }
   #   "dsh": { "profile": { "bundles": [ ..., "dsh-markstream" ] } }
   cd ~/.dsh/profiles/web && pnpm install
   ```

2. 重启 `dsh web` 并刷新页面。插件行由 `cordis.patch.yml` 的 `insert` 挂载;也可手动在 profile 的 `cordis.patch.yml` 追加:

   ```yaml
   - insert:
       - id: markstream
         name: 'dsh-markstream'
   ```

## 可选功能(markstream 可选 peer)

| 功能 | 包 | 说明 |
| --- | --- | --- |
| 增强代码块 | `stream-diffs`(已内置依赖) | File/FileDiff surface + 语法高亮;主题名解析注册后取精确色,未注册时按文档回退缝(`--markstream-code-fallback-*`)自动取选择主题的 bg/fg |
| Mermaid 图表 | `mermaid` | 未安装时 mermaid 块降级 |
| KaTeX 数学 | `katex` | 当前构建已随 `@deepseek-ai/dsh-client-ui-primitives` 依赖树内联 |
| D2 图表 | `@terrastruct/d2` | 构建时未解析,按外部处理(仅渲染 d2 块时才会用到) |
| 信息图 | `@antv/infographic` | 同上,按外部处理 |

## 已知限制

- **逐块渲染**:每个文本块独立交给 `MarkdownRender`(与出厂实现逐块渲染一致);跨块割裂的 Markdown 结构(如代码围栏横跨两个文本块)不会跨块合并。
- **文件提及(file mentions)增强缺失**:出厂 `MarkdownText` 的 `fileMentions` 内联代码高亮没有在 v1 复刻(markstream 按普通内联代码渲染)。
- **Think 行无尾部跟随滚动**:折叠摘要跟随最新行,但没有出厂实现的自动横向跟随滚动。
- **替换了出厂渲染器**:`assistant-step` 为键控槽,注册同名 key 会整体替换出厂实现;本插件复刻了文本/思考/图片/未知块/停止标记的呈现,但后续产品改动不会自动同步。
- **面板仅渲染设置**:右下角齿轮面板只编辑/持久化渲染字段(`enabled`/`theme` 等);设置写入持久化到 `$DSH_HOME/settings.yaml`。