Skip to content

Conversation

@clown145
Copy link
Contributor

@clown145 clown145 commented Jan 11, 2026

本次 PR 旨在重构 Dashboard 的 Markdown 渲染逻辑,以解决现有的渲染兼容性问题并提升安全性。主要解决以下问题:

  1. 渲染修复:修复 Shields.io 徽章、GithHub 风格表格、删除线以及 <details>/<summary> 折叠块无法正确显示的问题。
  2. 安全性增强:引入 DOMPurify 进行 HTML 清洗,防止 XSS 攻击;强制所有外部链接添加 rel="noopener noreferrer"target="_blank"
  3. 体验优化:引入 highlight.js 实现代码高亮,并添加了带有交互反馈的复制代码按钮。
  4. 代码清理:移除项目中未使用的依赖和冗余的 DOM 操作代码。

Modifications / 改动点

核心修改文件:

  1. dashboard/src/components/shared/ReadmeDialog.vue:
    • 使用 markdown-it 替换原有的渲染逻辑。
    • 集成 DOMPurify 配置安全白名单。
    • 集成 highlight.js 并自定义代码块渲染器(支持语言标签和复制按钮)。
    • 完善 CSS 样式以支持 GitHub 风格的 Markdown(表格、引用、折叠块等)。
  2. dashboard/package.json:
    • [NEW] 添加依赖:markdown-it, dompurify, highlight.js 及其类型定义。
    • [REMOVE] 移除未使用的依赖:@mdit/plugin-katex

实现功能:

  • ✅ 完整的 GFM (GitHub Flavored Markdown) 支持。
  • ✅ 安全的 HTML 渲染。
  • ✅ 优化的代码块显示与交互。
  • This is NOT a breaking change. / 这不是一个破坏性变更。

Screenshots or Test Results / 运行截图或测试结果

chrome_RsKbDCt6IC chrome_NKPuSwk450

Local CI Status:

  • npm run build: ✅ Passed (Vue-TSC Type Check OK)
  • Dependency Audit: ✅ Cleaned unused packages

Checklist / 检查清单

  • 😊 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。/ If there are new features added in the PR, I have discussed it with the authors through issues/emails, etc.
  • 👀 我的更改经过了良好的测试,并已在上方提供了“验证步骤”和“运行截图”。/ My changes have been well-tested, and "Verification Steps" and "Screenshots" have been provided above.
  • 🤓 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到了 package.json 文件相应位置。/ I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations in package.json.
  • 😮 我的更改没有引入恶意代码。/ My changes do not introduce malicious code.

Summary by Sourcery

将仪表盘 README 弹窗重构为使用自定义的 Markdown-It + DOMPurify + highlight.js 渲染管线,以实现更安全、更丰富的 Markdown 渲染和改进的代码块交互体验。

New Features:

  • 在 README 弹窗中支持 GitHub 风格的 Markdown 元素,包括表格、删除线、details/summary 折叠块以及徽章样式图片。
  • 在 README 弹窗中新增强化的代码块渲染:带语法高亮、语言标签以及交互式一键复制到剪贴板按钮。

Bug Fixes:

  • 修复 README 弹窗中 Markdown 内容渲染不正确或不完整的问题,包括 shields 徽章、表格、删除线和可折叠内容块。

Enhancements:

  • 通过 DOMPurify 清理并为外部链接自动添加安全属性,加强 README HTML 渲染的安全性。
  • 更新 README 弹窗中的 Markdown 与表格样式,使其更贴近 GitHub 风格展示,并更好适配深色主题视觉效果。

Build:

  • 调整仪表盘依赖:新增 markdown-it、dompurify 及其类型定义,同时移除未使用的 Markdown 插件依赖。
Original summary in English

Summary by Sourcery

Refactor the dashboard README dialog to use a custom Markdown-It + DOMPurify + highlight.js pipeline for safer, richer markdown rendering and improved code block UX.

New Features:

  • Support GitHub-flavored markdown elements in the README dialog, including tables, strikethrough, details/summary blocks, and badge-style images.
  • Add enhanced code block rendering with syntax highlighting, language labels, and an interactive copy-to-clipboard button in the README dialog.

Bug Fixes:

  • Fix incorrect or incomplete rendering of markdown content in the README dialog, including shields badges, tables, strikethrough, and collapsible sections.

Enhancements:

  • Harden README HTML rendering with DOMPurify sanitization and automatic secure attributes on external links.
  • Update markdown and table styling in the README dialog to better align with GitHub-style presentation and dark theme aesthetics.

Build:

  • Adjust dashboard dependencies by adding markdown-it, dompurify, and their type definitions while removing an unused markdown plugin dependency.

@dosubot dosubot bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Jan 11, 2026
Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - 我发现了 3 个问题,并给出了一些整体性的反馈:

  • 在模块作用域中使用全局的 DOMPurify.addHook('afterSanitizeAttributes', ...) 会影响到应用中所有对 DOMPurify 的使用;如果你只想在这里启用这一行为,建议使用一个独立的 DOMPurify 实例,或者只在本次 sanitize 调用前后注册/注销 hook,而不是全局注册。
  • 复制按钮的文案和 title 属性被硬编码为中文(复制代码);既然这个组件的其他部分已经使用了 useI18n,会更一致的做法是从 i18n 资源中读取这些文案。
给 AI Agent 的提示词
Please address the comments from this code review:

## Overall Comments
- The global `DOMPurify.addHook('afterSanitizeAttributes', ...)` at module scope will affect all DOMPurify usages in the app; if you only want this behavior here, consider using a dedicated DOMPurify instance or registering/unregistering the hook around the sanitize call instead of globally.
- The copy button label and `title` attribute are hardcoded in Chinese (`复制代码`); since the rest of the component uses `useI18n`, it would be more consistent to pull these strings from the i18n resources.

## Individual Comments

### Comment 1
<location> `dashboard/src/components/shared/ReadmeDialog.vue:232-233` </location>
<code_context>
 );

+// 监听 renderedHtml 变化,初始化复制按钮
+watch(renderedHtml, () => {
+  nextTick(() => {
+    initCopyButtons();
+  });
</code_context>

<issue_to_address>
**issue (bug_risk):** 每次渲染都会给复制按钮添加监听器但没有清理,这可能导致重复的事件处理和内存泄漏。

当 `renderedHtml` 变化时,`initCopyButtons` 会重新执行,并对所有 `.copy-code-btn` 元素调用 `addEventListener`,这样同一个按钮会积累多个点击处理函数并且保留不必要的引用。为避免这种情况,可以使用事件委托(例如在 `markdownContainer` 上挂一个 click 监听,并检查 `event.target.closest('.copy-code-btn')`),或者确保移除旧的监听器(例如通过 Vue 渲染按钮并使用 `@click`)。
</issue_to_address>

### Comment 2
<location> `dashboard/src/components/shared/ReadmeDialog.vue:251-257` </location>
<code_context>
+}
+
+// 处理复制代码
+function handleCopyCode(event) {
+  const btn = event.currentTarget;
+  const wrapper = btn.closest(".code-block-wrapper");
+  const code = wrapper.querySelector("code");
+
+  if (code) {
+    navigator.clipboard
+      .writeText(code.textContent)
+      .then(() => {
</code_context>

<issue_to_address>
**issue:** 假设 `navigator.clipboard` 总是可用,在不支持或非安全环境下可能会出错。

当 Clipboard API 不可用,或者页面不是通过 HTTPS 提供时,`navigator.clipboard.writeText` 会抛出异常。请添加能力检测(例如 `if (!navigator.clipboard || !navigator.clipboard.writeText)`),并在这条分支中提供合理的降级方案或用户可见的错误提示,而不是只打印到控制台。
</issue_to_address>

### Comment 3
<location> `dashboard/src/components/shared/ReadmeDialog.vue:52` </location>
<code_context>
+
+  return `<div class="code-block-wrapper">
+    ${langLabel}
+    <button class="copy-code-btn" title="复制代码">
+      <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+        <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
</code_context>

<issue_to_address>
**suggestion:** 复制按钮的文案被硬编码为中文,而没有使用现有的 i18n 工具。

由于该组件已经在使用 `useI18n`,请从 i18n 文案中获取按钮标题,而不是硬编码 `title="复制代码"`,这样复制按钮就能被本地化,也不会在同一视图中混用不同语言。

建议实现:

```
  return `<div class="code-block-wrapper">
    ${langLabel}
    <button class="copy-code-btn" title="${t('readmeDialog.copyCode')}">

```

1. 确保 `useI18n()` 返回的 `t` 能在这段代码中访问,例如在 `<script setup>` 中:
   `const { t } = useI18n();`
2. 在各语言的 locale 配置中添加 `readmeDialog.copyCode` 键(例如 `en``zh` 等),例如:
   - 英文:`"readmeDialog": { "copyCode": "Copy code" }`
   - 中文:`"readmeDialog": { "copyCode": "复制代码" }`
3. 如果你的项目对这个对话框使用了不同的命名约定或命名空间,请相应调整键路径(`readmeDialog.copyCode`),以匹配现有的 i18n 结构。
</issue_to_address>

Sourcery 对开源项目免费 —— 如果你觉得我们的评审对你有帮助,欢迎分享 ✨
请帮我变得更有用!欢迎在每条评论上点击 👍 或 👎,我会根据你的反馈改进代码评审。
Original comment in English

Hey - I've found 3 issues, and left some high level feedback:

  • The global DOMPurify.addHook('afterSanitizeAttributes', ...) at module scope will affect all DOMPurify usages in the app; if you only want this behavior here, consider using a dedicated DOMPurify instance or registering/unregistering the hook around the sanitize call instead of globally.
  • The copy button label and title attribute are hardcoded in Chinese (复制代码); since the rest of the component uses useI18n, it would be more consistent to pull these strings from the i18n resources.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The global `DOMPurify.addHook('afterSanitizeAttributes', ...)` at module scope will affect all DOMPurify usages in the app; if you only want this behavior here, consider using a dedicated DOMPurify instance or registering/unregistering the hook around the sanitize call instead of globally.
- The copy button label and `title` attribute are hardcoded in Chinese (`复制代码`); since the rest of the component uses `useI18n`, it would be more consistent to pull these strings from the i18n resources.

## Individual Comments

### Comment 1
<location> `dashboard/src/components/shared/ReadmeDialog.vue:232-233` </location>
<code_context>
 );

+// 监听 renderedHtml 变化,初始化复制按钮
+watch(renderedHtml, () => {
+  nextTick(() => {
+    initCopyButtons();
+  });
</code_context>

<issue_to_address>
**issue (bug_risk):** Copy button listeners are added on every render without cleanup, which can cause duplicated handlers and leaks.

When `renderedHtml` changes, `initCopyButtons` re-runs and calls `addEventListener` on all `.copy-code-btn` elements, so the same button can accumulate multiple click handlers and leak references. To avoid this, either use event delegation (e.g. a single click listener on `markdownContainer` that checks `event.target.closest('.copy-code-btn')`) or ensure old listeners are removed (e.g. render the buttons via Vue and use `@click`).
</issue_to_address>

### Comment 2
<location> `dashboard/src/components/shared/ReadmeDialog.vue:251-257` </location>
<code_context>
+}
+
+// 处理复制代码
+function handleCopyCode(event) {
+  const btn = event.currentTarget;
+  const wrapper = btn.closest(".code-block-wrapper");
+  const code = wrapper.querySelector("code");
+
+  if (code) {
+    navigator.clipboard
+      .writeText(code.textContent)
+      .then(() => {
</code_context>

<issue_to_address>
**issue:** Assumes `navigator.clipboard` is always available, which can break on unsupported or non-secure contexts.

`navigator.clipboard.writeText` will throw when the Clipboard API is unavailable or the page isn’t served over HTTPS. Add a feature check (e.g. `if (!navigator.clipboard || !navigator.clipboard.writeText)`) and handle that path with a proper fallback or user-facing error instead of just logging to the console.
</issue_to_address>

### Comment 3
<location> `dashboard/src/components/shared/ReadmeDialog.vue:52` </location>
<code_context>
+
+  return `<div class="code-block-wrapper">
+    ${langLabel}
+    <button class="copy-code-btn" title="复制代码">
+      <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+        <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
</code_context>

<issue_to_address>
**suggestion:** The copy button label is hardcoded in Chinese instead of using the existing i18n utilities.

Since this component already uses `useI18n`, please take the button label from the i18n strings instead of hardcoding `title="复制代码"`, so the copy button is localized and doesn’t mix languages within the same view.

Suggested implementation:

```
  return `<div class="code-block-wrapper">
    ${langLabel}
    <button class="copy-code-btn" title="${t('readmeDialog.copyCode')}">

```

1. Ensure that `t` from `useI18n()` is in scope where this code runs, e.g. in `<script setup>`:
   `const { t } = useI18n();`
2. Add the `readmeDialog.copyCode` key to your locale message files (e.g. `en`, `zh`, etc.), for example:
   - English: `"readmeDialog": { "copyCode": "Copy code" }`
   - Chinese: `"readmeDialog": { "copyCode": "复制代码" }`
3. If your project uses a different naming convention or namespace for this dialog, adjust the key path (`readmeDialog.copyCode`) accordingly to match your existing i18n structure.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@dosubot dosubot bot added the area:webui The bug / feature is about webui(dashboard) of astrbot. label Jan 11, 2026
@dosubot dosubot bot added size:XL This PR changes 500-999 lines, ignoring generated files. and removed size:L This PR changes 100-499 lines, ignoring generated files. labels Jan 11, 2026
@dosubot dosubot bot added the lgtm This PR has been approved by a maintainer label Jan 12, 2026
@Soulter Soulter merged commit 3416e89 into AstrBotDevs:master Jan 12, 2026
6 checks passed
@Soulter Soulter changed the title fix(dashboard): optimize markdown rendering and remove redundant code fix(webui): optimize markdown rendering and remove redundant code Jan 12, 2026
@clown145 clown145 deleted the fix/markdown-rendering branch January 12, 2026 09:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:webui The bug / feature is about webui(dashboard) of astrbot. lgtm This PR has been approved by a maintainer size:XL This PR changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants