diff --git a/src/card/reply-dispatcher.ts b/src/card/reply-dispatcher.ts index a2635c61..01c8fbab 100644 --- a/src/card/reply-dispatcher.ts +++ b/src/card/reply-dispatcher.ts @@ -215,12 +215,13 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP // 提取文本和媒体 URL const text = getVisiblePayloadText(payload); + const reasoningText = payload.isReasoning === true ? (payload.text ?? '') : ''; const payloadMediaUrls = payload.mediaUrls?.length ? payload.mediaUrls : payload.mediaUrl ? [payload.mediaUrl] : []; - if (!text.trim() && payloadMediaUrls.length === 0) { + if (!text.trim() && !reasoningText.trim() && payloadMediaUrls.length === 0) { log.debug('deliver: empty text and no media, skipping'); return; } @@ -232,12 +233,17 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP return; } - if (text.trim()) { + const controllerText = reasoningText.trim() ? reasoningText : text; + if (controllerText.trim()) { await controller.ensureCardCreated(); if (controller.isTerminated) return; if (controller.cardMessageId) { - await controller.onDeliver({ ...payload, text }); + if (payload.isReasoning === true) { + await controller.onReasoningStream({ ...payload, text: controllerText }); + return; + } + await controller.onDeliver({ ...payload, text: controllerText }); return; } // Card creation failed — fall through to static delivery diff --git a/tests/reply-dispatcher-tool-use.test.ts b/tests/reply-dispatcher-tool-use.test.ts index 5c3a8f1a..a7a6a1d9 100644 --- a/tests/reply-dispatcher-tool-use.test.ts +++ b/tests/reply-dispatcher-tool-use.test.ts @@ -184,6 +184,64 @@ describe('reply-dispatcher tool_use mode', () => { expect(controllerSpies.onDeliver).not.toHaveBeenCalled(); }); + it('routes reasoning payload text through the streaming card reasoning lane', async () => { + const result = createFeishuReplyDispatcher({ + cfg: {} as never, + agentId: 'main', + sessionKey: 'agent:main:feishu:dm:user-1', + chatId: 'chat-1', + accountId: 'default', + chatType: 'p2p', + replyInThread: false, + skipTyping: true, + toolUseDisplay: { + mode: 'on', + showToolUse: true, + showToolResultDetails: false, + showFullPaths: false, + }, + }); + + result.dispatcher.sendFinalReply({ text: 'Reasoning:\n_checking context_', isReasoning: true }); + await Promise.resolve(); + + expect(controllerSpies.ensureCardCreated).toHaveBeenCalled(); + expect(controllerSpies.onReasoningStream).toHaveBeenCalledWith({ + text: 'Reasoning:\n_checking context_', + isReasoning: true, + }); + expect(controllerSpies.onDeliver).not.toHaveBeenCalled(); + }); + + it('routes bare reasoning payload text through the streaming card reasoning lane', async () => { + const result = createFeishuReplyDispatcher({ + cfg: {} as never, + agentId: 'main', + sessionKey: 'agent:main:feishu:dm:user-1', + chatId: 'chat-1', + accountId: 'default', + chatType: 'p2p', + replyInThread: false, + skipTyping: true, + toolUseDisplay: { + mode: 'on', + showToolUse: true, + showToolResultDetails: false, + showFullPaths: false, + }, + }); + + result.dispatcher.sendFinalReply({ text: 'checking context', isReasoning: true }); + await Promise.resolve(); + + expect(controllerSpies.ensureCardCreated).toHaveBeenCalled(); + expect(controllerSpies.onReasoningStream).toHaveBeenCalledWith({ + text: 'checking context', + isReasoning: true, + }); + expect(controllerSpies.onDeliver).not.toHaveBeenCalled(); + }); + it('preserves SDK tool-result emission in static mode', () => { replyModeState.mode = 'static';