Browse Source

fix(sdk): 修复 marked 列表渲染异常导致回复挤成一行

marked v15 的 Parser.parse 按 token.type 做 switch 分发、无 list_item 分支,list(token) 用 this.parser.parse([item]) 会抛异常,被 renderMarkdown 降级为纯文本,导致换行折叠成一行、Markdown 全部失效。改为官方写法 this.listitem(item)。

- 修正 marked 版本注释 v18 → v15.0.12(与部署产物实际版本一致)
- 同步更新 client/CLAUDE.md 与 README.md 的 Markdown 模块说明与部署依赖
- .gitignore 忽略 sdk 构建产物目录
TDesign-Vue-Next-1.20.6
wanghanlin 2 weeks ago
parent
commit
c22d044e5c
  1. 1
      .gitignore
  2. 4
      client/CLAUDE.md
  3. 6
      client/README.md
  4. 10
      client/src/markdown.ts

1
.gitignore

@ -46,3 +46,4 @@ build/
/src/main/resources/static/index.html
/logs/
/node/
/src/main/resources/static/sdk/

4
client/CLAUDE.md

@ -68,7 +68,7 @@ init(rawConfig)
- **`api.ts`**:HTTP 封装 + SSE 流解析。三种 SSE 接口(普通流式、RAG 流式、同步)的解析逻辑都在这里,注意流式解析的边界处理。
- **`dom.ts`**:纯 DOM 构建与事件绑定,含知识库下拉、RAG 来源卡片、历史会话面板的渲染。
- **`styles.ts`**:所有 CSS 字符串模板,按 `primaryColor` 动态着色。改动 UI 视觉改这里,**不要**在 `dom.ts` 里写内联样式。
- **`markdown.ts`**零依赖轻量 Markdown 渲染器。**安全模型**:所有非代码块内容先 HTML 转义再转换语法,代码块内容同样转义;链接只允许 http/https。修改渲染器时必须维持这一 XSS 防护顺序。
- **`markdown.ts`**基于 marked(源文件 `assets/marked.min.js`,实际为 **v15.0.12**,构建时由 rollup 自动复制到 `dist/` 与后端 `static/sdk/`)的 Markdown 渲染器,`preloadMarked` 异步加载并缓存安全实例。**安全模型**:自定义 renderer 全接管输出,链接/图片仅允许 http/https,禁止 raw HTML,代码内容 escapeHtml 转义;修改渲染器时必须维持这一 XSS 防护顺序。**列表渲染坑**:`list(token)` 渲染列表项必须调用 `this.listitem(item)`,**不可** `this.parser.parse([item])` —— marked v15 的 `Parser.parse``token.type` 做 switch 分发、无 `list_item` 分支,直接 parse 会抛 `Token with "list_item" type was not found`,被 `renderMarkdown` 的 catch 降级为纯文本,导致整段回复挤成一行。
### 单例与生命周期
@ -102,4 +102,4 @@ init(rawConfig)
## 部署
构建后将 `dist/chatbot-sdk.min.js` 上传到后端 `src/main/resources/static/sdk/`(或 CDN),宿主页面通过 `<script src="/sdk/chatbot-sdk.min.js"></script>` 引入。`README.md` 有完整接入示例与全部配置参数表,文档是 SDK 对外契约的一部分,改公开 API(`init/destroy/open/close/toggle/clearHistory`)或 `SDKConfig` 字段时同步更新 README。
构建脚本(`rollup.config.js` 的 `copyAssets` 插件)会自动把 `chatbot-sdk.js` / `chatbot-sdk.min.js`、`marked.min.js`(运行时依赖,由 `markdown.ts` 动态加载)和 `launcher-logo.png` 复制到后端 `src/main/resources/static/sdk/`,宿主页面通过 `<script src="/sdk/chatbot-sdk.min.js"></script>` 引入;CDN 分发时需同时部署这三类文件,且 `marked.min.js` 必须与 SDK 同目录(`getSdkBasePath()` 从 script src 推断加载路径)。`README.md` 有完整接入示例与全部配置参数表,文档是 SDK 对外契约的一部分,改公开 API(`init/destroy/open/close/toggle/clearHistory`)或 `SDKConfig` 字段时同步更新 README。

6
client/README.md

@ -133,6 +133,8 @@ AI 回复支持 Markdown 格式渲染,包括:
> **XSS 安全**:所有非代码块内容先转义 HTML,再转换 Markdown 语法为安全 HTML 标签。代码块内容同样转义。
> **实现说明**:渲染基于 marked v15.0.12(外部脚本 `marked.min.js`,SDK 运行时异步加载,未就绪时降级为纯文本)。自定义 renderer 的列表项须用 `this.listitem(item)` 渲染,不能 `this.parser.parse([item])` —— marked v15 的 `Parser.parse``list_item` 分支,会抛异常并降级为纯文本,导致整段回复换行折叠成一行。
### 4.3 知识库联动
**方式一:直接启用 RAG(无需分类选择)**
@ -352,7 +354,7 @@ z-index 分层:悬浮按钮 9998,弹窗 9999。
- **语言**:TypeScript → ES2017
- **打包**:Rollup → IIFE 格式
- **压缩**:Terser
- **Markdown**内置轻量级渲染器(零外部依赖
- **Markdown**基于 marked v15.0.12(`assets/marked.min.js`,构建时自动复制到 `static/sdk/`
- **国际化**:内置 i18n 字典
- **浏览器兼容**:Chrome 70+, Firefox 70+, Safari 13+, Edge 79+
- **产物大小**:`.min.js` ~45KB(gzip ~15KB)
@ -389,7 +391,7 @@ client/
│ ├── dom.ts # DOM 构建 + 拖拽 + P1 来源/分类 + P2 面板
│ ├── styles.ts # CSS 注入 + 主题 + P1 Markdown/来源 + P2 面板
│ ├── chat.ts # 对话核心 + P1 Markdown/RAG + P2 i18n
│ ├── markdown.ts # P1 轻量级 Markdown 渲染器
│ ├── markdown.ts # P1 Markdown 渲染器(基于 marked v15)
│ ├── i18n.ts # P2 多语言国际化
│ └── utils.ts # UUID、防抖、XSS 转义
├── dist/ # 构建产物

10
client/src/markdown.ts

@ -1,7 +1,7 @@
/**
* Markdown - marked
*
* marked v18 SDK 宿
* marked v15.0.12 SDK 宿
*
*
* XSS
@ -53,8 +53,8 @@ function setupMarked(m: any): void {
// 保存到闭包变量,后续 renderMarkdown 只使用此引用,防止 window.marked 被篡改
_markedInstance = m;
// 全局选项:不自动转换换行为 <br>,启用 GFM
m.setOptions({ breaks: false, gfm: true });
// 全局选项:聊天场景保留换行,启用 GFM
m.setOptions({ breaks: true, gfm: true });
m.use({
renderer: {
@ -107,7 +107,9 @@ function setupMarked(m: any): void {
const tag = token.ordered ? 'ol' : 'ul';
const cls = token.ordered ? 'csk-md-ol' : 'csk-md-ul';
const startAttr = token.ordered && token.start !== 1 ? ` start="${token.start}"` : '';
const body = token.items.map((item: any) => this.parser.parse([item])).join('');
// 列表项须用 this.listitem(item) 渲染,不可 this.parser.parse([item]):
// marked v15 的 Parser.parse 按 token.type 做 switch 分发,无 "list_item" 分支,直接 parse 会抛异常
const body = token.items.map((item: any) => this.listitem(item)).join('');
return `<${tag} class="${cls}"${startAttr}>\n${body}</${tag}>\n`;
},

Loading…
Cancel
Save