fix(feishu): honor sender-scoped ACP bindings

This commit is contained in:
Tak Hoffman 2026-03-15 09:21:03 -05:00
parent 76782f4409
commit 4f9c85c02d
4 changed files with 103 additions and 16 deletions

View File

@ -123,14 +123,23 @@ function resolveConfiguredBindingRecord(params: {
bindings: AgentAcpBinding[]; bindings: AgentAcpBinding[];
channel: ConfiguredAcpBindingChannel; channel: ConfiguredAcpBindingChannel;
accountId: string; accountId: string;
selectConversation: ( selectConversation: (binding: AgentAcpBinding) => {
binding: AgentAcpBinding, conversationId: string;
) => { conversationId: string; parentConversationId?: string } | null; parentConversationId?: string;
matchPriority?: number;
} | null;
}): ResolvedConfiguredAcpBinding | null { }): ResolvedConfiguredAcpBinding | null {
let wildcardMatch: { let wildcardMatch: {
binding: AgentAcpBinding; binding: AgentAcpBinding;
conversationId: string; conversationId: string;
parentConversationId?: string; parentConversationId?: string;
matchPriority: number;
} | null = null;
let exactMatch: {
binding: AgentAcpBinding;
conversationId: string;
parentConversationId?: string;
matchPriority: number;
} | null = null; } | null = null;
for (const binding of params.bindings) { for (const binding of params.bindings) {
if (normalizeBindingChannel(binding.match.channel) !== params.channel) { if (normalizeBindingChannel(binding.match.channel) !== params.channel) {
@ -147,23 +156,40 @@ function resolveConfiguredBindingRecord(params: {
if (!conversation) { if (!conversation) {
continue; 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({ const spec = toConfiguredBindingSpec({
cfg: params.cfg, cfg: params.cfg,
channel: params.channel, channel: params.channel,
accountId: params.accountId, accountId: params.accountId,
conversationId: conversation.conversationId, conversationId: exactMatch.conversationId,
parentConversationId: conversation.parentConversationId, parentConversationId: exactMatch.parentConversationId,
binding, binding: exactMatch.binding,
}); });
if (accountMatchPriority === 2) { return {
return { spec,
spec, record: toConfiguredAcpBindingRecord(spec),
record: toConfiguredAcpBindingRecord(spec), };
};
}
if (!wildcardMatch) {
wildcardMatch = { binding, ...conversation };
}
} }
if (!wildcardMatch) { if (!wildcardMatch) {
return null; return null;
@ -426,6 +452,7 @@ export function resolveConfiguredAcpBindingRecord(params: {
parsed.scope === "group_topic" || parsed.scope === "group_topic_sender" parsed.scope === "group_topic" || parsed.scope === "group_topic_sender"
? parsed.chatId ? parsed.chatId
: undefined, : undefined,
matchPriority: matchesCanonicalConversation ? 2 : 1,
}; };
}, },
}); });

View File

@ -226,6 +226,34 @@ describe("resolveConfiguredAcpBindingRecord", () => {
expect(resolved?.spec.agentId).toBe("claude"); 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", () => { it("prefers exact account binding over wildcard for the same discord conversation", () => {
const cfg = createCfgWithBindings([ const cfg = createCfgWithBindings([
createDiscordBinding({ createDiscordBinding({

View File

@ -266,6 +266,24 @@ describe("commands-acp context", () => {
expect(resolveAcpCommandConversationId(params)).toBe("ou_sender_1"); 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", () => { it("does not infer a Feishu DM parent conversation id during fallback binding lookup", () => {
const params = buildCommandTestParams("/acp status", baseCfg, { const params = buildCommandTestParams("/acp status", baseCfg, {
Provider: "feishu", Provider: "feishu",

View File

@ -31,7 +31,21 @@ function parseFeishuTargetId(raw: unknown): string | undefined {
} }
function parseFeishuDirectConversationId(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) { if (!id) {
return undefined; return undefined;
} }