diff --git a/CHANGELOG.md b/CHANGELOG.md index c96b70c5805..a3ef02393de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/extensions/feishu/src/monitor.account.ts b/extensions/feishu/src/monitor.account.ts index 9fe5eb86a91..601f78f0843 100644 --- a/extensions/feishu/src/monitor.account.ts +++ b/extensions/feishu/src/monitor.account.ts @@ -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"; diff --git a/extensions/feishu/src/monitor.reaction.test.ts b/extensions/feishu/src/monitor.reaction.test.ts index 8bf06b57bab..f69ac647376 100644 --- a/extensions/feishu/src/monitor.reaction.test.ts +++ b/extensions/feishu/src/monitor.reaction.test.ts @@ -109,7 +109,10 @@ function createTextEvent(params: { }; } -async function setupDebounceMonitor(): Promise<(data: unknown) => Promise> { +async function setupDebounceMonitor(params?: { + botOpenId?: string; + botName?: string; +}): Promise<(data: unknown) => Promise> { const register = vi.fn((registered: Record Promise>) => { handlers = registered; }); @@ -123,7 +126,11 @@ async function setupDebounceMonitor(): Promise<(data: unknown) => Promise> 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); diff --git a/extensions/feishu/src/monitor.startup.ts b/extensions/feishu/src/monitor.startup.ts index a2d284c879e..42f3639c1de 100644 --- a/extensions/feishu/src/monitor.startup.ts +++ b/extensions/feishu/src/monitor.startup.ts @@ -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 { +): Promise { 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 { + const identity = await fetchBotIdentityForMonitor(account, options); + return identity.botOpenId; } diff --git a/extensions/feishu/src/monitor.state.ts b/extensions/feishu/src/monitor.state.ts index 6326dcf9444..30cada26821 100644 --- a/extensions/feishu/src/monitor.state.ts +++ b/extensions/feishu/src/monitor.state.ts @@ -11,6 +11,7 @@ import { export const wsClients = new Map(); export const httpServers = new Map(); export const botOpenIds = new Map(); +export const botNames = new Map(); 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(); } diff --git a/extensions/feishu/src/monitor.transport.ts b/extensions/feishu/src/monitor.transport.ts index e067e0e9f99..49a9130bb61 100644 --- a/extensions/feishu/src/monitor.transport.ts +++ b/extensions/feishu/src/monitor.transport.ts @@ -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 = () => { diff --git a/extensions/feishu/src/monitor.ts b/extensions/feishu/src/monitor.ts index 8617a928ac7..50241d36baa 100644 --- a/extensions/feishu/src/monitor.ts +++ b/extensions/feishu/src/monitor.ts @@ -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 }, }), ); }