refactor: align pairing replies, daemon hints, and feishu mention policy

This commit is contained in:
Peter Steinberger
2026-03-25 04:21:51 -07:00
parent 524004ff32
commit b7f2b0d7b9
15 changed files with 436 additions and 210 deletions

View File

@@ -414,8 +414,9 @@ export async function handleFeishuMessage(params: {
({ requireMention } = resolveFeishuReplyPolicy({
isDirectMessage: false,
globalConfig: feishuCfg,
groupConfig,
cfg,
accountId: account.accountId,
groupId: ctx.chatId,
groupPolicy,
}));

View File

@@ -20,8 +20,8 @@ describe("FeishuConfigSchema webhook validation", () => {
expect(result.dmPolicy).toBe("pairing");
expect(result.groupPolicy).toBe("allowlist");
// requireMention has no schema-level default now — it is resolved at runtime
// by resolveFeishuReplyPolicy(), which defaults to false for groupPolicy=open
// and true otherwise.
// through shared channel group-policy resolution, with an open-group override
// that defaults to false only when requireMention is otherwise unset.
expect(result.requireMention).toBeUndefined();
});

View File

@@ -1,154 +1,232 @@
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
import { describe, expect, it } from "vitest";
import {
isFeishuGroupAllowed,
resolveFeishuAllowlistMatch,
resolveFeishuGroupConfig,
resolveFeishuReplyPolicy,
} from "./policy.js";
import type { FeishuConfig } from "./types.js";
describe("feishu policy", () => {
describe("resolveFeishuGroupConfig", () => {
it("falls back to wildcard group config when direct match is missing", () => {
const cfg = {
groups: {
"*": { requireMention: false },
"oc-explicit": { requireMention: true },
},
} as unknown as FeishuConfig;
function createCfg(feishu: Record<string, unknown>): OpenClawConfig {
return {
channels: {
feishu,
},
} as OpenClawConfig;
}
const resolved = resolveFeishuGroupConfig({
cfg,
groupId: "oc-missing",
});
expect(resolved).toEqual({ requireMention: false });
});
it("prefers exact group config over wildcard", () => {
const cfg = {
groups: {
"*": { requireMention: false },
"oc-explicit": { requireMention: true },
},
} as unknown as FeishuConfig;
const resolved = resolveFeishuGroupConfig({
cfg,
groupId: "oc-explicit",
});
expect(resolved).toEqual({ requireMention: true });
});
it("keeps case-insensitive matching for explicit group ids", () => {
const cfg = {
groups: {
"*": { requireMention: false },
OC_UPPER: { requireMention: true },
},
} as unknown as FeishuConfig;
const resolved = resolveFeishuGroupConfig({
cfg,
groupId: "oc_upper",
});
expect(resolved).toEqual({ requireMention: true });
});
describe("resolveFeishuReplyPolicy", () => {
it("defaults open groups to no mention when unset", () => {
expect(
resolveFeishuReplyPolicy({
isDirectMessage: false,
cfg: createCfg({ groupPolicy: "open" }),
groupPolicy: "open",
groupId: "oc_1",
}),
).toEqual({ requireMention: false });
});
describe("resolveFeishuAllowlistMatch", () => {
it("allows wildcard", () => {
expect(
resolveFeishuAllowlistMatch({
allowFrom: ["*"],
senderId: "ou-attacker",
}),
).toEqual({ allowed: true, matchKey: "*", matchSource: "wildcard" });
});
it("matches normalized ID entries", () => {
expect(
resolveFeishuAllowlistMatch({
allowFrom: ["feishu:user:OU_ALLOWED"],
senderId: "ou_allowed",
}),
).toEqual({ allowed: true, matchKey: "ou_allowed", matchSource: "id" });
});
it("supports user_id as an additional immutable sender candidate", () => {
expect(
resolveFeishuAllowlistMatch({
allowFrom: ["on_user_123"],
senderId: "ou_other",
senderIds: ["on_user_123"],
}),
).toEqual({ allowed: true, matchKey: "on_user_123", matchSource: "id" });
});
it("does not authorize based on display-name collision", () => {
const victimOpenId = "ou_4f4ec5aa111122223333444455556666";
expect(
resolveFeishuAllowlistMatch({
allowFrom: [victimOpenId],
senderId: "ou_attacker_real_open_id",
senderIds: ["on_attacker_user_id"],
senderName: victimOpenId,
}),
).toEqual({ allowed: false });
});
it("keeps explicit top-level mention gating in open groups", () => {
expect(
resolveFeishuReplyPolicy({
isDirectMessage: false,
cfg: createCfg({ groupPolicy: "open", requireMention: true }),
groupPolicy: "open",
groupId: "oc_1",
}),
).toEqual({ requireMention: true });
});
describe("isFeishuGroupAllowed", () => {
it("matches group IDs with chat: prefix", () => {
expect(
isFeishuGroupAllowed({
it("keeps explicit account mention gating in open groups", () => {
expect(
resolveFeishuReplyPolicy({
isDirectMessage: false,
cfg: createCfg({
groupPolicy: "allowlist",
allowFrom: ["chat:oc_group_123"],
senderId: "oc_group_123",
requireMention: false,
accounts: {
work: {
groupPolicy: "open",
requireMention: true,
},
},
}),
).toBe(true);
});
accountId: "work",
groupPolicy: "open",
groupId: "oc_1",
}),
).toEqual({ requireMention: true });
});
it("allows group when groupPolicy is 'open'", () => {
expect(
isFeishuGroupAllowed({
it("keeps explicit per-group mention gating in open groups", () => {
expect(
resolveFeishuReplyPolicy({
isDirectMessage: false,
cfg: createCfg({
groupPolicy: "open",
allowFrom: [],
senderId: "oc_group_999",
groups: { oc_1: { requireMention: true } },
}),
).toBe(true);
});
groupPolicy: "open",
groupId: "oc_1",
}),
).toEqual({ requireMention: true });
});
it("treats 'allowall' as equivalent to 'open'", () => {
expect(
isFeishuGroupAllowed({
groupPolicy: "allowall",
allowFrom: [],
senderId: "oc_group_999",
}),
).toBe(true);
});
it("rejects group when groupPolicy is 'disabled'", () => {
expect(
isFeishuGroupAllowed({
groupPolicy: "disabled",
allowFrom: ["oc_group_999"],
senderId: "oc_group_999",
}),
).toBe(false);
});
it("rejects group when groupPolicy is 'allowlist' and allowFrom is empty", () => {
expect(
isFeishuGroupAllowed({
groupPolicy: "allowlist",
allowFrom: [],
senderId: "oc_group_999",
}),
).toBe(false);
});
it("defaults allowlist groups to require mentions", () => {
expect(
resolveFeishuReplyPolicy({
isDirectMessage: false,
cfg: createCfg({ groupPolicy: "allowlist" }),
groupPolicy: "allowlist",
groupId: "oc_1",
}),
).toEqual({ requireMention: true });
});
});
describe("resolveFeishuGroupConfig", () => {
it("falls back to wildcard group config when direct match is missing", () => {
const cfg = {
groups: {
"*": { requireMention: false },
"oc-explicit": { requireMention: true },
},
} as unknown as FeishuConfig;
const resolved = resolveFeishuGroupConfig({
cfg,
groupId: "oc-missing",
});
expect(resolved).toEqual({ requireMention: false });
});
it("prefers exact group config over wildcard", () => {
const cfg = {
groups: {
"*": { requireMention: false },
"oc-explicit": { requireMention: true },
},
} as unknown as FeishuConfig;
const resolved = resolveFeishuGroupConfig({
cfg,
groupId: "oc-explicit",
});
expect(resolved).toEqual({ requireMention: true });
});
it("keeps case-insensitive matching for explicit group ids", () => {
const cfg = {
groups: {
"*": { requireMention: false },
OC_UPPER: { requireMention: true },
},
} as unknown as FeishuConfig;
const resolved = resolveFeishuGroupConfig({
cfg,
groupId: "oc_upper",
});
expect(resolved).toEqual({ requireMention: true });
});
});
describe("resolveFeishuAllowlistMatch", () => {
it("allows wildcard", () => {
expect(
resolveFeishuAllowlistMatch({
allowFrom: ["*"],
senderId: "ou-attacker",
}),
).toEqual({ allowed: true, matchKey: "*", matchSource: "wildcard" });
});
it("matches normalized ID entries", () => {
expect(
resolveFeishuAllowlistMatch({
allowFrom: ["feishu:user:OU_ALLOWED"],
senderId: "ou_allowed",
}),
).toEqual({ allowed: true, matchKey: "ou_allowed", matchSource: "id" });
});
it("supports user_id as an additional immutable sender candidate", () => {
expect(
resolveFeishuAllowlistMatch({
allowFrom: ["on_user_123"],
senderId: "ou_other",
senderIds: ["on_user_123"],
}),
).toEqual({ allowed: true, matchKey: "on_user_123", matchSource: "id" });
});
it("does not authorize based on display-name collision", () => {
const victimOpenId = "ou_4f4ec5aa111122223333444455556666";
expect(
resolveFeishuAllowlistMatch({
allowFrom: [victimOpenId],
senderId: "ou_attacker_real_open_id",
senderIds: ["on_attacker_user_id"],
senderName: victimOpenId,
}),
).toEqual({ allowed: false });
});
});
describe("isFeishuGroupAllowed", () => {
it("matches group IDs with chat: prefix", () => {
expect(
isFeishuGroupAllowed({
groupPolicy: "allowlist",
allowFrom: ["chat:oc_group_123"],
senderId: "oc_group_123",
}),
).toBe(true);
});
it("allows group when groupPolicy is 'open'", () => {
expect(
isFeishuGroupAllowed({
groupPolicy: "open",
allowFrom: [],
senderId: "oc_group_999",
}),
).toBe(true);
});
it("treats 'allowall' as equivalent to 'open'", () => {
expect(
isFeishuGroupAllowed({
groupPolicy: "allowall",
allowFrom: [],
senderId: "oc_group_999",
}),
).toBe(true);
});
it("rejects group when groupPolicy is 'disabled'", () => {
expect(
isFeishuGroupAllowed({
groupPolicy: "disabled",
allowFrom: ["oc_group_999"],
senderId: "oc_group_999",
}),
).toBe(false);
});
it("rejects group when groupPolicy is 'allowlist' and allowFrom is empty", () => {
expect(
isFeishuGroupAllowed({
groupPolicy: "allowlist",
allowFrom: [],
senderId: "oc_group_999",
}),
).toBe(false);
});
});

View File

@@ -1,3 +1,8 @@
import {
normalizeAccountId,
resolveMergedAccountConfig,
} from "openclaw/plugin-sdk/account-resolution";
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
import type { AllowlistMatch, ChannelGroupContext, GroupToolPolicyConfig } from "../runtime-api.js";
import { evaluateSenderGroupAccessForPolicy } from "../runtime-api.js";
import { normalizeFeishuTarget } from "./targets.js";
@@ -105,27 +110,41 @@ export function isFeishuGroupAllowed(params: {
export function resolveFeishuReplyPolicy(params: {
isDirectMessage: boolean;
globalConfig?: FeishuConfig;
groupConfig?: FeishuGroupConfig;
cfg: OpenClawConfig;
accountId?: string | null;
groupId?: string | null;
/**
* Effective group policy resolved for this chat. When "open", requireMention
* defaults to false so that non-text messages (e.g. images) that cannot carry
* @-mentions are still delivered to the agent.
*/
groupPolicy?: string;
groupPolicy?: "open" | "allowlist" | "disabled" | "allowall";
}): { requireMention: boolean } {
if (params.isDirectMessage) {
return { requireMention: false };
}
// When groupPolicy is "open" and requireMention is not explicitly configured,
// default to false: an open group should respond to all messages including
// images and files that cannot carry @-mentions.
const requireMentionDefault = params.groupPolicy === "open" ? false : true;
const requireMention =
params.groupConfig?.requireMention ??
params.globalConfig?.requireMention ??
requireMentionDefault;
const feishuCfg = params.cfg.channels?.feishu as FeishuConfig | undefined;
const resolvedCfg = resolveMergedAccountConfig<FeishuConfig>({
channelConfig: feishuCfg,
accounts: feishuCfg?.accounts as Record<string, Partial<FeishuConfig>> | undefined,
accountId: normalizeAccountId(params.accountId),
normalizeAccountId,
omitKeys: ["defaultAccount"],
});
const groupRequireMention = resolveFeishuGroupConfig({
cfg: resolvedCfg,
groupId: params.groupId,
})?.requireMention;
return { requireMention };
return {
requireMention:
typeof groupRequireMention === "boolean"
? groupRequireMention
: typeof resolvedCfg.requireMention === "boolean"
? resolvedCfg.requireMention
: params.groupPolicy === "open"
? false
: true,
};
}