fix(feishu): use probed botName for mention checks (#36391)

This commit is contained in:
Liu Xiaopai
2026-03-06 00:55:04 +08:00
committed by GitHub
parent ba223c7766
commit b9f3f8d737
7 changed files with 87 additions and 18 deletions

View File

@@ -19,8 +19,8 @@ import {
warmupDedupFromDisk,
} from "./dedup.js";
import { isMentionForwardRequest } from "./mention.js";
import { fetchBotOpenIdForMonitor } from "./monitor.startup.js";
import { botOpenIds } from "./monitor.state.js";
import { fetchBotIdentityForMonitor } from "./monitor.startup.js";
import { botNames, botOpenIds } from "./monitor.state.js";
import { monitorWebhook, monitorWebSocket } from "./monitor.transport.js";
import { getFeishuRuntime } from "./runtime.js";
import { getMessageFeishu } from "./send.js";
@@ -247,6 +247,7 @@ function registerEventHandlers(
cfg,
event,
botOpenId: botOpenIds.get(accountId),
botName: botNames.get(accountId),
runtime,
chatHistories,
accountId,
@@ -260,7 +261,7 @@ function registerEventHandlers(
};
const resolveDebounceText = (event: FeishuMessageEvent): string => {
const botOpenId = botOpenIds.get(accountId);
const parsed = parseFeishuMessageEvent(event, botOpenId);
const parsed = parseFeishuMessageEvent(event, botOpenId, botNames.get(accountId));
return parsed.content.trim();
};
const recordSuppressedMessageIds = async (
@@ -430,6 +431,7 @@ function registerEventHandlers(
cfg,
event: syntheticEvent,
botOpenId: myBotId,
botName: botNames.get(accountId),
runtime,
chatHistories,
accountId,
@@ -483,7 +485,9 @@ function registerEventHandlers(
});
}
export type BotOpenIdSource = { kind: "prefetched"; botOpenId?: string } | { kind: "fetch" };
export type BotOpenIdSource =
| { kind: "prefetched"; botOpenId?: string; botName?: string }
| { kind: "fetch" };
export type MonitorSingleAccountParams = {
cfg: ClawdbotConfig;
@@ -499,11 +503,18 @@ export async function monitorSingleAccount(params: MonitorSingleAccountParams):
const log = runtime?.log ?? console.log;
const botOpenIdSource = params.botOpenIdSource ?? { kind: "fetch" };
const botOpenId =
const botIdentity =
botOpenIdSource.kind === "prefetched"
? botOpenIdSource.botOpenId
: await fetchBotOpenIdForMonitor(account, { runtime, abortSignal });
? { botOpenId: botOpenIdSource.botOpenId, botName: botOpenIdSource.botName }
: await fetchBotIdentityForMonitor(account, { runtime, abortSignal });
const botOpenId = botIdentity.botOpenId;
const botName = botIdentity.botName?.trim();
botOpenIds.set(accountId, botOpenId ?? "");
if (botName) {
botNames.set(accountId, botName);
} else {
botNames.delete(accountId);
}
log(`feishu[${accountId}]: bot open_id resolved: ${botOpenId ?? "unknown"}`);
const connectionMode = account.config.connectionMode ?? "websocket";

View File

@@ -109,7 +109,10 @@ function createTextEvent(params: {
};
}
async function setupDebounceMonitor(): Promise<(data: unknown) => Promise<void>> {
async function setupDebounceMonitor(params?: {
botOpenId?: string;
botName?: string;
}): Promise<(data: unknown) => Promise<void>> {
const register = vi.fn((registered: Record<string, (data: unknown) => Promise<void>>) => {
handlers = registered;
});
@@ -123,7 +126,11 @@ async function setupDebounceMonitor(): Promise<(data: unknown) => Promise<void>>
error: vi.fn(),
exit: vi.fn(),
} as RuntimeEnv,
botOpenIdSource: { kind: "prefetched", botOpenId: "ou_bot" },
botOpenIdSource: {
kind: "prefetched",
botOpenId: params?.botOpenId ?? "ou_bot",
botName: params?.botName,
},
});
const onMessage = handlers["im.message.receive_v1"];
@@ -434,6 +441,37 @@ describe("Feishu inbound debounce regressions", () => {
expect(mergedMentions.some((mention) => mention.id.open_id === "ou_user_a")).toBe(false);
});
it("passes prefetched botName through to handleFeishuMessage", async () => {
vi.spyOn(dedup, "tryRecordMessage").mockReturnValue(true);
vi.spyOn(dedup, "tryRecordMessagePersistent").mockResolvedValue(true);
vi.spyOn(dedup, "hasRecordedMessage").mockReturnValue(false);
vi.spyOn(dedup, "hasRecordedMessagePersistent").mockResolvedValue(false);
const onMessage = await setupDebounceMonitor({ botName: "OpenClaw Bot" });
await onMessage(
createTextEvent({
messageId: "om_name_passthrough",
text: "@bot hello",
mentions: [
{
key: "@_user_1",
id: { open_id: "ou_bot" },
name: "OpenClaw Bot",
},
],
}),
);
await Promise.resolve();
await Promise.resolve();
await vi.advanceTimersByTimeAsync(25);
expect(handleFeishuMessageMock).toHaveBeenCalledTimes(1);
const firstParams = handleFeishuMessageMock.mock.calls[0]?.[0] as
| { botName?: string }
| undefined;
expect(firstParams?.botName).toBe("OpenClaw Bot");
});
it("does not synthesize mention-forward intent across separate messages", async () => {
vi.spyOn(dedup, "tryRecordMessage").mockReturnValue(true);
vi.spyOn(dedup, "tryRecordMessagePersistent").mockResolvedValue(true);

View File

@@ -10,6 +10,11 @@ type FetchBotOpenIdOptions = {
timeoutMs?: number;
};
export type FeishuMonitorBotIdentity = {
botOpenId?: string;
botName?: string;
};
function isTimeoutErrorMessage(message: string | undefined): boolean {
return message?.toLowerCase().includes("timeout") || message?.toLowerCase().includes("timed out")
? true
@@ -20,12 +25,12 @@ function isAbortErrorMessage(message: string | undefined): boolean {
return message?.toLowerCase().includes("aborted") ?? false;
}
export async function fetchBotOpenIdForMonitor(
export async function fetchBotIdentityForMonitor(
account: ResolvedFeishuAccount,
options: FetchBotOpenIdOptions = {},
): Promise<string | undefined> {
): Promise<FeishuMonitorBotIdentity> {
if (options.abortSignal?.aborted) {
return undefined;
return {};
}
const timeoutMs = options.timeoutMs ?? FEISHU_STARTUP_BOT_INFO_TIMEOUT_MS;
@@ -34,11 +39,11 @@ export async function fetchBotOpenIdForMonitor(
abortSignal: options.abortSignal,
});
if (result.ok) {
return result.botOpenId;
return { botOpenId: result.botOpenId, botName: result.botName };
}
if (options.abortSignal?.aborted || isAbortErrorMessage(result.error)) {
return undefined;
return {};
}
if (isTimeoutErrorMessage(result.error)) {
@@ -47,5 +52,13 @@ export async function fetchBotOpenIdForMonitor(
`feishu[${account.accountId}]: bot info probe timed out after ${timeoutMs}ms; continuing startup`,
);
}
return undefined;
return {};
}
export async function fetchBotOpenIdForMonitor(
account: ResolvedFeishuAccount,
options: FetchBotOpenIdOptions = {},
): Promise<string | undefined> {
const identity = await fetchBotIdentityForMonitor(account, options);
return identity.botOpenId;
}

View File

@@ -11,6 +11,7 @@ import {
export const wsClients = new Map<string, Lark.WSClient>();
export const httpServers = new Map<string, http.Server>();
export const botOpenIds = new Map<string, string>();
export const botNames = new Map<string, string>();
export const FEISHU_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024;
export const FEISHU_WEBHOOK_BODY_TIMEOUT_MS = 30_000;
@@ -140,6 +141,7 @@ export function stopFeishuMonitorState(accountId?: string): void {
httpServers.delete(accountId);
}
botOpenIds.delete(accountId);
botNames.delete(accountId);
return;
}
@@ -149,4 +151,5 @@ export function stopFeishuMonitorState(accountId?: string): void {
}
httpServers.clear();
botOpenIds.clear();
botNames.clear();
}

View File

@@ -7,6 +7,7 @@ import {
} from "openclaw/plugin-sdk/feishu";
import { createFeishuWSClient } from "./client.js";
import {
botNames,
botOpenIds,
FEISHU_WEBHOOK_BODY_TIMEOUT_MS,
FEISHU_WEBHOOK_MAX_BODY_BYTES,
@@ -42,6 +43,7 @@ export async function monitorWebSocket({
const cleanup = () => {
wsClients.delete(accountId);
botOpenIds.delete(accountId);
botNames.delete(accountId);
};
const handleAbort = () => {
@@ -134,6 +136,7 @@ export async function monitorWebhook({
server.close();
httpServers.delete(accountId);
botOpenIds.delete(accountId);
botNames.delete(accountId);
};
const handleAbort = () => {

View File

@@ -5,7 +5,7 @@ import {
resolveReactionSyntheticEvent,
type FeishuReactionCreatedEvent,
} from "./monitor.account.js";
import { fetchBotOpenIdForMonitor } from "./monitor.startup.js";
import { fetchBotIdentityForMonitor } from "./monitor.startup.js";
import {
clearFeishuWebhookRateLimitStateForTest,
getFeishuWebhookRateLimitStateSizeForTest,
@@ -66,7 +66,7 @@ export async function monitorFeishuProvider(opts: MonitorFeishuOpts = {}): Promi
}
// Probe sequentially so large multi-account startups do not burst Feishu's bot-info endpoint.
const botOpenId = await fetchBotOpenIdForMonitor(account, {
const { botOpenId, botName } = await fetchBotIdentityForMonitor(account, {
runtime: opts.runtime,
abortSignal: opts.abortSignal,
});
@@ -82,7 +82,7 @@ export async function monitorFeishuProvider(opts: MonitorFeishuOpts = {}): Promi
account,
runtime: opts.runtime,
abortSignal: opts.abortSignal,
botOpenIdSource: { kind: "prefetched", botOpenId },
botOpenIdSource: { kind: "prefetched", botOpenId, botName },
}),
);
}