Add streaming AI card replies to the DingTalk channel - #538
Open
silence8013 wants to merge 4 commits into
Open
silence8013 wants to merge 4 commits into
silence8013 wants to merge 4 commits into
Conversation
added 4 commits
September 1, 2026 11:24
The writing state rendered both the streaming `content` variable and every blockList entry with `type < 3`. When finalization wrote the final answer, the type 0 block and `content` landed in the same card data while the card was still in the writing state, so the answer appeared twice until the client switched to the done layout. Restrict the writing timeline to progress blocks (`type == 1`) so the streaming body stays the single answer source, and drop tool status blocks (`type == 2`) from both states: the tool names carried no signal for chat readers and only added noise. Runtime still emits type 2 blocks, so no adapter change is needed. Tested: re-parsed both JSON layers and asserted the four loop conditions; vitest dingtalk, dingtalk-api, dingtalk-image (69 tests) Not-tested: actual rendering in the DingTalk client after template import Scope-risk: narrow
Contributor
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Summary
Adds streaming AI card replies to the DingTalk channel, so a DingTalk conversation shows a live-updating card instead of waiting for one final Markdown message. The card carries the streaming answer, high-level progress, and any agent-produced images, and it degrades to the existing
sessionWebhookMarkdown path whenever the card cannot be created or finalized.Problem
DingTalk replies were a single Markdown message delivered after the whole turn finished. For a multi-minute investigation the chat stayed silent, and images the agent produced (charts, rendered visuals) arrived as separate messages detached from the answer. Lark already had a streaming card; DingTalk had no equivalent.
Solution
reply_mode=ai_cardopts a channel into the card path:dingtalk-card-api.tswraps the three DingTalk card APIs:createAndDeliverto open the card,card/streamingfor the answer body, andcard/instancesfor structured content and the terminal atomic commit.dingtalk-card-stream.tsdrives updates through two independent single-flight, latest-wins senders (body vs. structured blocks) so a slow request can never be overtaken by a stale snapshot.finalizedrains both before the last write.dingtalk-card.tsdefines the block contract consumed by the template:0answer,1progress,2tool status,3image. Raw tool arguments and results deliberately never reach the card — they may carry credentials or verbose infra output.collectChannelResponse(shared by Lark and DingTalk, already imported bydingtalk.tsonmain) gains two optional callbacks,onTextSnapshotandonToolActivity. Both are opt-in, wrapped in try/catch, and unset for Lark, so Lark behaviour is unchanged.type: 3block in the same card; anything not embedded still falls back to separate image messages.The template in
docs/assets/dingtalk-ai-card-template.jsonrenders the answer from the streamingcontentvariable while the card is writing, and from the structuredblockListonce done. The writing state deliberately shows onlytype == 1blocks: renderingtype == 0there made the final answer appear twice during the state transition, because the terminal update writescontent,blockList, andflowStatustogether and the client applies them with a visible window. Tool status blocks (type == 2) are hidden in both states — the bare tool names carried no signal for chat readers.Alternatives rejected:
```chartfences in the channel adapter. That would pull browser rendering into the Runtime process and duplicaterender_chart, which already produces PNG artifacts inside the AgentBox isolation boundary.sanitizeMarkdownForDingTalkstrips image URLs on purpose so a prompt-injected agent cannot make clients auto-fetch an attacker-controlled endpoint.Test Plan
npx tsc --noEmitpasses (no diagnostics)npm testpasses — could not complete locally; long-running commands are cut short in this environment. Targeted suites that cover the touched code all pass:src/gateway/channels367 tests,src/core646 tests,src/gateway/agentbox287 tests. Please let CI be the gate on the full suite.reply_mode=ai_cardpluscard_template_id, then confirm streaming body, image blocks, and the terminal card state.Rebase note for reviewers: this branch was replayed onto current
main, which introducedsessionTurnLocks.cardControllerand the card timeline are now declared outside the turn-locktrybecause the card is finalized, and its timeline read for image embedding, after the lock is released. One behavioural consequence: the card is opened after the turn lock is acquired, so a busy session's "still working" notice goes out over the webhook instead of a card.Architecture Checklist
General Checklist
feat:prefix; happy to reword if that is required.