fix(dingtalk): repair _extract_text for dingtalk-stream >= 0.20 SDK shape

The cherry-picked SDK compat fix (previous commit) wired process() to
parse CallbackMessage.data into a ChatbotMessage, but _extract_text()
was still written against the pre-0.20 payload shape:

  * message.text changed from dict {content: ...} → TextContent object.
    The old code's str(text) fallback produced 'TextContent(content=...)'
    as the agent's input, so every received message came in mangled.
  * rich_text moved from message.rich_text (list) to
    message.rich_text_content.rich_text_list.

This preserves legacy fallbacks (dict-shaped text, bare rich_text list)
while handling the current SDK layout via hasattr(text, 'content').

Adds regression tests covering:
  * webhook domain allowlist (api.*, oapi.*, and hostile lookalikes)
  * _IncomingHandler.process is a coroutine function
  * _extract_text against TextContent object, dict, rich_text_content,
    legacy rich_text, and empty-message cases

Also adds kevinskysunny to scripts/release.py AUTHOR_MAP (release CI
blocks unmapped emails).
This commit is contained in:
Teknium
2026-04-17 00:38:16 -07:00
committed by Teknium
parent c3d2895b18
commit 3438d274f6
3 changed files with 158 additions and 10 deletions

View File

@@ -238,18 +238,35 @@ class DingTalkAdapter(BasePlatformAdapter):
@staticmethod
def _extract_text(message: "ChatbotMessage") -> str:
"""Extract plain text from a DingTalk chatbot message."""
text = getattr(message, "text", None) or ""
if isinstance(text, dict):
content = text.get("content", "").strip()
else:
content = str(text).strip()
"""Extract plain text from a DingTalk chatbot message.
Handles both legacy and current dingtalk-stream SDK payload shapes:
* legacy: ``message.text`` was a dict ``{"content": "..."}``
* >= 0.20: ``message.text`` is a ``TextContent`` dataclass whose
``__str__`` returns ``"TextContent(content=...)"`` — never fall
back to ``str(text)`` without extracting ``.content`` first.
* rich text moved from ``message.rich_text`` (list) to
``message.rich_text_content.rich_text_list`` (list of dicts).
"""
text = getattr(message, "text", None)
content = ""
if text is not None:
if isinstance(text, dict):
content = (text.get("content") or "").strip()
elif hasattr(text, "content"):
content = str(text.content or "").strip()
else:
content = str(text).strip()
# Fall back to rich text if present
if not content:
rich_text = getattr(message, "rich_text", None)
if rich_text and isinstance(rich_text, list):
parts = [item["text"] for item in rich_text
rich_list = None
rtc = getattr(message, "rich_text_content", None)
if rtc is not None and hasattr(rtc, "rich_text_list"):
rich_list = rtc.rich_text_list
if rich_list is None:
rich_list = getattr(message, "rich_text", None)
if rich_list and isinstance(rich_list, list):
parts = [item["text"] for item in rich_list
if isinstance(item, dict) and item.get("text")]
content = " ".join(parts).strip()
return content