From 5bef83a1565894f4118c6df9638ce798ac5e8162 Mon Sep 17 00:00:00 2001 From: matevip Date: Tue, 28 Apr 2026 11:13:13 +0800 Subject: [PATCH] fix(dingtalk): forward voice messages by reading recognition from stream payload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The stream SDK delivers voice messages as ChatbotMessage with msgtype=audio and the server-side ASR result already filled into MessageContent.recognition (same shape as WeCom's voice.content). The adapter's handleStreamMessage only read msg.getText(), which is null for audio events, so the message landed in handleWebhook with no msgtype, fell through to the default text branch, found null content, and got dropped at 'Empty message content, ignoring'. From the user's side: send a voice, nothing happens, no log of the attempt. Two surgical edits: - handleStreamMessage now checks getContent().getRecognition() first; if present and non-blank, builds payload {msgtype: audio, audio: {recognition}} before falling back to the existing text path. The earlier comment about richText being handled inside handleWebhook was wrong — picture and richText also need their fields propagated through the payload Map; left a TODO for them. - handleWebhook gains an explicit case 'audio' branch that pulls text out of audio.recognition and pushes it onto contentParts. - ChannelMessage.inputMode now reflects 'voice' when msgtype=audio, mirroring feishu's behavior so downstream code (memory-extraction filters, voice-themed system prompts) can tell text vs voice turns apart. No STT call required — DingTalk transcribes server-side and ships text in the webhook, so this is a 0-network, 0-config fix. --- .../dingtalk/DingTalkChannelAdapter.java | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/mateclaw-server/src/main/java/vip/mate/channel/dingtalk/DingTalkChannelAdapter.java b/mateclaw-server/src/main/java/vip/mate/channel/dingtalk/DingTalkChannelAdapter.java index de55c738..49508735 100644 --- a/mateclaw-server/src/main/java/vip/mate/channel/dingtalk/DingTalkChannelAdapter.java +++ b/mateclaw-server/src/main/java/vip/mate/channel/dingtalk/DingTalkChannelAdapter.java @@ -166,11 +166,19 @@ public class DingTalkChannelAdapter extends AbstractChannelAdapter implements St payload.put("sessionWebhook", msg.getSessionWebhook()); // 消息内容 - if (msg.getText() != null) { + // 钉钉服务端已经把语音转写好放在 MessageContent.recognition 里(跟 + // 企业微信 voice.content 一个模式),不需要 STT。优先读 recognition; + // 否则读 text.content。其他复杂类型(picture / richText)暂由 + // handleWebhook 内部处理 —— 但 stream 模式下我们目前没把那些类型 + // 的字段塞进 payload,是个遗留待修项(picture / richText 同样会掉消息)。 + String recognition = msg.getContent() != null ? msg.getContent().getRecognition() : null; + if (recognition != null && !recognition.isBlank()) { + payload.put("msgtype", "audio"); + payload.put("audio", Map.of("recognition", recognition)); + } else if (msg.getText() != null) { payload.put("msgtype", "text"); payload.put("text", Map.of("content", msg.getText().getContent() != null ? msg.getText().getContent() : "")); } - // richText 等复杂类型暂由 handleWebhook 内部处理 handleWebhook(payload); } catch (Exception e) { @@ -385,6 +393,15 @@ public class DingTalkChannelAdapter extends AbstractChannelAdapter implements St textContent = textBuilder.toString().trim(); } } + } else if ("audio".equals(msgtype)) { + // 钉钉服务端已经把语音转写好放在 audio.recognition 里。这跟企业微信 + // 的 voice.content 是一个模式 —— webhook 自带 ASR 文本,0 STT 调用。 + Map audioBody = (Map) payload.get("audio"); + String recognition = audioBody != null ? (String) audioBody.get("recognition") : null; + if (recognition != null && !recognition.isBlank()) { + textContent = recognition.trim(); + contentParts.add(MessageContentPart.text(textContent)); + } } else { // 默认 text 消息 Map msgBody = (Map) payload.get("text"); @@ -424,6 +441,7 @@ public class DingTalkChannelAdapter extends AbstractChannelAdapter implements St .content(content) .contentType(contentParts.stream().anyMatch(p -> "image".equals(p.getType())) ? "image" : "text") .contentParts(contentParts) + .inputMode("audio".equals(msgtype) ? "voice" : "text") .timestamp(LocalDateTime.now()) .rawPayload(payload) .build();