Skip to content

feat: support DingTalk streaming AI cards - #8890

Open
Accenemy wants to merge 4 commits into
AstrBotDevs:masterfrom
Accenemy:fix/dingtalk-stream-card
Open

feat: support DingTalk streaming AI cards#8890
Accenemy wants to merge 4 commits into
AstrBotDevs:masterfrom
Accenemy:fix/dingtalk-stream-card

Conversation

@Accenemy

@Accenemy Accenemy commented Jun 19, 2026

Copy link
Copy Markdown

中文说明

Related to #5785

这个 PR 为 AstrBot 内置钉钉平台适配器增加原生“互动 AI 卡片流式回复”支持。

目前 WebUI 里的钉钉设置已经有 card_template_id 配置项,并提示“启用后将使用互动卡片进行流式回复”,但实际 DingtalkMessageEvent.send_streaming() 仍然会把模型输出缓冲到结束后再作为普通消息发送。用户在钉钉里提问后需要等待完整生成结束才看到回复,体验上不是流式输出。

本 PR 基于已经在外部插件中验证通过的适配逻辑迁移到官方钉钉 adapter:填写钉钉互动卡片模板 ID 后,流式回复会创建钉钉 AI 卡片,并把模型生成的文本持续更新到卡片内容变量中;未配置模板或创建卡片失败时,会自动回退到原来的普通消息发送方式。

改动内容

  • 使用现有 card_template_id 作为钉钉流式卡片开关。
  • 通过 dingtalk_stream.AICardReplier 创建并投递钉钉互动 AI 卡片。
  • 将流式生成的纯文本内容按间隔更新到卡片中。
  • 新增可选配置 card_content_key,用于指定卡片模板中的内容变量名,默认 content
  • 新增可选配置 card_update_interval,用于控制卡片更新最小间隔,默认 0.35 秒。
  • 未配置模板、缺少原始钉钉消息或卡片创建失败时,自动回退到原来的缓冲普通消息。
  • 对非纯文本消息段保留原有普通消息发送路径。
  • 在流式生成异常或取消时,也会尝试发送最终卡片状态并清理卡片会话,避免会话残留。
  • 若中间卡片更新失败,会停止无效的频繁更新但继续收集完整结果;最终完整更新仍失败时,自动回退为一条普通消息,避免回复截断或丢失。
  • 明确保留基类调用仅用于 AstrBot 发送状态与指标记录;新增事件层测试确认生成器只消费一次,卡片成功时不会重复发送普通消息。
  • 新增可选配置 send_plain_text_as_card(默认关闭),开启后将未来任务、定时任务、插件 context.send_message 等通过 send_by_session 主动发送的纯文本回复也投递为互动卡片。
  • 主动消息先通过 createAndDeliver 创建空卡片,再调用 /v1.0/card/streaming 写入正文并完成卡片,避免卡片已创建但内容为空。
  • 新增可选配置 animate_proactive_card(默认开启),让已经生成完成的主动消息分段更新到同一张卡片,提供“正在输出”的视觉反馈;单次最多更新 20 次。
  • 主动消息仅在消息链全部为纯文本时使用卡片;图片、语音、文件及混合消息继续走原有发送路径。
  • 去除正常回退分支中的重复提示日志,仅保留鉴权、HTTP 响应、JSON 解析和卡片接口异常等故障日志。

主动消息与未来任务

AstrBot 的普通对话仍使用模型生成器提供的真实流式输出;未来任务、定时任务和插件主动消息通常只能在任务执行完成后拿到完整文本,因此这一类消息无法获得模型原始 token 流。本实现会先立即创建空卡片,再把完整结果按自适应分段更新到同一张卡片,最后发送完整正文并将卡片标记为完成,从视觉上保留“处理中 → 正在输出 → 输出完成”的连续体验。

卡片更新使用完整累计内容和覆盖语义(isFull=True),避免只发送 delta 时前文被替换。主动消息最多执行 20 次更新,并复用 card_update_interval 控制更新间隔;关闭 animate_proactive_card 后会直接写入最终内容。任何创建或更新失败都会返回原有普通消息路径,不影响现有功能。

达到的效果

1. 处理中

收到用户问题后,先创建钉钉互动 AI 卡片,并展示“该问题正在处理中...”的占位状态。这样用户发送问题后能立即看到机器人已经接收并开始处理请求,不需要等到模型完整生成结束才看到第一条回复。

DingTalk AI card processing state

2. 输出结果中

模型开始生成内容后,卡片进入流式输出状态,标题显示“正在输出结果...”,内容区域会随着模型生成不断刷新已经产出的文本。用户可以边等边看当前回答进度,避免普通消息模式下长时间无反馈的割裂感。

DingTalk AI card streaming state

3. 输出结果

模型生成完成后,卡片更新为最终状态,标题显示“输出结果”,内容区域展示完整回复。最终结果仍保留在同一张卡片中,不会额外刷出多条普通消息。

DingTalk AI card final result state

卡片的具体标题、布局、按钮和视觉样式均可在钉钉开发者平台的互动卡片模板中自定义;本 PR 只负责将 AstrBot 的流式输出接入钉钉 AI 卡片,并把内容写入配置的模板变量,例如默认的 content

关于完整内容更新

当前实现会在每次卡片更新时发送完整的 full_content,这是有意保留的行为。钉钉互动 AI 卡片这里使用的是覆盖式内容更新语义;在 append=False 的更新方式下,如果只发送新增 delta,卡片内容可能只保留最新片段,导致前文丢失。

为了控制请求频率和 payload 增长,本 PR 通过 card_update_interval 对更新节奏做限流,默认最小间隔为 0.35 秒。后续如果钉钉侧确认可稳定使用追加式更新,也可以再切换为 delta 追加模式。

验证情况

  • 已基于官方最新 masterd2d7e5a,2026-08-27)重新整理提交并验证兼容性。
  • Python 3.12 执行 pytest tests/test_dingtalk_adapter.py -q21 passed
  • 全量测试:2210 passed;仅 tests/test_dashboard.py::test_plugins 因本地受限网络无法访问插件市场接口而失败,与本 PR 无关。
  • 官方 scripts/smoke_startup_check.py:通过。
  • ruff checkruff format --checkpy_compilegit diff --check 均通过。
  • 没有引入新的第三方依赖。
  • 已在 Docker Compose 部署的 AstrBot v4.26.7 上使用同逻辑插件 v0.2.3 完成钉钉实机验证:
    • 普通对话可使用互动 AI 卡片真实流式更新;
    • 未来任务的主动消息可创建互动卡片,不再退化为普通消息;
    • 通过“先创建空卡片、再调用 streaming 接口写入”的两阶段流程,最终 Markdown 正文可以正常显示,不再出现空卡片;
    • 主动消息会展示分段输出动画,并在同一张卡片中完成最终内容;
    • 卡片接口不可用或消息含媒体组件时仍保留原有普通消息发送路径。

Checklist / 检查清单

  • This is NOT a breaking change. / 这不是一个破坏性变更。

  • 😊 If there are new features added in the PR, I have discussed it with the authors through issues/emails, etc.
    / 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。相关讨论见 [Feature]功能请求:适配钉钉打字机效果流式 AI 卡片 #5785

  • 👀 My changes have been well-tested, and "Verification Steps" and "Screenshots" have been provided above.
    / 我的更改经过了良好的测试,并已在上方提供了“验证步骤”和“运行截图”

  • 🤓 I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations in requirements.txt and pyproject.toml.
    / 我确保没有引入新依赖库,或者引入新依赖库的同时将其添加到 requirements.txtpyproject.toml 文件相应位置。

  • 😮 My changes do not introduce malicious code.
    / 我的更改没有引入恶意代码。

English Summary

This PR adds native DingTalk interactive AI card streaming support to the built-in DingTalk platform adapter. When card_template_id is configured, streaming text chunks are delivered through a DingTalk AI card via dingtalk_stream.AICardReplier; otherwise the adapter falls back to the existing buffered normal-message behavior.

The user experience has three visible states: a processing card is shown immediately after the user asks a question, the same card is continuously updated while the model is generating, and the final card displays the complete answer. The card title, layout, buttons, and visual style can be customized in the DingTalk developer platform's interactive card template; this PR only connects AstrBot's streaming output to the configured card content variable.

The card update path intentionally sends the full accumulated content on each update because the current DingTalk AI card update flow is used with overwrite semantics (append=False). Sending only delta text in that mode can cause the card to display only the latest chunk instead of the whole generated answer. Update frequency is throttled by card_update_interval.

Optional send_plain_text_as_card support also covers proactive plain-text replies sent by future tasks, scheduled jobs, and plugins. Proactive messages are created as empty cards and then populated through DingTalk's streaming endpoint; when animate_proactive_card is enabled, the completed text is replayed in at most 20 accumulated updates to provide continuous visual feedback. Mixed or media messages and any failed card operation retain the existing normal-message fallback.

Summary by Sourcery

Enable configurable DingTalk interactive AI cards for streaming and proactive plain-text replies while preserving reliable normal-message fallbacks.

New Features:

  • Add native DingTalk interactive AI card streaming for conversational responses when a card template is configured.
  • Support optional interactive cards for proactive plain-text messages from scheduled tasks, future tasks, and plugins, including configurable progress animation.
  • Allow configuring the card content variable and update interval.

Bug Fixes:

  • Fall back to buffered normal messages when cards are unavailable, creation or updates fail, source messages are missing, or responses contain media or mixed message components.
  • Prevent proactive cards from remaining empty by creating them before streaming their final content.

Enhancements:

  • Preserve accumulated content during overwrite-based card updates and clean up completed card sessions.
  • Retain normal message delivery for non-text and unsupported scenarios while reducing redundant fallback logging.

Tests:

  • Expand DingTalk adapter coverage for card selection, proactive card creation and animation, streaming updates, duplicate-send prevention, and fallback behavior.

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

Copy link
Copy Markdown
Contributor

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 introduces support for DingTalk streaming interactive cards by adding configuration options, implementing card creation and update methods in the DingTalk adapter, and updating the event handler to stream message updates. The review feedback highlights a potential memory leak if the streaming generator raises an exception, which can be resolved by wrapping the loop in a try...finally block, and a bug where a configured update interval of 0.0 is overridden due to a falsy check.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread astrbot/core/platform/sources/dingtalk/dingtalk_event.py
@Accenemy
Accenemy force-pushed the fix/dingtalk-stream-card branch from 11b0ca7 to 9b36e52 Compare June 19, 2026 05:16
@Accenemy
Accenemy marked this pull request as ready for review June 19, 2026 05:21
@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. area:platform The bug / feature is about IM platform adapter, such as QQ, Lark, Telegram, WebChat and so on. labels Jun 19, 2026

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - I've left some high level feedback:

  • In DingtalkMessageEvent._send_streaming_as_plain_text, the previous call to super().send_streaming(...) has been removed and the use_fallback parameter is now ignored; if you intended to preserve the base-class streaming/fallback behavior, consider still delegating to super() after buffering or dropping the unused parameter.
  • The card streaming path repeatedly sends the full accumulated full_content on each update; if payload size or rate limits become a concern, consider tracking and sending only the newly appended text (e.g., via a diff or slice) instead of the entire history each time.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `DingtalkMessageEvent._send_streaming_as_plain_text`, the previous call to `super().send_streaming(...)` has been removed and the `use_fallback` parameter is now ignored; if you intended to preserve the base-class streaming/fallback behavior, consider still delegating to `super()` after buffering or dropping the unused parameter.
- The card streaming path repeatedly sends the full accumulated `full_content` on each update; if payload size or rate limits become a concern, consider tracking and sending only the newly appended text (e.g., via a diff or slice) instead of the entire history each time.

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.

@Accenemy

Copy link
Copy Markdown
Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - I've left some high level feedback:

  • In DingtalkMessageEvent.send_streaming, the use_fallback parameter is now unused and the method no longer delegates to super().send_streaming; consider either removing the parameter or making the intended behavior explicit so callers are not misled about fallback semantics.
  • The card streaming implementation always sends the full accumulated full_content on each update, which can lead to increasingly large payloads for long responses; consider introducing a maximum content length or a configurable option to switch to delta-based updates once DingTalk reliably supports append semantics.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `DingtalkMessageEvent.send_streaming`, the `use_fallback` parameter is now unused and the method no longer delegates to `super().send_streaming`; consider either removing the parameter or making the intended behavior explicit so callers are not misled about fallback semantics.
- The card streaming implementation always sends the full accumulated `full_content` on each update, which can lead to increasingly large payloads for long responses; consider introducing a maximum content length or a configurable option to switch to delta-based updates once DingTalk reliably supports append semantics.

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.

@Accenemy

Copy link
Copy Markdown
Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - I've left some high level feedback:

  • In DingtalkMessageEvent.send_streaming, the generator is passed to super().send_streaming and then consumed again locally; if AstrMessageEvent.send_streaming iterates the generator, this will result in an exhausted generator and inconsistent behavior, so consider either moving the super() call after your own streaming logic or using a separate notification mechanism that doesn’t reuse the same generator.
  • Both DingtalkPlatformAdapter.send_by_session and DingtalkMessageEvent.send call the card-sending helpers and then unconditionally delegate to super().send_by_session/super().send, which may cause duplicate side effects depending on the base implementations; it would be safer to clearly separate “send card only” vs “send normal message” paths and ensure the base method isn’t invoked in cases where the card send fully replaces the original message delivery.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `DingtalkMessageEvent.send_streaming`, the generator is passed to `super().send_streaming` and then consumed again locally; if `AstrMessageEvent.send_streaming` iterates the generator, this will result in an exhausted generator and inconsistent behavior, so consider either moving the `super()` call after your own streaming logic or using a separate notification mechanism that doesn’t reuse the same generator.
- Both `DingtalkPlatformAdapter.send_by_session` and `DingtalkMessageEvent.send` call the card-sending helpers and then unconditionally delegate to `super().send_by_session`/`super().send`, which may cause duplicate side effects depending on the base implementations; it would be safer to clearly separate “send card only” vs “send normal message” paths and ensure the base method isn’t invoked in cases where the card send fully replaces the original message delivery.

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.

@Accenemy
Accenemy force-pushed the fix/dingtalk-stream-card branch from 792332d to 83f2bba Compare August 28, 2026 15:25

Copy link
Copy Markdown
Author

已将本 PR 更新并重放到最新 masterd2d7e5a)。本次补充:

  • 流式卡片中间更新失败后继续收集完整结果,最终更新仍失败时回退普通消息,避免内容截断或丢失;
  • 明确基类 send / send_streaming / send_by_session 仅用于 AstrBot 指标与发送状态记录,不会重复投递,也不会消费生成器;
  • 新增事件层测试,覆盖生成器单次消费、完整累计内容更新、成功时不重复发送以及最终更新失败回退。

本地验证:钉钉相关测试 21 passed,Ruff/格式/py_compile/git diff --check 均通过,官方 Smoke Test 通过;全量测试 2210 passed,唯一失败是受限网络无法访问插件市场接口。

新的 GitHub Actions 当前显示 action_required,需要仓库维护者批准 fork 工作流后才会实际运行。烦请在方便时重新审核,谢谢。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:platform The bug / feature is about IM platform adapter, such as QQ, Lark, Telegram, WebChat and so on. size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant