From c22d044e5ce5d2dc19cefde116b374cf137b254e Mon Sep 17 00:00:00 2001
From: wanghanlin <1533525126@qq.com>
Date: Thu, 13 Aug 2026 09:58:09 +0800
Subject: [PATCH] =?UTF-8?q?fix(sdk):=20=E4=BF=AE=E5=A4=8D=20marked=20?=
=?UTF-8?q?=E5=88=97=E8=A1=A8=E6=B8=B2=E6=9F=93=E5=BC=82=E5=B8=B8=E5=AF=BC?=
=?UTF-8?q?=E8=87=B4=E5=9B=9E=E5=A4=8D=E6=8C=A4=E6=88=90=E4=B8=80=E8=A1=8C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
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 构建产物目录
---
.gitignore | 1 +
client/CLAUDE.md | 4 ++--
client/README.md | 6 ++++--
client/src/markdown.ts | 10 ++++++----
4 files changed, 13 insertions(+), 8 deletions(-)
diff --git a/.gitignore b/.gitignore
index a625ac4..0ac4bf6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -46,3 +46,4 @@ build/
/src/main/resources/static/index.html
/logs/
/node/
+/src/main/resources/static/sdk/
diff --git a/client/CLAUDE.md b/client/CLAUDE.md
index 435920c..1c0f243 100644
--- a/client/CLAUDE.md
+++ b/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),宿主页面通过 `` 引入。`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/`,宿主页面通过 `` 引入;CDN 分发时需同时部署这三类文件,且 `marked.min.js` 必须与 SDK 同目录(`getSdkBasePath()` 从 script src 推断加载路径)。`README.md` 有完整接入示例与全部配置参数表,文档是 SDK 对外契约的一部分,改公开 API(`init/destroy/open/close/toggle/clearHistory`)或 `SDKConfig` 字段时同步更新 README。
diff --git a/client/README.md b/client/README.md
index 06f65af..1867169 100644
--- a/client/README.md
+++ b/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/ # 构建产物
diff --git a/client/src/markdown.ts b/client/src/markdown.ts
index 6d6d065..b0c6f27 100644
--- a/client/src/markdown.ts
+++ b/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;
- // 全局选项:不自动转换换行为
,启用 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`;
},