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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 87 additions and 18 deletions

View File

@ -29,6 +29,7 @@ Docs: https://docs.openclaw.ai
- Gateway/Control UI version reporting: align runtime and browser client version metadata to avoid `dev` placeholders, wait for bootstrap version before first UI websocket connect, and only forward bootstrap `serverVersion` to same-origin gateway targets to prevent cross-target version leakage. (from #35230, #30928, #33928) Thanks @Sid-Qin, @joelnishanth, and @MoerAI.
- Web UI/config form: treat `additionalProperties: true` object schemas as editable map entries instead of unsupported fields so Accounts-style maps stay editable in form mode. (#35380, supersedes #32072) Thanks @stakeswky and @liuxiaopai-ai.
- Feishu/streaming card delivery synthesis: unify snapshot and delta streaming merge semantics, apply overlap-aware final merge, suppress duplicate final text delivery (including text+media final packets), prefer topic-thread `message.reply` routing when a reply target exists, and tune card print cadence to avoid duplicate incremental rendering. (from #33245, #32896, #33840) Thanks @rexl2018, @kcinzgg, and @aerelune.
- Feishu/group mention detection: carry startup-probed bot display names through monitor dispatch so `requireMention` checks compare against current bot identity instead of stale config names, fixing missed `@bot` handling in groups while preserving multi-bot false-positive guards. (#36317, #34271) Thanks @liuxiaopai-ai.
- Security/dependency audit: patch transitive Hono vulnerabilities by pinning `hono` to `4.12.5` and `@hono/node-server` to `1.19.10` in production resolution paths. Thanks @shakkernerd.
- Security/dependency audit: bump `tar` to `7.5.10` (from `7.5.9`) to address the high-severity hardlink path traversal advisory (`GHSA-qffp-2rhf-9h96`). Thanks @shakkernerd.
- Cron/announce delivery robustness: bypass pending-descendant announce guards for cron completion sends, ensure named-agent announce routes have outbound session entries, and fall back to direct delivery only when an announce send was actually attempted and failed. (from #35185, #32443, #34987) Thanks @Sid-Qin, @scoootscooob, and @bmendonca3.

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 },
}),
);
}