From 6a8d83b6ddaa38a6d91d0e170c69765100f6309b Mon Sep 17 00:00:00 2001 From: neverland <10937319+Bermudarat@users.noreply.github.com> Date: Sat, 28 Feb 2026 12:16:20 +0800 Subject: [PATCH] fix(feishu): Remove incorrect oc_ prefix assumption in resolveFeishuSession (#10407) * fix(feishu): remove incorrect oc_ prefix assumption in resolveFeishuSession - Feishu oc_ is a generic chat_id that can represent both groups and DMs - Must use chat_mode field from API to distinguish, not ID prefix - Only ou_/on_ prefixes reliably indicate user IDs (always DM) - Fixes session misrouting for DMs with oc_ chat IDs This bug caused DM messages with oc_ chat_ids to be incorrectly created as group sessions, breaking session isolation and routing. * docs: update Feishu ID format comment to reflect oc_ ambiguity The previous comment incorrectly stated oc_ is always a group chat. This update clarifies that oc_ chat_ids can be either groups or DMs, and explicit prefixes (dm:/group:) should be used to distinguish. * feishu: add regression coverage for oc session routing --------- Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> --- CHANGELOG.md | 1 + src/infra/outbound/outbound-session.ts | 17 ++++++++---- src/infra/outbound/outbound.test.ts | 36 ++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb641908511..1c1e122f527 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ Docs: https://docs.openclaw.ai - 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) - Feishu/Reaction notifications: add `channels.feishu.reactionNotifications` (`off | own | all`, default `own`) so operators can disable reaction ingress or allow all verified reaction events (not only bot-authored message reactions). (#28529) +- 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. - Feishu/Group session routing: add configurable group session scopes (`group`, `group_sender`, `group_topic`, `group_topic_sender`) with legacy `topicSessionMode=enabled` compatibility so Feishu group conversations can isolate sessions by sender/topic as configured. (#17798) - Feishu/Reply-in-thread routing: add `replyInThread` config (`disabled|enabled`) for group replies, propagate `reply_in_thread` across text/card/media/streaming sends, and align topic-scoped session routing so newly created reply threads stay on the same session root. (#27325) - Feishu/Typing backoff: re-throw Feishu typing add/remove rate-limit and quota errors (`429`, `99991400`, `99991403`) and detect SDK non-throwing backoff responses so the typing keepalive circuit breaker can stop retries instead of looping indefinitely. (#28494) diff --git a/src/infra/outbound/outbound-session.ts b/src/infra/outbound/outbound-session.ts index 7a764fa53c3..fa2727f9c4f 100644 --- a/src/infra/outbound/outbound-session.ts +++ b/src/infra/outbound/outbound-session.ts @@ -786,7 +786,7 @@ function resolveTlonSession( /** * Feishu ID formats: - * - oc_xxx: chat_id (group chat) + * - oc_xxx: chat_id (can be group or DM, use chat_mode to distinguish or explicit dm:/group: prefix) * - ou_xxx: user open_id (DM) * - on_xxx: user union_id (DM) * - cli_xxx: app_id (not a valid send target) @@ -802,20 +802,27 @@ function resolveFeishuSession( const lower = trimmed.toLowerCase(); let isGroup = false; + let typeExplicit = false; if (lower.startsWith("group:") || lower.startsWith("chat:")) { trimmed = trimmed.replace(/^(group|chat):/i, "").trim(); isGroup = true; + typeExplicit = true; } else if (lower.startsWith("user:") || lower.startsWith("dm:")) { trimmed = trimmed.replace(/^(user|dm):/i, "").trim(); isGroup = false; + typeExplicit = true; } const idLower = trimmed.toLowerCase(); - if (idLower.startsWith("oc_")) { - isGroup = true; - } else if (idLower.startsWith("ou_") || idLower.startsWith("on_")) { - isGroup = false; + // Only infer type from ID prefix if not explicitly specified + // Note: oc_ is a chat_id and can be either group or DM (must check chat_mode from API) + // Only ou_/on_ can be reliably identified as user IDs (always DM) + if (!typeExplicit) { + if (idLower.startsWith("ou_") || idLower.startsWith("on_")) { + isGroup = false; + } + // oc_ requires explicit prefix: dm:oc_xxx or group:oc_xxx } const peer: RoutePeer = { diff --git a/src/infra/outbound/outbound.test.ts b/src/infra/outbound/outbound.test.ts index f15f3de3730..01cdaf3e7c9 100644 --- a/src/infra/outbound/outbound.test.ts +++ b/src/infra/outbound/outbound.test.ts @@ -973,6 +973,42 @@ describe("resolveOutboundSessionRoute", () => { from: "slack:group:G123", }, }, + { + name: "Feishu explicit group prefix keeps group routing", + cfg: baseConfig, + channel: "feishu", + target: "group:oc_group_chat", + expected: { + sessionKey: "agent:main:feishu:group:oc_group_chat", + from: "feishu:group:oc_group_chat", + to: "oc_group_chat", + chatType: "group", + }, + }, + { + name: "Feishu explicit dm prefix keeps direct routing", + cfg: perChannelPeerCfg, + channel: "feishu", + target: "dm:oc_dm_chat", + expected: { + sessionKey: "agent:main:feishu:direct:oc_dm_chat", + from: "feishu:oc_dm_chat", + to: "oc_dm_chat", + chatType: "direct", + }, + }, + { + name: "Feishu bare oc_ target defaults to direct routing", + cfg: perChannelPeerCfg, + channel: "feishu", + target: "oc_ambiguous_chat", + expected: { + sessionKey: "agent:main:feishu:direct:oc_ambiguous_chat", + from: "feishu:oc_ambiguous_chat", + to: "oc_ambiguous_chat", + chatType: "direct", + }, + }, ]; for (const testCase of cases) {