Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions src/card/reply-dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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
Expand Down
58 changes: 58 additions & 0 deletions tests/reply-dispatcher-tool-use.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
Loading