mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-03 09:20:22 +00:00
refactor: align pairing replies, daemon hints, and feishu mention policy
This commit is contained in:
@@ -414,8 +414,9 @@ export async function handleFeishuMessage(params: {
|
||||
|
||||
({ requireMention } = resolveFeishuReplyPolicy({
|
||||
isDirectMessage: false,
|
||||
globalConfig: feishuCfg,
|
||||
groupConfig,
|
||||
cfg,
|
||||
accountId: account.accountId,
|
||||
groupId: ctx.chatId,
|
||||
groupPolicy,
|
||||
}));
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user