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}\n`; },