Skip to content

Conversation

@dingyi222666
Copy link
Member

@dingyi222666 dingyi222666 commented Jan 12, 2026

This PR fixes Markdown output rendering issues by replacing the external koishi-plugin-markdown dependency with a custom markdown transformer implementation.

Bug Fixes

Other Changes

  • Add transformToMarkdown function in utils/koishi.ts using the marked library with custom tokenizer for strikethrough
  • Refactor text renderer to use new transformer with platform-aware rendering support
  • Pass session to render options to enable platform-specific markdown handling
  • Remove he dependency for HTML entity decoding
  • Simplify getRenderer to synchronous method

…ormer

- Add transformToMarkdown function in utils/koishi.ts using marked library
- Refactor text renderer to use new transformer with platform support
- Pass session to render options for platform-aware markdown rendering
- Remove he dependency for HTML entity decoding
- Simplify getRenderer to synchronous method
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 12, 2026

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

将渲染器获取改为同步接口,向渲染选项注入 session,并新增基于 Marked 的平台感知 Markdown→Koishi 虚拟 DOM 转换(transformToMarkdown);文本渲染管道切换为使用该转换器并基于 session.platform 选择行为。

Changes

Cohort / File(s) 变更摘要
渲染选项扩展
\packages/core/src/middlewares/chat/render_message.ts``
在调用 renderMessage 时将 session 合并进传递的 render options(...context.options.renderOptions, session),使 renderer 接收的 options 包含 session 字段。
核心渲染流程同步化
\packages/core/src/render.ts``
getRenderer 从异步改为同步(不再返回 Promise),移除对其的 await,相关调用改为同步获取 renderer。
导入/类型调整
\packages/core/src/services/chat.ts``
改为从 koishi-plugin-chatluna 导入 DefaultRendererRenderer,调整导入来源但未更改调用逻辑。
文本渲染适配(platform)
\packages/core/src/renders/text.ts``
transformToMarkdown 替换原有 markdown/HTML 解码逻辑;移除 he 相关代码;transformAndEscape 新增 platform 参数(来自 options.session?.platform 或默认 'sandbox')。
平台感知 Markdown→vDOM 引擎
\packages/core/src/utils/koishi.ts``
新增基于 Marked 的解析与渲染管道:自定义 tokenizer、renderToken / render、以及多重重载的 transformToMarkdown(source, platform?),产出 Koishi h() 节点并包含平台分支(例如 Discord/onebot)。API 表面增加 transformToMarkdown 重载。

Sequence Diagram

sequenceDiagram
    participant Client as 客户端
    participant Renderer as DefaultRenderer
    participant TextRender as TextRenderer
    participant Transform as transformToMarkdown
    participant Marked as Marked

    Client->>Renderer: 调用 renderMessage(context, options)
    Renderer->>TextRender: render(message, mergedOptions)(mergedOptions 包含 session)
    TextRender->>Transform: transformAndEscape(elements, platform)
    Transform->>Marked: marked.parse(markdown)
    Marked-->>Transform: tokens[]
    Transform->>Transform: renderToken / render -> 生成 h() 节点
    Transform-->>TextRender: 返回 h[] 虚拟节点
    TextRender-->>Renderer: 返回渲染后的文本/节点
    Renderer-->>Client: 平台感知的消息输出
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 我在代码田间轻轻跳,
波浪符号的迷雾被理好,
Marked 切词成片,h 节点成行,
平台分支里换了模样,
渲染同步了,消息更直了 ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed 标题准确反映了主要变更:使用自定义markdown转换器替换koishi-plugin-markdown依赖,是整个PR的核心目标。
Description check ✅ Passed 描述与changeset相关,清晰说明了bug修复(#689)以及具体的代码改动内容。
Linked Issues check ✅ Passed PR实现了#689中的全部关键需求:自定义markdown转换器处理strikethrough、支持平台感知的markdown渲染、通过session传递平台信息、移除he依赖。
Out of Scope Changes check ✅ Passed 所有变更都围绕解决markdown渲染问题进行,包括getRenderer同步化、render options扩展都直接支持该目标,无超出范围的改动。

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/markdown-bug

🧹 Recent nitpick comments
packages/core/src/utils/koishi.ts (1)

99-103: HTML img 标签关闭时隐式返回 undefined。

第 100 行 return 不带值会返回 undefined,虽然会被 filter(Boolean) 过滤掉,但显式返回空数组更清晰。

♻️ 建议改进
         if (cap[2] === 'img') {
-            if (cap[1]) return
+            if (cap[1]) return []
             const src = cap[3].match(/src="([^"]+)"/)
             if (src) return h.image(src[1])
         }

📜 Recent review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e2f08ef and 8a61d24.

📒 Files selected for processing (1)
  • packages/core/src/utils/koishi.ts
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: dingyi222666
Repo: ChatLunaLab/chatluna PR: 548
File: packages/core/src/llm-core/chat/app.ts:0-0
Timestamp: 2025-09-17T00:25:27.195Z
Learning: 在 ChatInterface 类中,响应式 watch 调用通过 ctx.effect() 包装来自动处理清理工作,避免内存泄漏。字段 _chain 和 _embeddings 的类型已更新为可空类型 (| undefined),并添加 ctx.on('dispose') 处理器提供额外的清理保障。这种使用 Koishi effect 系统的方式比手动管理 stop 句柄更优雅。
🧬 Code graph analysis (1)
packages/core/src/utils/koishi.ts (3)
packages/core/src/utils/types.ts (1)
  • PromiseLikeDisposable (3-3)
packages/core/src/renders/text.ts (1)
  • render (8-38)
packages/core/src/render.ts (1)
  • render (48-85)
🪛 GitHub Check: CodeFactor
packages/core/src/utils/koishi.ts

[notice] 31-96: packages/core/src/utils/koishi.ts#L31-L96
Complex Method

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build
  • GitHub Check: lint
🔇 Additional comments (3)
packages/core/src/utils/koishi.ts (3)

5-21: 自定义删除线 tokenizer 实现正确。

正则表达式 ^~~(?=\S)([\s\S]*?\S)~~ 严格要求双波浪号,有效解决了 #689 中单个 ~ 被误识别为删除线的问题。使用前向断言确保内容非空白,符合 GFM 标准。


109-113: LGTM!

使用 flatMap 正确处理 renderToken 返回单个元素或数组的情况,filter(Boolean) 清理无效值。


115-138: transformToMarkdown API 设计合理。

函数重载清晰,支持字符串和模板字面量两种输入方式。模板字面量的插值处理(lines 133-135)正确拼接了参数和模板片段。

一个小建议:第一个重载中 platform 是可选的,但实现中默认值为 'onebot',建议在文档中明确说明默认平台。

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @dingyi222666, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request overhauls the Markdown rendering mechanism to resolve existing display issues and introduce greater flexibility. By replacing an external library with a custom marked-based solution, the system now supports platform-specific Markdown transformations, ensuring consistent and correct output across different environments. This change also streamlines the rendering process and reduces external dependencies.

Highlights

  • Custom Markdown Transformer: Replaced the external koishi-plugin-markdown dependency with a custom Markdown transformer implementation, utilizing the marked library with a custom tokenizer for strikethrough.
  • Platform-Aware Rendering: Refactored the text renderer to support platform-aware Markdown rendering, passing the session to render options for platform-specific handling.
  • Dependency Removal: Removed the he dependency, which was previously used for HTML entity decoding.
  • Synchronous Renderer: Simplified the getRenderer method to be synchronous, improving the rendering pipeline's flow.
  • Bug Fix: Addressed and fixed Markdown output rendering issues, resolving bug [Bug] Markdown 输出渲染格式问题 #689.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request refactors the message rendering pipeline by introducing a new transformToMarkdown utility in koishi.ts that leverages the marked library for markdown parsing, replacing the previous koishi-plugin-markdown dependency. The DefaultRenderer's getRenderer method is made synchronous, and rendering options now merge with defaults and include session information. The TextRenderer is updated to use the new transformToMarkdown function, passing platform information for platform-specific rendering. Review comments suggest further improvements to the renderToken function within the new markdown utility, specifically to correctly render blockquotes using h('quote'), clarify ambiguous fallback rendering by returning children directly, and enhance readability by using switch statements. Additionally, the render function is advised to use flatMap to properly handle renderToken potentially returning an array of elements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @packages/core/src/renders/text.ts:
- Around line 25-26: Remove the leftover debugging console.log statements that
print JSON.stringify(transformed) in the text rendering module; locate the
occurrences in packages/core/src/renders/text.ts (calls that reference the
transformed variable around the render/transform function) and delete those
console.log lines (both the one near the earlier log and the duplicate around
line 38) so production code no longer emits debug output or pays the
JSON.stringify cost.
🧹 Nitpick comments (3)
packages/core/src/utils/koishi.ts (2)

62-62: 未知 token 类型使用 '?' 作为元素类型可能导致渲染问题。

当遇到未知的带有子节点的 token 类型时,返回 h('?', children) 可能在某些平台上无法正确渲染。建议返回一个更安全的容器元素或直接返回子节点。

💡 建议的修改
-        return h('?', children)
+        return h('span', children)

或者直接返回第一个子节点(如果只有一个)或包装成 fragment:

-        return h('?', children)
+        return children.length === 1 ? children[0] : h('', children)

87-91: 建议显式返回 null 而非隐式 undefined

当遇到 </img> 闭合标签时,return 隐式返回 undefined。虽然后续 filter(Boolean) 会过滤掉,但显式返回 null 可以提高代码可读性。

💡 建议的修改
         if (cap[2] === 'img') {
-            if (cap[1]) return
+            if (cap[1]) return null
             const src = cap[3].match(/src="([^"]+)"/)
             if (src) return h.image(src[1])
         }
packages/core/src/renders/text.ts (1)

51-58: 平台参数传递正确,但默认值与 transformToMarkdown 不一致。

transformAndEscape 默认平台为 'sandbox',而 transformToMarkdown 默认为 'onebot'。虽然调用链中 options.session?.platform ?? 'sandbox' 会覆盖,但建议保持一致性。

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c65737f and 57f28f6.

📒 Files selected for processing (5)
  • packages/core/src/middlewares/chat/render_message.ts
  • packages/core/src/render.ts
  • packages/core/src/renders/text.ts
  • packages/core/src/services/chat.ts
  • packages/core/src/utils/koishi.ts
🧰 Additional context used
🧬 Code graph analysis (1)
packages/core/src/utils/koishi.ts (1)
packages/core/src/utils/types.ts (1)
  • PromiseLikeDisposable (3-3)
🪛 GitHub Check: CodeFactor
packages/core/src/utils/koishi.ts

[notice] 31-96: packages/core/src/utils/koishi.ts#L31-L96
Complex Method

packages/core/src/renders/text.ts

[warning] 25-25: packages/core/src/renders/text.ts#L25
Replace "0·"+JSON.stringify(transformed))⏎ with '0·'·+·JSON.stringify(transformed)) (prettier/prettier)


[warning] 38-38: packages/core/src/renders/text.ts#L38
Replace "1·"+ with '1·'·+· (prettier/prettier)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: lint
  • GitHub Check: build
🔇 Additional comments (7)
packages/core/src/utils/koishi.ts (2)

5-21: 自定义 del tokenizer 正确修复了单波浪号问题。

正则表达式 ^~~(?=\S)([\s\S]*?\S)~~ 要求双波浪号 ~~ 才会触发删除线解析,这正确解决了 issue #689 中单个 ~ 被错误渲染为删除线的问题。


101-124: transformToMarkdown 实现正确,支持字符串和模板字符串两种调用方式。

函数正确处理了空值检查和模板字符串拼接逻辑,默认平台为 'onebot' 符合项目需求。

packages/core/src/services/chat.ts (1)

51-51: 导入路径调整正确。

DefaultRendererRendererkoishi-plugin-chatluna 统一导入,符合模块组织规范。

packages/core/src/middlewares/chat/render_message.ts (1)

14-17: 正确传递 session 到渲染选项。

通过将 session 添加到渲染选项中,使得下游的 transformToMarkdown 可以根据 session.platform 进行平台感知的渲染,这是修复 issue #689 跨平台 Markdown 渲染问题的关键改动。

packages/core/src/renders/text.ts (1)

3-3: 导入新的 transformToMarkdown 函数。

正确从 koishi-plugin-chatluna/utils/koishi 导入自定义的 Markdown 转换函数,替代了原有的外部依赖。

packages/core/src/render.ts (2)

55-61: 选项合并和渲染器获取逻辑正确。

Object.assign 移到获取渲染器之前确保了选项正确合并,同步获取渲染器简化了代码流程。


104-106: 无需担忧。当前代码中 getRenderer 方法没有证据表明曾为异步方法。全代码库中不存在对该方法使用 await 的调用模式,也没有找到任何异步版本的定义。该方法一直以来都是同步返回 this.renderers[type],因此不存在破坏性变更。

Likely an incorrect or invalid review comment.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
packages/core/src/renders/text.ts (1)

46-53: 建议添加空值检查以提高健壮性。

根据 transformToMarkdown 的实现(当 source 为 falsy 时返回空数组 []),当 element.attrs['content']undefined 或空字符串时会返回空数组,这可能导致原本的文本节点被意外移除。

♻️ 建议的改进
 export function transformAndEscape(source: h[], platform: string = 'sandbox') {
     return source.flatMap((element) => {
         if (element.type === 'text') {
+            const content = element.attrs['content']
+            if (!content) {
+                return element
+            }
-            const base = transformToMarkdown(element.attrs['content'], platform)
+            const base = transformToMarkdown(content, platform)
             return base
         }
         return element
     })
 }
packages/core/src/render.ts (1)

103-105: 建议添加渲染器存在性检查。

getRenderer 直接返回 this.renderers[type],如果传入无效的 type,将返回 undefined,后续调用 currentRenderer.render() 时会抛出运行时错误。

♻️ 建议的改进
     getRenderer(type: string) {
-        return this.renderers[type]
+        const renderer = this.renderers[type]
+        if (!renderer) {
+            throw new ChatLunaError(
+                ChatLunaErrorCode.RENDER_ERROR,
+                new Error(`Renderer '${type}' not found`)
+            )
+        }
+        return renderer
     }
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 57f28f6 and e096ed6.

📒 Files selected for processing (2)
  • packages/core/src/render.ts
  • packages/core/src/renders/text.ts
🧰 Additional context used
🧬 Code graph analysis (1)
packages/core/src/renders/text.ts (3)
packages/core/src/renders/koishi-element.ts (1)
  • transformAndEscape (44-56)
packages/core/src/renders/pure-text.ts (1)
  • transformAndEscape (58-62)
packages/core/src/utils/koishi.ts (1)
  • transformToMarkdown (110-124)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build
  • GitHub Check: lint
🔇 Additional comments (3)
packages/core/src/renders/text.ts (2)

3-3: LGTM!

导入 transformToMarkdown 替代外部依赖,符合 PR 目标。


14-17: LGTM!

平台参数传递逻辑正确,使用 options.session?.platform 获取平台信息,回退到 'sandbox' 作为默认值。

packages/core/src/render.ts (1)

55-61: LGTM!

选项合并逻辑正确,使用 Object.assign({}, this.defaultOptions, options) 确保默认值被正确继承且不会修改原对象。同步获取渲染器简化了代码流程。

Allow renderToken to return h[] for unknown token types, preventing
wrapper elements and preserving children directly in output.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
packages/core/src/utils/koishi.ts (2)

87-91: 隐式返回 undefined 应显式处理。

当遇到闭合的 <img> 标签时(cap[1] 为真),函数隐式返回 undefined。虽然 render 函数会通过 .filter(Boolean) 过滤掉,但显式返回更清晰。

♻️ 建议修复
         if (cap[2] === 'img') {
-            if (cap[1]) return
+            if (cap[1]) return null
             const src = cap[3].match(/src="([^"]+)"/)
             if (src) return h.image(src[1])
         }

41-63: 可选:使用映射表降低圈复杂度。

静态分析标记此函数过于复杂。可考虑将 token 类型映射到处理函数,提高可维护性:

♻️ 可选重构示例
const tokenHandlers: Record<string, (token: Token, children: h[], platform?: string) => h | h[]> = {
    paragraph: (_, children) => h('p', children),
    em: (_, children) => h('em', children),
    strong: (_, children) => h('strong', children),
    del: (_, children) => h('del', children),
    link: (token, children) => h('a', { href: token.href }, children),
    // ... 其他类型
}

function renderToken(token: Token, platform?: string): h | h[] {
    // ... 获取 children
    const handler = tokenHandlers[token.type]
    if (handler) return handler(token, children, platform)
    // ... fallback 逻辑
}
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e096ed6 and e2f08ef.

📒 Files selected for processing (1)
  • packages/core/src/utils/koishi.ts
🧰 Additional context used
🧬 Code graph analysis (1)
packages/core/src/utils/koishi.ts (1)
packages/core/src/utils/types.ts (1)
  • PromiseLikeDisposable (3-3)
🪛 GitHub Check: CodeFactor
packages/core/src/utils/koishi.ts

[notice] 31-96: packages/core/src/utils/koishi.ts#L31-L96
Complex Method

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build
  • GitHub Check: lint
🔇 Additional comments (4)
packages/core/src/utils/koishi.ts (4)

5-21: 自定义删除线分词器实现正确。

正则表达式 ^~~(?=\S)([\s\S]*?\S)~~ 仅匹配双波浪线 (~~),解决了 issue #689 中单个 ~ 导致意外删除线的问题。使用 this.lexer.inlineTokens 解析嵌套内容也是正确的做法。


61-62: 展平未知 token 类型的子节点处理正确。

根据 commit 信息,对于未知 token 类型直接返回 children 是预期行为,避免产生不必要的包装元素。


97-101: LGTM!

flatMap + filter(Boolean) 的组合正确处理了 renderToken 返回 h | h[] 或 falsy 值的情况。


103-126: 函数签名和实现良好。

两个重载提供了良好的类型支持,模板字面量的重组逻辑正确。对 falsy 输入的提前返回是合理的防御性编程。

- Remove li wrapper for list items on non-Discord platforms to fix
  rendering issues
- Add Telegram platform support for code-block element type
@dingyi222666 dingyi222666 merged commit 50c983e into v1-dev Jan 18, 2026
4 of 5 checks passed
@dingyi222666 dingyi222666 deleted the fix/markdown-bug branch January 18, 2026 11:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Markdown 输出渲染格式问题

1 participant