Skip to content

Commit b9f3f8d

Browse files
fix(feishu): use probed botName for mention checks (openclaw#36391)
1 parent ba223c7 commit b9f3f8d

7 files changed

Lines changed: 87 additions & 18 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Docs: https://docs.openclaw.ai
2929
- 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.
3030
- 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.
3131
- 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.
32+
- 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.
3233
- 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.
3334
- 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.
3435
- 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.

extensions/feishu/src/monitor.account.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ import {
1919
warmupDedupFromDisk,
2020
} from "./dedup.js";
2121
import { isMentionForwardRequest } from "./mention.js";
22-
import { fetchBotOpenIdForMonitor } from "./monitor.startup.js";
23-
import { botOpenIds } from "./monitor.state.js";
22+
import { fetchBotIdentityForMonitor } from "./monitor.startup.js";
23+
import { botNames, botOpenIds } from "./monitor.state.js";
2424
import { monitorWebhook, monitorWebSocket } from "./monitor.transport.js";
2525
import { getFeishuRuntime } from "./runtime.js";
2626
import { getMessageFeishu } from "./send.js";
@@ -247,6 +247,7 @@ function registerEventHandlers(
247247
cfg,
248248
event,
249249
botOpenId: botOpenIds.get(accountId),
250+
botName: botNames.get(accountId),
250251
runtime,
251252
chatHistories,
252253
accountId,
@@ -260,7 +261,7 @@ function registerEventHandlers(
260261
};
261262
const resolveDebounceText = (event: FeishuMessageEvent): string => {
262263
const botOpenId = botOpenIds.get(accountId);
263-
const parsed = parseFeishuMessageEvent(event, botOpenId);
264+
const parsed = parseFeishuMessageEvent(event, botOpenId, botNames.get(accountId));
264265
return parsed.content.trim();
265266
};
266267
const recordSuppressedMessageIds = async (
@@ -430,6 +431,7 @@ function registerEventHandlers(
430431
cfg,
431432
event: syntheticEvent,
432433
botOpenId: myBotId,
434+
botName: botNames.get(accountId),
433435
runtime,
434436
chatHistories,
435437
accountId,
@@ -483,7 +485,9 @@ function registerEventHandlers(
483485
});
484486
}
485487

486-
export type BotOpenIdSource = { kind: "prefetched"; botOpenId?: string } | { kind: "fetch" };
488+
export type BotOpenIdSource =
489+
| { kind: "prefetched"; botOpenId?: string; botName?: string }
490+
| { kind: "fetch" };
487491

488492
export type MonitorSingleAccountParams = {
489493
cfg: ClawdbotConfig;
@@ -499,11 +503,18 @@ export async function monitorSingleAccount(params: MonitorSingleAccountParams):
499503
const log = runtime?.log ?? console.log;
500504

501505
const botOpenIdSource = params.botOpenIdSource ?? { kind: "fetch" };
502-
const botOpenId =
506+
const botIdentity =
503507
botOpenIdSource.kind === "prefetched"
504-
? botOpenIdSource.botOpenId
505-
: await fetchBotOpenIdForMonitor(account, { runtime, abortSignal });
508+
? { botOpenId: botOpenIdSource.botOpenId, botName: botOpenIdSource.botName }
509+
: await fetchBotIdentityForMonitor(account, { runtime, abortSignal });
510+
const botOpenId = botIdentity.botOpenId;
511+
const botName = botIdentity.botName?.trim();
506512
botOpenIds.set(accountId, botOpenId ?? "");
513+
if (botName) {
514+
botNames.set(accountId, botName);
515+
} else {
516+
botNames.delete(accountId);
517+
}
507518
log(`feishu[${accountId}]: bot open_id resolved: ${botOpenId ?? "unknown"}`);
508519

509520
const connectionMode = account.config.connectionMode ?? "websocket";

extensions/feishu/src/monitor.reaction.test.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,10 @@ function createTextEvent(params: {
109109
};
110110
}
111111

112-
async function setupDebounceMonitor(): Promise<(data: unknown) => Promise<void>> {
112+
async function setupDebounceMonitor(params?: {
113+
botOpenId?: string;
114+
botName?: string;
115+
}): Promise<(data: unknown) => Promise<void>> {
113116
const register = vi.fn((registered: Record<string, (data: unknown) => Promise<void>>) => {
114117
handlers = registered;
115118
});
@@ -123,7 +126,11 @@ async function setupDebounceMonitor(): Promise<(data: unknown) => Promise<void>>
123126
error: vi.fn(),
124127
exit: vi.fn(),
125128
} as RuntimeEnv,
126-
botOpenIdSource: { kind: "prefetched", botOpenId: "ou_bot" },
129+
botOpenIdSource: {
130+
kind: "prefetched",
131+
botOpenId: params?.botOpenId ?? "ou_bot",
132+
botName: params?.botName,
133+
},
127134
});
128135

129136
const onMessage = handlers["im.message.receive_v1"];
@@ -434,6 +441,37 @@ describe("Feishu inbound debounce regressions", () => {
434441
expect(mergedMentions.some((mention) => mention.id.open_id === "ou_user_a")).toBe(false);
435442
});
436443

444+
it("passes prefetched botName through to handleFeishuMessage", async () => {
445+
vi.spyOn(dedup, "tryRecordMessage").mockReturnValue(true);
446+
vi.spyOn(dedup, "tryRecordMessagePersistent").mockResolvedValue(true);
447+
vi.spyOn(dedup, "hasRecordedMessage").mockReturnValue(false);
448+
vi.spyOn(dedup, "hasRecordedMessagePersistent").mockResolvedValue(false);
449+
const onMessage = await setupDebounceMonitor({ botName: "OpenClaw Bot" });
450+
451+
await onMessage(
452+
createTextEvent({
453+
messageId: "om_name_passthrough",
454+
text: "@bot hello",
455+
mentions: [
456+
{
457+
key: "@_user_1",
458+
id: { open_id: "ou_bot" },
459+
name: "OpenClaw Bot",
460+
},
461+
],
462+
}),
463+
);
464+
await Promise.resolve();
465+
await Promise.resolve();
466+
await vi.advanceTimersByTimeAsync(25);
467+
468+
expect(handleFeishuMessageMock).toHaveBeenCalledTimes(1);
469+
const firstParams = handleFeishuMessageMock.mock.calls[0]?.[0] as
470+
| { botName?: string }
471+
| undefined;
472+
expect(firstParams?.botName).toBe("OpenClaw Bot");
473+
});
474+
437475
it("does not synthesize mention-forward intent across separate messages", async () => {
438476
vi.spyOn(dedup, "tryRecordMessage").mockReturnValue(true);
439477
vi.spyOn(dedup, "tryRecordMessagePersistent").mockResolvedValue(true);

extensions/feishu/src/monitor.startup.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ type FetchBotOpenIdOptions = {
1010
timeoutMs?: number;
1111
};
1212

13+
export type FeishuMonitorBotIdentity = {
14+
botOpenId?: string;
15+
botName?: string;
16+
};
17+
1318
function isTimeoutErrorMessage(message: string | undefined): boolean {
1419
return message?.toLowerCase().includes("timeout") || message?.toLowerCase().includes("timed out")
1520
? true
@@ -20,12 +25,12 @@ function isAbortErrorMessage(message: string | undefined): boolean {
2025
return message?.toLowerCase().includes("aborted") ?? false;
2126
}
2227

23-
export async function fetchBotOpenIdForMonitor(
28+
export async function fetchBotIdentityForMonitor(
2429
account: ResolvedFeishuAccount,
2530
options: FetchBotOpenIdOptions = {},
26-
): Promise<string | undefined> {
31+
): Promise<FeishuMonitorBotIdentity> {
2732
if (options.abortSignal?.aborted) {
28-
return undefined;
33+
return {};
2934
}
3035

3136
const timeoutMs = options.timeoutMs ?? FEISHU_STARTUP_BOT_INFO_TIMEOUT_MS;
@@ -34,11 +39,11 @@ export async function fetchBotOpenIdForMonitor(
3439
abortSignal: options.abortSignal,
3540
});
3641
if (result.ok) {
37-
return result.botOpenId;
42+
return { botOpenId: result.botOpenId, botName: result.botName };
3843
}
3944

4045
if (options.abortSignal?.aborted || isAbortErrorMessage(result.error)) {
41-
return undefined;
46+
return {};
4247
}
4348

4449
if (isTimeoutErrorMessage(result.error)) {
@@ -47,5 +52,13 @@ export async function fetchBotOpenIdForMonitor(
4752
`feishu[${account.accountId}]: bot info probe timed out after ${timeoutMs}ms; continuing startup`,
4853
);
4954
}
50-
return undefined;
55+
return {};
56+
}
57+
58+
export async function fetchBotOpenIdForMonitor(
59+
account: ResolvedFeishuAccount,
60+
options: FetchBotOpenIdOptions = {},
61+
): Promise<string | undefined> {
62+
const identity = await fetchBotIdentityForMonitor(account, options);
63+
return identity.botOpenId;
5164
}

extensions/feishu/src/monitor.state.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
export const wsClients = new Map<string, Lark.WSClient>();
1212
export const httpServers = new Map<string, http.Server>();
1313
export const botOpenIds = new Map<string, string>();
14+
export const botNames = new Map<string, string>();
1415

1516
export const FEISHU_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024;
1617
export const FEISHU_WEBHOOK_BODY_TIMEOUT_MS = 30_000;
@@ -140,6 +141,7 @@ export function stopFeishuMonitorState(accountId?: string): void {
140141
httpServers.delete(accountId);
141142
}
142143
botOpenIds.delete(accountId);
144+
botNames.delete(accountId);
143145
return;
144146
}
145147

@@ -149,4 +151,5 @@ export function stopFeishuMonitorState(accountId?: string): void {
149151
}
150152
httpServers.clear();
151153
botOpenIds.clear();
154+
botNames.clear();
152155
}

extensions/feishu/src/monitor.transport.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
} from "openclaw/plugin-sdk/feishu";
88
import { createFeishuWSClient } from "./client.js";
99
import {
10+
botNames,
1011
botOpenIds,
1112
FEISHU_WEBHOOK_BODY_TIMEOUT_MS,
1213
FEISHU_WEBHOOK_MAX_BODY_BYTES,
@@ -42,6 +43,7 @@ export async function monitorWebSocket({
4243
const cleanup = () => {
4344
wsClients.delete(accountId);
4445
botOpenIds.delete(accountId);
46+
botNames.delete(accountId);
4547
};
4648

4749
const handleAbort = () => {
@@ -134,6 +136,7 @@ export async function monitorWebhook({
134136
server.close();
135137
httpServers.delete(accountId);
136138
botOpenIds.delete(accountId);
139+
botNames.delete(accountId);
137140
};
138141

139142
const handleAbort = () => {

extensions/feishu/src/monitor.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
resolveReactionSyntheticEvent,
66
type FeishuReactionCreatedEvent,
77
} from "./monitor.account.js";
8-
import { fetchBotOpenIdForMonitor } from "./monitor.startup.js";
8+
import { fetchBotIdentityForMonitor } from "./monitor.startup.js";
99
import {
1010
clearFeishuWebhookRateLimitStateForTest,
1111
getFeishuWebhookRateLimitStateSizeForTest,
@@ -66,7 +66,7 @@ export async function monitorFeishuProvider(opts: MonitorFeishuOpts = {}): Promi
6666
}
6767

6868
// Probe sequentially so large multi-account startups do not burst Feishu's bot-info endpoint.
69-
const botOpenId = await fetchBotOpenIdForMonitor(account, {
69+
const { botOpenId, botName } = await fetchBotIdentityForMonitor(account, {
7070
runtime: opts.runtime,
7171
abortSignal: opts.abortSignal,
7272
});
@@ -82,7 +82,7 @@ export async function monitorFeishuProvider(opts: MonitorFeishuOpts = {}): Promi
8282
account,
8383
runtime: opts.runtime,
8484
abortSignal: opts.abortSignal,
85-
botOpenIdSource: { kind: "prefetched", botOpenId },
85+
botOpenIdSource: { kind: "prefetched", botOpenId, botName },
8686
}),
8787
);
8888
}

0 commit comments

Comments
 (0)