diff --git a/src/acp/persistent-bindings.resolve.ts b/src/acp/persistent-bindings.resolve.ts index 2429945e6db..223fc3e099f 100644 --- a/src/acp/persistent-bindings.resolve.ts +++ b/src/acp/persistent-bindings.resolve.ts @@ -123,14 +123,23 @@ function resolveConfiguredBindingRecord(params: { bindings: AgentAcpBinding[]; channel: ConfiguredAcpBindingChannel; accountId: string; - selectConversation: ( - binding: AgentAcpBinding, - ) => { conversationId: string; parentConversationId?: string } | null; + selectConversation: (binding: AgentAcpBinding) => { + conversationId: string; + parentConversationId?: string; + matchPriority?: number; + } | null; }): ResolvedConfiguredAcpBinding | null { let wildcardMatch: { binding: AgentAcpBinding; conversationId: string; parentConversationId?: string; + matchPriority: number; + } | null = null; + let exactMatch: { + binding: AgentAcpBinding; + conversationId: string; + parentConversationId?: string; + matchPriority: number; } | null = null; for (const binding of params.bindings) { if (normalizeBindingChannel(binding.match.channel) !== params.channel) { @@ -147,23 +156,40 @@ function resolveConfiguredBindingRecord(params: { if (!conversation) { continue; } + const matchPriority = conversation.matchPriority ?? 0; + if (accountMatchPriority === 2) { + if (!exactMatch || matchPriority > exactMatch.matchPriority) { + exactMatch = { + binding, + conversationId: conversation.conversationId, + parentConversationId: conversation.parentConversationId, + matchPriority, + }; + } + continue; + } + if (!wildcardMatch || matchPriority > wildcardMatch.matchPriority) { + wildcardMatch = { + binding, + conversationId: conversation.conversationId, + parentConversationId: conversation.parentConversationId, + matchPriority, + }; + } + } + if (exactMatch) { const spec = toConfiguredBindingSpec({ cfg: params.cfg, channel: params.channel, accountId: params.accountId, - conversationId: conversation.conversationId, - parentConversationId: conversation.parentConversationId, - binding, + conversationId: exactMatch.conversationId, + parentConversationId: exactMatch.parentConversationId, + binding: exactMatch.binding, }); - if (accountMatchPriority === 2) { - return { - spec, - record: toConfiguredAcpBindingRecord(spec), - }; - } - if (!wildcardMatch) { - wildcardMatch = { binding, ...conversation }; - } + return { + spec, + record: toConfiguredAcpBindingRecord(spec), + }; } if (!wildcardMatch) { return null; @@ -426,6 +452,7 @@ export function resolveConfiguredAcpBindingRecord(params: { parsed.scope === "group_topic" || parsed.scope === "group_topic_sender" ? parsed.chatId : undefined, + matchPriority: matchesCanonicalConversation ? 2 : 1, }; }, }); diff --git a/src/acp/persistent-bindings.test.ts b/src/acp/persistent-bindings.test.ts index 353ab801991..f5654fed9c9 100644 --- a/src/acp/persistent-bindings.test.ts +++ b/src/acp/persistent-bindings.test.ts @@ -226,6 +226,34 @@ describe("resolveConfiguredAcpBindingRecord", () => { expect(resolved?.spec.agentId).toBe("claude"); }); + it("prefers sender-scoped Feishu bindings over topic inheritance", () => { + const cfg = createCfgWithBindings([ + createFeishuBinding({ + agentId: "codex", + conversationId: "oc_group_chat:topic:om_topic_root", + accountId: "work", + }), + createFeishuBinding({ + agentId: "claude", + conversationId: "oc_group_chat:topic:om_topic_root:sender:ou_sender_1", + accountId: "work", + }), + ]); + + const resolved = resolveConfiguredAcpBindingRecord({ + cfg, + channel: "feishu", + accountId: "work", + conversationId: "oc_group_chat:topic:om_topic_root:sender:ou_sender_1", + parentConversationId: "oc_group_chat", + }); + + expect(resolved?.spec.conversationId).toBe( + "oc_group_chat:topic:om_topic_root:sender:ou_sender_1", + ); + expect(resolved?.spec.agentId).toBe("claude"); + }); + it("prefers exact account binding over wildcard for the same discord conversation", () => { const cfg = createCfgWithBindings([ createDiscordBinding({ diff --git a/src/auto-reply/reply/commands-acp/context.test.ts b/src/auto-reply/reply/commands-acp/context.test.ts index 35f9e0767d2..5b1e60ad1fc 100644 --- a/src/auto-reply/reply/commands-acp/context.test.ts +++ b/src/auto-reply/reply/commands-acp/context.test.ts @@ -266,6 +266,24 @@ describe("commands-acp context", () => { expect(resolveAcpCommandConversationId(params)).toBe("ou_sender_1"); }); + it("resolves Feishu DM conversation ids from user_id fallback targets", () => { + const params = buildCommandTestParams("/acp status", baseCfg, { + Provider: "feishu", + Surface: "feishu", + OriginatingChannel: "feishu", + OriginatingTo: "user:user_123", + }); + + expect(resolveAcpCommandBindingContext(params)).toEqual({ + channel: "feishu", + accountId: "default", + threadId: undefined, + conversationId: "user_123", + parentConversationId: undefined, + }); + expect(resolveAcpCommandConversationId(params)).toBe("user_123"); + }); + it("does not infer a Feishu DM parent conversation id during fallback binding lookup", () => { const params = buildCommandTestParams("/acp status", baseCfg, { Provider: "feishu", diff --git a/src/auto-reply/reply/commands-acp/context.ts b/src/auto-reply/reply/commands-acp/context.ts index 2153a7358a9..fd5eb50ee09 100644 --- a/src/auto-reply/reply/commands-acp/context.ts +++ b/src/auto-reply/reply/commands-acp/context.ts @@ -31,7 +31,21 @@ function parseFeishuTargetId(raw: unknown): string | undefined { } function parseFeishuDirectConversationId(raw: unknown): string | undefined { - const id = parseFeishuTargetId(raw); + const target = normalizeConversationText(raw); + if (!target) { + return undefined; + } + const withoutProvider = target.replace(/^(feishu|lark):/i, "").trim(); + if (!withoutProvider) { + return undefined; + } + const lowered = withoutProvider.toLowerCase(); + for (const prefix of ["user:", "dm:", "open_id:"]) { + if (lowered.startsWith(prefix)) { + return normalizeConversationText(withoutProvider.slice(prefix.length)); + } + } + const id = parseFeishuTargetId(target); if (!id) { return undefined; }