mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:40:44 +00:00
fix(feishu): preserve disabled group policy for explicit groups
This commit is contained in:
@@ -59,11 +59,11 @@ openclaw pairing approve feishu <CODE>
|
||||
|
||||
**Group policy** (`channels.feishu.groupPolicy`):
|
||||
|
||||
| Value | Behavior |
|
||||
| ------------- | ------------------------------------------ |
|
||||
| `"open"` | Respond to all messages in groups |
|
||||
| `"allowlist"` | Only respond to groups in `groupAllowFrom` |
|
||||
| `"disabled"` | Disable all group messages |
|
||||
| Value | Behavior |
|
||||
| ------------- | -------------------------------------------------------------------------------------------- |
|
||||
| `"open"` | Respond to all messages in groups |
|
||||
| `"allowlist"` | Only respond to groups in `groupAllowFrom` or explicitly configured under `groups.<chat_id>` |
|
||||
| `"disabled"` | Disable all group messages |
|
||||
|
||||
Default: `allowlist`
|
||||
|
||||
@@ -117,6 +117,23 @@ Default: `allowlist`
|
||||
}
|
||||
```
|
||||
|
||||
You can also admit a group by adding an explicit `groups.<chat_id>` entry. Wildcard defaults under `groups.*` configure matching groups, but they do not admit groups by themselves.
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
feishu: {
|
||||
groupPolicy: "allowlist",
|
||||
groups: {
|
||||
oc_xxx: {
|
||||
requireMention: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Restrict senders within a group
|
||||
|
||||
```json5
|
||||
@@ -386,34 +403,34 @@ See [Get group/user IDs](#get-groupuser-ids) for lookup tips.
|
||||
|
||||
Full configuration: [Gateway configuration](/gateway/configuration)
|
||||
|
||||
| Setting | Description | Default |
|
||||
| ------------------------------------------------- | ------------------------------------------ | ---------------- |
|
||||
| `channels.feishu.enabled` | Enable/disable the channel | `true` |
|
||||
| `channels.feishu.domain` | API domain (`feishu` or `lark`) | `feishu` |
|
||||
| `channels.feishu.connectionMode` | Event transport (`websocket` or `webhook`) | `websocket` |
|
||||
| `channels.feishu.defaultAccount` | Default account for outbound routing | `default` |
|
||||
| `channels.feishu.verificationToken` | Required for webhook mode | — |
|
||||
| `channels.feishu.encryptKey` | Required for webhook mode | — |
|
||||
| `channels.feishu.webhookPath` | Webhook route path | `/feishu/events` |
|
||||
| `channels.feishu.webhookHost` | Webhook bind host | `127.0.0.1` |
|
||||
| `channels.feishu.webhookPort` | Webhook bind port | `3000` |
|
||||
| `channels.feishu.accounts.<id>.appId` | App ID | — |
|
||||
| `channels.feishu.accounts.<id>.appSecret` | App Secret | — |
|
||||
| `channels.feishu.accounts.<id>.domain` | Per-account domain override | `feishu` |
|
||||
| `channels.feishu.accounts.<id>.tts` | Per-account TTS override | `messages.tts` |
|
||||
| `channels.feishu.dmPolicy` | DM policy | `allowlist` |
|
||||
| `channels.feishu.allowFrom` | DM allowlist (open_id list) | [BotOwnerId] |
|
||||
| `channels.feishu.groupPolicy` | Group policy | `allowlist` |
|
||||
| `channels.feishu.groupAllowFrom` | Group allowlist | — |
|
||||
| `channels.feishu.requireMention` | Require @mention in groups | `true` |
|
||||
| `channels.feishu.groups.<chat_id>.requireMention` | Per-group @mention override | inherited |
|
||||
| `channels.feishu.groups.<chat_id>.enabled` | Enable/disable a specific group | `true` |
|
||||
| `channels.feishu.textChunkLimit` | Message chunk size | `2000` |
|
||||
| `channels.feishu.mediaMaxMb` | Media size limit | `30` |
|
||||
| `channels.feishu.streaming` | Streaming card output | `true` |
|
||||
| `channels.feishu.blockStreaming` | Block-level streaming | `true` |
|
||||
| `channels.feishu.typingIndicator` | Send typing reactions | `true` |
|
||||
| `channels.feishu.resolveSenderNames` | Resolve sender display names | `true` |
|
||||
| Setting | Description | Default |
|
||||
| ------------------------------------------------- | -------------------------------------------------------------------------------- | ---------------- |
|
||||
| `channels.feishu.enabled` | Enable/disable the channel | `true` |
|
||||
| `channels.feishu.domain` | API domain (`feishu` or `lark`) | `feishu` |
|
||||
| `channels.feishu.connectionMode` | Event transport (`websocket` or `webhook`) | `websocket` |
|
||||
| `channels.feishu.defaultAccount` | Default account for outbound routing | `default` |
|
||||
| `channels.feishu.verificationToken` | Required for webhook mode | — |
|
||||
| `channels.feishu.encryptKey` | Required for webhook mode | — |
|
||||
| `channels.feishu.webhookPath` | Webhook route path | `/feishu/events` |
|
||||
| `channels.feishu.webhookHost` | Webhook bind host | `127.0.0.1` |
|
||||
| `channels.feishu.webhookPort` | Webhook bind port | `3000` |
|
||||
| `channels.feishu.accounts.<id>.appId` | App ID | — |
|
||||
| `channels.feishu.accounts.<id>.appSecret` | App Secret | — |
|
||||
| `channels.feishu.accounts.<id>.domain` | Per-account domain override | `feishu` |
|
||||
| `channels.feishu.accounts.<id>.tts` | Per-account TTS override | `messages.tts` |
|
||||
| `channels.feishu.dmPolicy` | DM policy | `allowlist` |
|
||||
| `channels.feishu.allowFrom` | DM allowlist (open_id list) | [BotOwnerId] |
|
||||
| `channels.feishu.groupPolicy` | Group policy | `allowlist` |
|
||||
| `channels.feishu.groupAllowFrom` | Group allowlist | — |
|
||||
| `channels.feishu.requireMention` | Require @mention in groups | `true` |
|
||||
| `channels.feishu.groups.<chat_id>.requireMention` | Per-group @mention override; explicit IDs also admit the group in allowlist mode | inherited |
|
||||
| `channels.feishu.groups.<chat_id>.enabled` | Enable/disable a specific group | `true` |
|
||||
| `channels.feishu.textChunkLimit` | Message chunk size | `2000` |
|
||||
| `channels.feishu.mediaMaxMb` | Media size limit | `30` |
|
||||
| `channels.feishu.streaming` | Streaming card output | `true` |
|
||||
| `channels.feishu.blockStreaming` | Block-level streaming | `true` |
|
||||
| `channels.feishu.typingIndicator` | Send typing reactions | `true` |
|
||||
| `channels.feishu.resolveSenderNames` | Resolve sender display names | `true` |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -30,6 +30,23 @@ describe("Dockerfile", () => {
|
||||
expect(dockerfile).not.toContain("OPENCLAW_VARIANT");
|
||||
});
|
||||
|
||||
it("installs CA certificates in the slim runtime stage", async () => {
|
||||
const dockerfile = await readFile(dockerfilePath, "utf8");
|
||||
const collapsed = collapseDockerContinuations(dockerfile);
|
||||
const runtimeIndex = collapsed.indexOf(
|
||||
"FROM ${OPENCLAW_NODE_BOOKWORM_SLIM_IMAGE} AS base-runtime",
|
||||
);
|
||||
const caInstallIndex = collapsed.indexOf(
|
||||
"ca-certificates procps hostname curl git lsof openssl",
|
||||
);
|
||||
|
||||
expect(runtimeIndex).toBeGreaterThan(-1);
|
||||
expect(caInstallIndex).toBeGreaterThan(runtimeIndex);
|
||||
expect(caInstallIndex).toBeLessThan(collapsed.indexOf("RUN chown node:node /app"));
|
||||
expect(collapsed).toMatch(/apt-get install -y --no-install-recommends\s+ca-certificates/);
|
||||
expect(collapsed).toContain("update-ca-certificates");
|
||||
});
|
||||
|
||||
it("installs optional browser dependencies after pnpm install", async () => {
|
||||
const dockerfile = await readFile(dockerfilePath, "utf8");
|
||||
const installIndex = dockerfile.indexOf("pnpm install --frozen-lockfile");
|
||||
|
||||
Reference in New Issue
Block a user