feat(channels): rich-text markdown rendering for feishu and dingtalk - #99
feat(channels): rich-text markdown rendering for feishu and dingtalk#99skywclouds wants to merge 4 commits into
Conversation
Add markdown_render.py with a hand-written parser (zero new deps) that covers headings, bold, inline code, fenced/indented/toplevel code blocks, lists, quotes, links, tables, and thematic breaks. Feishu: messages containing markdown are rendered as post rich text with code_block tags; plain text falls back to the original text msgtype. DingTalk: messages containing markdown are sent as native markdown with automatic code-fence insertion (ensure_code_fences) so toplevel code (def/class/import) is properly rendered by DingTalk's markdown engine. Key fixes during development: - Fix infinite loop in _parse_inline_recursive when multiple inline code segments were present (code placeholder regex used text[pos:] causing relative match.end() to never advance pos past the second placeholder). This caused 95% CPU spin -> health check failure -> app crash loop. - Add 4-space indented code block support. - Add toplevel code block detection (def/class/import/from/async def/@decorator) with conservative continuation heuristics (indent lines, # comments, assignments, function calls). - Add toplevel code start to has_markdown detection so code-only messages are routed through the rich-text path. Config: CHANNEL_RICH_RENDER_ENABLED (default true) gates the feature; setting it to false restores the original plain-text behavior. Tests: 1450 passed, ruff clean.
|
#99 目前有一个需要修改的 P1 问题、 位置:
```python
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx |
split_markdown_by_lines now tracks fenced code block state. When a split point falls inside a code fence, the previous chunk is closed and the next chunk reopens the fence with the original language, so every chunk is self-contained valid Markdown. Fixes broken code fences on feishu post and dingtalk markdown when messages exceed the channel length limit. Adds 9 regression tests covering overlong fenced blocks, language preservation, content recovery, tilde fences, and adapter-level chunk validation.
|
感谢详细的 review,P1 问题已修复并推送(commit 204e4c7)。 修复方案重写了 split_markdown_by_lines()(backend/app/channels/markdown_render.py:551),新增围栏代码块状态跟踪:
回归测试新增 9 项测试覆盖该场景:
验证
关于您提到的其余几点确认如下:
|
概述为飞书渠道增加实时展示智能体执行步骤的能力。用户在飞书发消息后,机器人会创建一张独立交互式卡片,随智能体执行(SOP 匹配 / 步骤推进 / 工具调用 / 知识检索)逐步更新,结束后定格为完成或失败状态。正文回复仍走原有 outbox 投递,与卡片互不影响。 动机网页端对话已能逐步展示智能体解决问题的过程(匹配到哪个 SOP、执行到哪一步、调用了什么工具、检索了什么知识),但飞书渠道此前只能向用户展示一个最终结果文本,看不到中间过程。用户在飞书中无法感知智能体"正在做什么",体验上缺乏透明度和信任感。 改动范围仅飞书渠道生效,不改微信/企微/钉钉、不改网页端、不改入站解析、不改正文回复投递机制。卡片是"进度展示",不进入 outbox 重试体系。 文件说明
技术方案单一事件钩子:所有 trace 事件都流经 EventLog.record,只需在这一处加一个可选 event_sink 即可覆盖 SOP/步骤/工具/知识全部类型。event_sink 默认 None,网页端行为完全不变。 测试
|
|
markdown_render.py:597 feishu_trace.py:171 / event_log.py:44 |
…ace I/O to background worker - markdown_render: overlong single-line code inside fenced blocks now wraps each hard-cut chunk in open/close fences with language tag, ensuring all chunks render as code and content is fully recoverable - feishu_trace: all HTTP I/O (create_card/update_card) moved to a background worker thread with a task queue; on_event never blocks the AgentLoop main thread; finish/abort join worker with 8s timeout - handle race where finish/abort is called before card creation completes via _final_state flag and _draining pattern - add regression tests for content recovery and async edge cases
|
PR 评论回复: 1. 长单行代码切分后中间 chunk 丢失围栏问题:split_markdown_by_lines 在围栏代码块内遇到超长单行时,硬切产生的中间片段以纯文本形式发出,没有包裹围栏,导致这些片段被渲染为普通文本而非代码,内容也无法通过 parse_markdown 恢复。 修复:当 fence_state 处于激活状态时,每个硬切 chunk 都会包裹对应的 open/close 围栏(含语言标识)。例如: xxxxxxxxxx...(100字符)
xxxxxxxxxx...(100字符)
修复后验证:500 字符的单行代码被切分为 7 段,每段围栏成对,`parse_markdown` 可完整恢复全部 500 字符内容。对 ` ``` `、`~~~`、带语言标识和无语言标识的围栏均添加了回归测试。
### 2. Feishu PATCH 同步调用阻塞 AgentLoop 主线程
**问题**:`FeishuTraceStreamer` 的 `start()`、`on_event()`、`finish()`、`abort()` 均在 AgentLoop 主线程同步执行 HTTP 调用(`create_card` / `update_card`),超时上限 15s,会阻塞 turn 执行和 session 锁。
**修复**:引入后台 worker 线程 + 任务队列,所有 HTTP I/O 在 worker 中执行:
- **`on_event`**:仅做内存中的行累积 + 节流判断,满足条件时入队 `_PatchCardTask`,立即返回,绝不阻塞
- **`start`**:入队 `_CreateCardTask`,立即返回
- **`finish` / `abort`**:设置 `_final_state`,入队最终状态 patch 任务,然后 join worker(超时 8s)确保最终卡片更新被尝试
同时处理了竞态:`finish`/`abort` 在卡片创建完成前被调用时,`_do_create_card` 会检测 `_final_state` 并自动补发最终状态更新。worker 使用 `_draining` 标志确保动态入队任务(如卡片创建后触发的 patch)在哨兵之前被处理。
新增 2 个异步边界测试:卡片未创建时 `on_event` 不阻塞、`finish` 先于卡片创建时仍发送最终状态。
全部 1484 后端测试 + 66 前端测试通过,ruff 检查通过。 |
概述
为飞书和钉钉渠道的出站消息增加 Markdown 富文本渲染能力。新增零依赖手写 Markdown 解析器,飞书走 post 富文本、钉钉走原生 markdown 透传 + 围栏补全。通过全局开关 CHANNEL_RICH_RENDER_ENABLED(默认开启)控制,关闭后回退纯文本行为。
动机
此前飞书和钉钉的出站消息均以纯 text 类型发送,AI 回复中的标题、代码块、列表、粗体、行内代码等 Markdown 语法全部以纯文本呈现,可读性差。尤其是代码——缩进丢失、无等宽字体、函数体与说明文字混为一团。
改动范围
只做出站渲染,不改入站解析、不改微信/企微、不改前端控制台。
文件说明
backend/app/channels/markdown_render.py 新增:Markdown 解析器 + 飞书 post 渲染 + 钉钉围栏补全 + 分块 + title 提取
backend/app/channels/adapters/feishu.py send() 富文本化:含 markdown 走 post,纯文本走 text
backend/app/channels/adapters/dingtalk.py send() 富文本化:含 markdown 走 markdown msgtype + ensure_code_fences 围栏补全
backend/app/config.py 新增 channel_rich_render_enabled: bool = True
backend/.env.example 新增 CHANNEL_RICH_RENDER_ENABLED="true"
backend/tests/test_markdown_render.py 新增:解析器 + 渲染器 + 围栏补全 52 项单测
backend/tests/test_feishu_adapter.py 新增 7 项富文本 send 测试
backend/tests/test_channel_dingtalk.py 新增 11 项富文本 send 测试
技术方案
Markdown 解析器(markdown_render.py,零新增依赖):
飞书渲染:
钉钉渲染:
顶格代码块识别:
开关与回退
测试
风险