From 05b84e718ba6857a24a9d3fd64b9beb232d45cbd Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 2 Mar 2026 13:40:55 +0000 Subject: [PATCH] fix(feishu): preserve explicit target routing hints (#31594) (thanks @liuxiaopai-ai) --- CHANGELOG.md | 1 + extensions/feishu/src/send-target.test.ts | 74 +++++++++++++++++++++++ extensions/feishu/src/send-target.ts | 8 ++- extensions/feishu/src/targets.test.ts | 4 ++ 4 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 extensions/feishu/src/send-target.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index d4f4d6a10..9acdb93bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -85,6 +85,7 @@ Docs: https://docs.openclaw.ai - Cron/Delivery: disable the agent messaging tool when `delivery.mode` is `"none"` so cron output is not sent to Telegram or other channels. (#21808) Thanks @lailoo. - CLI/Cron: clarify `cron list` output by renaming `Agent` to `Agent ID` and adding a `Model` column for isolated agent-turn jobs. (#26259) Thanks @openperf. - Feishu/Reply media attachments: send Feishu reply `mediaUrl`/`mediaUrls` payloads as attachments alongside text/streamed replies in the reply dispatcher, including legacy fallback when `mediaUrls` is empty. (#28959) Thanks @icesword0760. +- Feishu/Send target prefixes: normalize explicit `group:`/`dm:` send targets and preserve explicit receive-id routing hints when resolving outbound Feishu targets. (#31594) Thanks @liuxiaopai-ai. - Slack/User-token resolution: normalize Slack account user-token sourcing through resolved account metadata (`SLACK_USER_TOKEN` env + config) so monitor reads, Slack actions, directory lookups, onboarding allow-from resolution, and capabilities probing consistently use the effective user token. (#28103) Thanks @Glucksberg. - Slack/Channel message subscriptions: register explicit `message.channels` and `message.groups` monitor handlers (alongside generic `message`) so channel/group event subscriptions are consumed even when Slack dispatches typed message event names. Fixes #31674. - Feishu/Outbound session routing: stop assuming bare `oc_` identifiers are always group chats, honor explicit `dm:`/`group:` prefixes for `oc_` chat IDs, and default ambiguous bare `oc_` targets to direct routing to avoid DM session misclassification. (#10407) Thanks @Bermudarat. diff --git a/extensions/feishu/src/send-target.test.ts b/extensions/feishu/src/send-target.test.ts new file mode 100644 index 000000000..617c2aa05 --- /dev/null +++ b/extensions/feishu/src/send-target.test.ts @@ -0,0 +1,74 @@ +import type { ClawdbotConfig } from "openclaw/plugin-sdk"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { resolveFeishuSendTarget } from "./send-target.js"; + +const resolveFeishuAccountMock = vi.hoisted(() => vi.fn()); +const createFeishuClientMock = vi.hoisted(() => vi.fn()); + +vi.mock("./accounts.js", () => ({ + resolveFeishuAccount: resolveFeishuAccountMock, +})); + +vi.mock("./client.js", () => ({ + createFeishuClient: createFeishuClientMock, +})); + +describe("resolveFeishuSendTarget", () => { + const cfg = {} as ClawdbotConfig; + const client = { id: "client" }; + + beforeEach(() => { + resolveFeishuAccountMock.mockReset().mockReturnValue({ + accountId: "default", + enabled: true, + configured: true, + }); + createFeishuClientMock.mockReset().mockReturnValue(client); + }); + + it("keeps explicit group targets as chat_id even when ID shape is ambiguous", () => { + const result = resolveFeishuSendTarget({ + cfg, + to: "feishu:group:group_room_alpha", + }); + + expect(result.receiveId).toBe("group_room_alpha"); + expect(result.receiveIdType).toBe("chat_id"); + expect(result.client).toBe(client); + }); + + it("maps dm-prefixed open IDs to open_id", () => { + const result = resolveFeishuSendTarget({ + cfg, + to: "lark:dm:ou_123", + }); + + expect(result.receiveId).toBe("ou_123"); + expect(result.receiveIdType).toBe("open_id"); + }); + + it("maps dm-prefixed non-open IDs to user_id", () => { + const result = resolveFeishuSendTarget({ + cfg, + to: " feishu:dm:user_123 ", + }); + + expect(result.receiveId).toBe("user_123"); + expect(result.receiveIdType).toBe("user_id"); + }); + + it("throws when target account is not configured", () => { + resolveFeishuAccountMock.mockReturnValue({ + accountId: "default", + enabled: true, + configured: false, + }); + + expect(() => + resolveFeishuSendTarget({ + cfg, + to: "feishu:group:oc_123", + }), + ).toThrow('Feishu account "default" not configured'); + }); +}); diff --git a/extensions/feishu/src/send-target.ts b/extensions/feishu/src/send-target.ts index 7d0d28663..caf02f9cf 100644 --- a/extensions/feishu/src/send-target.ts +++ b/extensions/feishu/src/send-target.ts @@ -8,18 +8,22 @@ export function resolveFeishuSendTarget(params: { to: string; accountId?: string; }) { + const target = params.to.trim(); const account = resolveFeishuAccount({ cfg: params.cfg, accountId: params.accountId }); if (!account.configured) { throw new Error(`Feishu account "${account.accountId}" not configured`); } const client = createFeishuClient(account); - const receiveId = normalizeFeishuTarget(params.to); + const receiveId = normalizeFeishuTarget(target); if (!receiveId) { throw new Error(`Invalid Feishu target: ${params.to}`); } + // Preserve explicit routing prefixes (chat/group/user/dm/open_id) when present. + // normalizeFeishuTarget strips these prefixes, so infer type from the raw target first. + const withoutProviderPrefix = target.replace(/^(feishu|lark):/i, ""); return { client, receiveId, - receiveIdType: resolveReceiveIdType(receiveId), + receiveIdType: resolveReceiveIdType(withoutProviderPrefix), }; } diff --git a/extensions/feishu/src/targets.test.ts b/extensions/feishu/src/targets.test.ts index e5a6f3063..657738f59 100644 --- a/extensions/feishu/src/targets.test.ts +++ b/extensions/feishu/src/targets.test.ts @@ -17,6 +17,10 @@ describe("resolveReceiveIdType", () => { it("treats explicit group targets as chat_id", () => { expect(resolveReceiveIdType("group:oc_123")).toBe("chat_id"); }); + + it("treats dm-prefixed open IDs as open_id", () => { + expect(resolveReceiveIdType("dm:ou_123")).toBe("open_id"); + }); }); describe("normalizeFeishuTarget", () => {