fix: address 4 PR review issues in feishu cross-bot relay

Issue 2: card path skipped relay in outbound.ts
- Extract triggerRelay() helper to avoid duplication
- Card branch now calls triggerRelay() before returning instead of early-returning without relay
- Also passes threadId to relay so topic threads are preserved

Issue 4: streaming messageId any cast + closeStreaming timing
- Add getMessageId() typed accessor to FeishuStreamingSession
- Save messageId via streaming.getMessageId() BEFORE calling closeStreaming()
  (closeStreaming sets streaming=null, making post-close access always undefined)
- Replace (streaming as any)?.state?.messageId with the typed accessor

Issue 5: sendChunk callbacks missing return value
- sendChunkedTextReply callbacks for both card and text paths now use
  'return sendStructuredCardFeishu(...)' / 'return sendMessageFeishu(...)'
  so result?.messageId is correctly captured in lastMessageId

Issue 8: relay missing topic/thread metadata
- Add threadId field to RelayOutboundParams
- Populate root_id/thread_id in synthetic event from threadId
- Pass rootId from reply-dispatcher relay calls
- Pass threadId from outbound.ts sendText relay calls
This commit is contained in:
Claude Code Agent 2026-03-21 10:39:11 +08:00
parent 9a8a666880
commit c0cead3063
4 changed files with 41 additions and 20 deletions

View File

@ -48,6 +48,8 @@ export type RelayOutboundParams = {
text: string;
/** Message ID returned by Feishu API after sending */
messageId?: string;
/** Thread/topic ID for topic group messages (root_id in Feishu events) */
threadId?: string;
/** Bot's open_id (sender identity) */
senderBotOpenId?: string;
/** Bot's display name */
@ -55,7 +57,8 @@ export type RelayOutboundParams = {
};
export async function relayOutboundToOtherBots(params: RelayOutboundParams): Promise<void> {
const { senderAccountId, chatId, text, messageId, senderBotOpenId, senderBotName } = params;
const { senderAccountId, chatId, text, messageId, threadId, senderBotOpenId, senderBotName } =
params;
// Only relay to group chats
if (!chatId) return;
@ -123,6 +126,8 @@ export async function relayOutboundToOtherBots(params: RelayOutboundParams): Pro
content: JSON.stringify({ text }),
create_time: String(Date.now()),
mentions,
// Preserve thread/topic metadata so relay messages stay in the correct topic
...(threadId ? { root_id: threadId, thread_id: threadId } : {}),
},
};
const log = target.runtime?.log ?? console.log;

View File

@ -120,6 +120,25 @@ export const feishuOutbound: ChannelOutboundAdapter = {
const account = resolveFeishuAccount({ cfg, accountId: accountId ?? undefined });
const renderMode = account.config?.renderMode ?? "auto";
const useCard = renderMode === "card" || (renderMode === "auto" && shouldUseCard(text));
// Cross-bot relay helper — extracted to avoid duplication between card and text paths
const triggerRelay = (result: { messageId?: string }) => {
const feishuCfg = resolveFeishuAccount({ cfg, accountId: accountId ?? undefined }).config;
const effectiveAccountId = accountId ?? undefined;
if (feishuCfg?.crossBotRelay && to.startsWith("oc_") && text?.trim()) {
void relayOutboundToOtherBots({
senderAccountId: effectiveAccountId ?? "default",
chatId: to,
text,
messageId: result.messageId,
threadId: threadId != null ? String(threadId) : undefined,
senderBotOpenId: botOpenIds.get(effectiveAccountId ?? "default"),
senderBotName: botNames.get(effectiveAccountId ?? "default"),
}).catch((err) => {
console.error(`[feishu] cross-bot relay failed:`, err);
});
}
};
if (useCard) {
const header = identity
? {
@ -129,7 +148,7 @@ export const feishuOutbound: ChannelOutboundAdapter = {
template: "blue" as const,
}
: undefined;
return await sendStructuredCardFeishu({
const cardResult = await sendStructuredCardFeishu({
cfg,
to,
text,
@ -138,6 +157,9 @@ export const feishuOutbound: ChannelOutboundAdapter = {
accountId: accountId ?? undefined,
header: header?.title ? header : undefined,
});
// Relay card messages too — previously skipped by early return
triggerRelay(cardResult);
return cardResult;
}
const result = await sendOutboundText({
cfg,
@ -147,21 +169,7 @@ export const feishuOutbound: ChannelOutboundAdapter = {
replyToMessageId,
});
// Cross-bot relay: notify other bots in the same group
const feishuCfg = resolveFeishuAccount({ cfg, accountId: accountId ?? undefined }).config;
if (feishuCfg?.crossBotRelay && to.startsWith("oc_") && text?.trim()) {
const effectiveAccountId = accountId ?? undefined;
void relayOutboundToOtherBots({
senderAccountId: effectiveAccountId ?? "default",
chatId: to,
text,
messageId: result.messageId,
senderBotOpenId: botOpenIds.get(effectiveAccountId ?? "default"),
senderBotName: botNames.get(effectiveAccountId ?? "default"),
}).catch((err) => {
console.error(`[feishu] cross-bot relay failed:`, err);
});
}
triggerRelay(result);
return result;
},

View File

@ -344,6 +344,7 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
chatId,
text: params.text,
messageId: lastMessageId,
threadId: rootId,
senderBotOpenId: botOpenIds.get(accountId ?? "default"),
senderBotName: botNames.get(accountId ?? "default"),
});
@ -426,18 +427,20 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
}
if (info?.kind === "final") {
streamText = mergeStreamingText(streamText, text);
// Save messageId before closeStreaming() clears the streaming object
const streamingMsgId = streaming?.getMessageId();
await closeStreaming();
deliveredFinalTexts.add(text);
// Cross-bot relay: trigger after streaming final
const feishuCfg = resolveFeishuAccount({ cfg, accountId }).config;
if (feishuCfg?.crossBotRelay && chatId?.startsWith("oc_") && text?.trim()) {
try {
const streamingMsgId = (streaming as any)?.state?.messageId;
await relayOutboundToOtherBots({
senderAccountId: accountId ?? "default",
chatId,
text,
messageId: streamingMsgId,
threadId: rootId,
senderBotOpenId: botOpenIds.get(accountId ?? "default"),
senderBotName: botNames.get(accountId ?? "default"),
});
@ -461,7 +464,7 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
useCard: true,
infoKind: info?.kind,
sendChunk: async ({ chunk, isFirst }) => {
await sendStructuredCardFeishu({
return sendStructuredCardFeishu({
cfg,
to: chatId,
text: chunk,
@ -480,7 +483,7 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
useCard: false,
infoKind: info?.kind,
sendChunk: async ({ chunk, isFirst }) => {
await sendMessageFeishu({
return sendMessageFeishu({
cfg,
to: chatId,
text: chunk,

View File

@ -447,4 +447,9 @@ export class FeishuStreamingSession {
isActive(): boolean {
return this.state !== null && !this.closed;
}
/** Returns the Feishu message_id of the streaming card, or undefined if not started. */
getMessageId(): string | undefined {
return this.state?.messageId;
}
}