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>
This commit is contained in:
neverland
2026-02-28 12:16:20 +08:00
committed by GitHub
parent 079bc24613
commit 6a8d83b6dd
3 changed files with 49 additions and 5 deletions

View File

@@ -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)

View File

@@ -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 = {

View File

@@ -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) {