fix(feishu): preserve sender-scoped ACP rebinding

This commit is contained in:
Tak Hoffman
2026-03-14 22:49:21 -05:00
parent e2b7fa9493
commit 574ea852b3
5 changed files with 36 additions and 5 deletions

View File

@@ -1262,6 +1262,9 @@ export async function handleFeishuMessage(params: {
configuredBinding = configuredRoute.configuredBinding;
route = configuredRoute.route;
// Bound Feishu conversations intentionally require an exact live conversation-id match.
// Sender-scoped topic sessions therefore bind on `chat:topic:root:sender:user`, while
// configured ACP bindings may still inherit the shared `chat:topic:root` topic session.
const threadBinding = getSessionBindingService().resolveByConversation({
channel: "feishu",
accountId: account.accountId,

View File

@@ -247,6 +247,9 @@ export function resolveConfiguredAcpBindingSpecBySessionKey(params: {
channel: "feishu",
accountId: parsedSessionKey.accountId,
conversationId: targetParsed.canonicalConversationId,
// Session-key recovery deliberately collapses sender-scoped topic bindings onto the
// canonical topic conversation id so `group_topic` and `group_topic_sender` reuse
// the same configured ACP session identity.
parentConversationId:
targetParsed.scope === "group_topic" || targetParsed.scope === "group_topic_sender"
? targetParsed.chatId

View File

@@ -174,6 +174,29 @@ describe("commands-acp context", () => {
);
});
it("preserves sender-scoped Feishu topic ids after ACP route takeover via ParentSessionKey", () => {
const params = buildCommandTestParams("/acp status", baseCfg, {
Provider: "feishu",
Surface: "feishu",
OriginatingChannel: "feishu",
OriginatingTo: "chat:oc_group_chat",
MessageThreadId: "om_topic_root",
SenderId: "ou_topic_user",
AccountId: "work",
ParentSessionKey:
"agent:main:feishu:group:oc_group_chat:topic:om_topic_root:sender:ou_topic_user",
});
params.sessionKey = "agent:codex:acp:binding:feishu:work:abc123";
expect(resolveAcpCommandBindingContext(params)).toEqual({
channel: "feishu",
accountId: "work",
threadId: "om_topic_root",
conversationId: "oc_group_chat:topic:om_topic_root:sender:ou_topic_user",
parentConversationId: "oc_group_chat",
});
});
it("resolves Feishu DM conversation ids from user targets", () => {
const params = buildCommandTestParams("/acp status", baseCfg, {
Provider: "feishu",

View File

@@ -45,15 +45,16 @@ function resolveFeishuSenderScopedConversationId(params: {
threadId?: string;
senderId?: string;
sessionKey?: string;
parentSessionKey?: string;
}): string | undefined {
const parentConversationId = normalizeConversationText(params.parentConversationId);
const threadId = normalizeConversationText(params.threadId);
const senderId = normalizeConversationText(params.senderId);
const scopedRest = parseAgentSessionKey(params.sessionKey)?.rest?.trim().toLowerCase() ?? "";
const expectedScopePrefix = `feishu:group:${parentConversationId?.toLowerCase()}:topic:${threadId?.toLowerCase()}:sender:`;
const isSenderScopedSession = Boolean(
scopedRest && expectedScopePrefix && scopedRest.startsWith(expectedScopePrefix),
);
const isSenderScopedSession = [params.sessionKey, params.parentSessionKey].some((candidate) => {
const scopedRest = parseAgentSessionKey(candidate)?.rest?.trim().toLowerCase() ?? "";
return Boolean(scopedRest && expectedScopePrefix && scopedRest.startsWith(expectedScopePrefix));
});
if (!parentConversationId || !threadId || !senderId || !isSenderScopedSession) {
return undefined;
}
@@ -123,6 +124,7 @@ export function resolveAcpCommandConversationId(params: HandleCommandsParams): s
threadId,
senderId: params.command.senderId ?? params.ctx.SenderId,
sessionKey: params.sessionKey,
parentSessionKey: params.ctx.ParentSessionKey,
});
return (
senderScopedConversationId ??

View File

@@ -90,7 +90,7 @@ const AcpBindingSchema = z
}
if (
channel === "feishu" &&
!/^(ou_[^:]+|on_[^:]+|[^:]+:topic:[^:]+(?::sender:[^:]+)?)$/.test(peerId)
!/^(ou_[^:]+|on_[^:]+|oc_[^:]+:topic:[^:]+(?::sender:[^:]+)?)$/.test(peerId)
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,