fix(dingtalk): preserve richText formatting and set HTTP client timeout

richText messages only kept rich_text_list items whose type was
"text", so bold/italic/inlineCode/pre segments were silently dropped;
a message made entirely of formatted segments produced empty content
and fell through to the "unsupported message type: richText" warning.
Now any item carrying text is kept (matching the SDK's own
get_text_list, which keys off the "text" field) and its type is mapped
to Markdown so the formatting survives into the agent's context.

Text and downloadCode within a rich-text item are handled
independently (the SDK treats them as separate via get_text_list /
get_image_list), so an item carrying both a caption and an attachment
no longer drops the file.

The shared httpx.AsyncClient was created without a timeout, so all
requests (including large file/image downloads) used httpx's 5s
default and hit ConnectTimeout/ReadTimeout on uploads. Set an explicit
httpx.Timeout (connect=10s, read/write=30s).

Adds regression tests for formatted-segment preservation, the
all-formatted no-drop case, the text+downloadCode item, and the client
timeout configuration.

Closes #4497
This commit is contained in:
Max Hsu
2026-06-25 22:52:45 +08:00
committed by Xubin Ren
parent 42aa37cfc0
commit 638af123ba
2 changed files with 164 additions and 6 deletions
+20 -6
View File
@@ -94,11 +94,23 @@ class NanobotDingTalkHandler(CallbackHandler):
for item in rich_list:
if not isinstance(item, dict):
continue
if item.get("type") == "text":
t = item.get("text", "").strip()
if t:
content = (content + " " + t).strip() if content else t
elif item.get("downloadCode"):
# A rich-text item may carry text and/or a downloadCode; the
# DingTalk SDK treats them independently, so handle both.
t = item.get("text", "").strip()
if t:
fmt = item.get("type", "")
if fmt == "bold":
formatted = f"**{t}**"
elif fmt == "italic":
formatted = f"*{t}*"
elif fmt == "inlineCode":
formatted = f"`{t}`"
elif fmt == "pre":
formatted = f"```\n{t}\n```"
else:
formatted = t
content = (content + " " + formatted).strip() if content else formatted
if item.get("downloadCode"):
dc = item["downloadCode"]
fname = item.get("fileName") or "file"
sender_uid = chatbot_msg.sender_staff_id or chatbot_msg.sender_id or "unknown"
@@ -214,7 +226,9 @@ class DingTalkChannel(BaseChannel):
return
self._running = True
self._http = httpx.AsyncClient()
self._http = httpx.AsyncClient(
timeout=httpx.Timeout(10.0, connect=10.0, read=30.0, write=30.0, pool=10.0)
)
self.logger.info(
"Initializing Stream Client with Client ID: {}...",