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[];
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,
};
},
});

View File

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

View File

@ -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",

View File

@ -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;
}