fix(feishu): preserve disabled group policy for explicit groups

This commit is contained in:
Peter Steinberger
2026-04-27 21:55:27 +01:00
parent b3bc60ae25
commit 18ef83c0da
6 changed files with 196 additions and 50 deletions

View File

@@ -1425,7 +1425,7 @@ describe("handleFeishuMessage command authorization", () => {
const cfg: ClawdbotConfig = {
channels: {
feishu: {
// groupPolicy intentionally omitted -> schema default is "allowlist"
groupPolicy: "allowlist",
// groupAllowFrom intentionally omitted -> empty []
groups: {
"oc-explicit-group": {
@@ -1456,6 +1456,72 @@ describe("handleFeishuMessage command authorization", () => {
expect(mockDispatchReplyFromConfig).toHaveBeenCalled();
});
it("does not let explicit group config override disabled group policy", async () => {
const cfg: ClawdbotConfig = {
channels: {
feishu: {
groupPolicy: "disabled",
groups: {
"oc-disabled-policy-group": {
requireMention: false,
},
},
},
},
} as ClawdbotConfig;
const event: FeishuMessageEvent = {
sender: {
sender_id: { open_id: "ou-sender" },
},
message: {
message_id: "msg-disabled-policy-group",
chat_id: "oc-disabled-policy-group",
chat_type: "group",
message_type: "text",
content: JSON.stringify({ text: "hello bot" }),
},
};
await dispatchMessage({ cfg, event });
expect(mockFinalizeInboundContext).not.toHaveBeenCalled();
expect(mockDispatchReplyFromConfig).not.toHaveBeenCalled();
});
it("does not treat wildcard group defaults as allowlist admission", async () => {
const cfg: ClawdbotConfig = {
channels: {
feishu: {
groupPolicy: "allowlist",
groups: {
"*": {
requireMention: false,
},
},
},
},
} as ClawdbotConfig;
const event: FeishuMessageEvent = {
sender: {
sender_id: { open_id: "ou-sender" },
},
message: {
message_id: "msg-wildcard-group-default",
chat_id: "oc-wildcard-only",
chat_type: "group",
message_type: "text",
content: JSON.stringify({ text: "hello bot" }),
},
};
await dispatchMessage({ cfg, event });
expect(mockFinalizeInboundContext).not.toHaveBeenCalled();
expect(mockDispatchReplyFromConfig).not.toHaveBeenCalled();
});
it("drops message when groupConfig.enabled is false", async () => {
const cfg: ClawdbotConfig = {
channels: {

View File

@@ -44,10 +44,11 @@ import { finalizeFeishuMessageProcessing, tryRecordMessagePersistent } from "./d
import { maybeCreateDynamicAgent } from "./dynamic-agent.js";
import { extractMentionTargets, isMentionForwardRequest } from "./mention.js";
import {
hasExplicitFeishuGroupConfig,
isFeishuGroupAllowed,
resolveFeishuAllowlistMatch,
resolveFeishuGroupConfig,
resolveFeishuReplyPolicy,
resolveFeishuAllowlistMatch,
isFeishuGroupAllowed,
} from "./policy.js";
import { resolveFeishuReasoningPreviewEnabled } from "./reasoning-preview.js";
import { createFeishuReplyDispatcher } from "./reply-dispatcher.js";
@@ -554,23 +555,25 @@ export async function handleFeishuMessage(params: {
const groupAllowFrom = feishuCfg?.groupAllowFrom ?? [];
// DEBUG: log(`feishu[${account.accountId}]: groupPolicy=${groupPolicy}`);
// A group that is explicitly configured under `channels.feishu.groups.<chat_id>`
// is treated as admitted regardless of `groupAllowFrom`. The reporter case in
// #67687 only sets `groups.<chat_id>.requireMention=false` and leaves
// `groupAllowFrom` empty; with the schema-default `groupPolicy="allowlist"`,
// an empty allowlist would otherwise reject the group before any per-group
// `requireMention` override is evaluated.
const groupExplicitlyConfigured = groupConfig !== undefined;
// A group explicitly configured under `channels.feishu.groups.<chat_id>` is
// treated as admitted in allowlist mode even when `groupAllowFrom` is empty.
// Wildcard defaults still configure matching groups, but they are not an
// admission signal by themselves.
const groupExplicitlyConfigured = hasExplicitFeishuGroupConfig({
cfg: feishuCfg,
groupId: ctx.chatId,
});
// Check if this GROUP is allowed (groupAllowFrom contains group IDs like oc_xxx, not user IDs)
const groupAllowed =
groupExplicitlyConfigured ||
isFeishuGroupAllowed({
groupPolicy,
allowFrom: groupAllowFrom,
senderId: ctx.chatId, // Check group ID, not sender ID
senderName: undefined,
});
groupPolicy !== "disabled" &&
(groupExplicitlyConfigured ||
isFeishuGroupAllowed({
groupPolicy,
allowFrom: groupAllowFrom,
senderId: ctx.chatId, // Check group ID, not sender ID
senderName: undefined,
}));
if (!groupAllowed) {
log(

View File

@@ -2,6 +2,7 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
import { describe, expect, it } from "vitest";
import { FeishuConfigSchema } from "./config-schema.js";
import {
hasExplicitFeishuGroupConfig,
isFeishuGroupAllowed,
resolveFeishuAllowlistMatch,
resolveFeishuGroupConfig,
@@ -141,6 +142,29 @@ describe("resolveFeishuGroupConfig", () => {
});
});
describe("hasExplicitFeishuGroupConfig", () => {
it("matches direct and case-insensitive group ids", () => {
const cfg = createFeishuConfig({
groups: {
OC_UPPER: { requireMention: true },
},
});
expect(hasExplicitFeishuGroupConfig({ cfg, groupId: "OC_UPPER" })).toBe(true);
expect(hasExplicitFeishuGroupConfig({ cfg, groupId: "oc_upper" })).toBe(true);
});
it("does not treat wildcard group defaults as explicit admission", () => {
const cfg = createFeishuConfig({
groups: {
"*": { requireMention: false },
},
});
expect(hasExplicitFeishuGroupConfig({ cfg, groupId: "oc_any" })).toBe(false);
});
});
describe("resolveFeishuAllowlistMatch", () => {
it("allows wildcard", () => {
expect(

View File

@@ -148,6 +148,25 @@ export function resolveFeishuGroupConfig(params: { cfg?: FeishuConfig; groupId?:
return wildcard;
}
export function hasExplicitFeishuGroupConfig(params: {
cfg?: FeishuConfig;
groupId?: string | null;
}): boolean {
const groups = params.cfg?.groups ?? {};
const groupId = params.groupId?.trim();
if (!groupId) {
return false;
}
if (Object.prototype.hasOwnProperty.call(groups, groupId) && groupId !== "*") {
return true;
}
const lowered = normalizeOptionalLowercaseString(groupId) ?? "";
return Object.keys(groups).some(
(key) => key !== "*" && normalizeOptionalLowercaseString(key) === lowered,
);
}
export function resolveFeishuGroupToolPolicy(params: ChannelGroupContext) {
const cfg = params.cfg.channels?.feishu;
if (!cfg) {