mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
refactor: centralize channel ingress access
This commit is contained in:
@@ -18,7 +18,6 @@ import {
|
||||
resolveOpenProviderRuntimeGroupPolicy,
|
||||
warnMissingProviderGroupPolicyFallbackOnce,
|
||||
} from "openclaw/plugin-sdk/runtime-group-policy";
|
||||
import { resolveOpenDmAllowlistAccess } from "openclaw/plugin-sdk/security-runtime";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { resolveFeishuRuntimeAccount } from "./accounts.js";
|
||||
import {
|
||||
@@ -33,7 +32,6 @@ import {
|
||||
import {
|
||||
buildAgentMediaPayload,
|
||||
evaluateSupplementalContextVisibility,
|
||||
filterSupplementalContextItems,
|
||||
normalizeAgentId,
|
||||
resolveChannelContextVisibilityMode,
|
||||
} from "./bot-runtime-api.js";
|
||||
@@ -47,9 +45,10 @@ import { maybeCreateDynamicAgent } from "./dynamic-agent.js";
|
||||
import { extractMentionTargets, isMentionForwardRequest } from "./mention.js";
|
||||
import {
|
||||
hasExplicitFeishuGroupConfig,
|
||||
isFeishuGroupAllowed,
|
||||
resolveFeishuAllowlistMatch,
|
||||
resolveFeishuDmIngressAccess,
|
||||
resolveFeishuGroupConfig,
|
||||
resolveFeishuGroupConversationIngressAccess,
|
||||
resolveFeishuGroupSenderActivationIngressAccess,
|
||||
resolveFeishuReplyPolicy,
|
||||
} from "./policy.js";
|
||||
import { resolveFeishuReasoningPreviewEnabled } from "./reasoning-preview.js";
|
||||
@@ -353,44 +352,33 @@ export function buildFeishuAgentBody(params: {
|
||||
return messageBody;
|
||||
}
|
||||
|
||||
function isFetchedGroupContextSenderAllowed(params: {
|
||||
isGroup: boolean;
|
||||
allowFrom: Array<string | number>;
|
||||
senderId?: string;
|
||||
senderType?: string;
|
||||
}): boolean {
|
||||
if (!params.isGroup || params.allowFrom.length === 0) {
|
||||
return true;
|
||||
}
|
||||
if (params.senderType === "app") {
|
||||
return true;
|
||||
}
|
||||
const senderId = params.senderId?.trim();
|
||||
const senderAllowed =
|
||||
!!senderId &&
|
||||
isFeishuGroupAllowed({
|
||||
groupPolicy: "allowlist",
|
||||
allowFrom: params.allowFrom,
|
||||
senderId,
|
||||
senderName: undefined,
|
||||
});
|
||||
return senderAllowed;
|
||||
}
|
||||
|
||||
function shouldIncludeFetchedGroupContextMessage(params: {
|
||||
async function shouldIncludeFetchedGroupContextMessage(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
accountId: string;
|
||||
chatId: string;
|
||||
isGroup: boolean;
|
||||
allowFrom: Array<string | number>;
|
||||
mode: "all" | "allowlist" | "allowlist_quote";
|
||||
kind: "quote" | "thread" | "history";
|
||||
senderId?: string;
|
||||
senderType?: string;
|
||||
}): boolean {
|
||||
const senderAllowed = isFetchedGroupContextSenderAllowed({
|
||||
isGroup: params.isGroup,
|
||||
allowFrom: params.allowFrom,
|
||||
senderId: params.senderId,
|
||||
senderType: params.senderType,
|
||||
});
|
||||
}): Promise<boolean> {
|
||||
let senderAllowed =
|
||||
!params.isGroup || params.allowFrom.length === 0 || params.senderType === "app";
|
||||
const senderId = params.senderId?.trim();
|
||||
if (!senderAllowed && senderId) {
|
||||
const access = await resolveFeishuGroupSenderActivationIngressAccess({
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
chatId: params.chatId,
|
||||
allowFrom: params.allowFrom,
|
||||
senderOpenId: senderId,
|
||||
senderUserId: senderId,
|
||||
requireMention: false,
|
||||
mentionedBot: true,
|
||||
});
|
||||
senderAllowed = access.senderAccess.decision === "allow";
|
||||
}
|
||||
return evaluateSupplementalContextVisibility({
|
||||
mode: params.mode,
|
||||
kind: params.kind,
|
||||
@@ -398,29 +386,38 @@ function shouldIncludeFetchedGroupContextMessage(params: {
|
||||
}).include;
|
||||
}
|
||||
|
||||
function filterFetchedGroupContextMessages<
|
||||
async function filterFetchedGroupContextMessages<
|
||||
T extends Pick<FeishuMessageInfo, "senderId" | "senderType">,
|
||||
>(
|
||||
messages: readonly T[],
|
||||
params: {
|
||||
cfg: ClawdbotConfig;
|
||||
accountId: string;
|
||||
chatId: string;
|
||||
isGroup: boolean;
|
||||
allowFrom: Array<string | number>;
|
||||
mode: "all" | "allowlist" | "allowlist_quote";
|
||||
kind: "quote" | "thread" | "history";
|
||||
},
|
||||
): T[] {
|
||||
return filterSupplementalContextItems({
|
||||
items: messages,
|
||||
mode: params.mode,
|
||||
kind: params.kind,
|
||||
isSenderAllowed: (message) =>
|
||||
isFetchedGroupContextSenderAllowed({
|
||||
): Promise<T[]> {
|
||||
const results: Array<T | undefined> = await Promise.all(
|
||||
messages.map(async (message) =>
|
||||
(await shouldIncludeFetchedGroupContextMessage({
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
chatId: params.chatId,
|
||||
isGroup: params.isGroup,
|
||||
allowFrom: params.allowFrom,
|
||||
mode: params.mode,
|
||||
kind: params.kind,
|
||||
senderId: message.senderId,
|
||||
senderType: message.senderType,
|
||||
}),
|
||||
}).items;
|
||||
}))
|
||||
? message
|
||||
: undefined,
|
||||
),
|
||||
);
|
||||
return results.filter((message): message is T => message !== undefined);
|
||||
}
|
||||
|
||||
export async function handleFeishuMessage(params: {
|
||||
@@ -595,7 +592,6 @@ export async function handleFeishuMessage(params: {
|
||||
const groupHistoryKey = isGroup ? (groupSession?.peerId ?? ctx.chatId) : undefined;
|
||||
const dmPolicy = feishuCfg?.dmPolicy ?? "pairing";
|
||||
const configAllowFrom = feishuCfg?.allowFrom ?? [];
|
||||
const useAccessGroups = cfg.commands?.useAccessGroups !== false;
|
||||
const rawBroadcastAgents = isGroup ? resolveBroadcastAgents(cfg, ctx.chatId) : null;
|
||||
const broadcastAgents = rawBroadcastAgents
|
||||
? [...new Set(rawBroadcastAgents.map((id) => normalizeAgentId(id)))]
|
||||
@@ -639,39 +635,22 @@ export async function handleFeishuMessage(params: {
|
||||
groupId: ctx.chatId,
|
||||
});
|
||||
|
||||
// Check if this GROUP is allowed (groupAllowFrom contains group IDs like oc_xxx, not user IDs)
|
||||
const groupAllowed =
|
||||
groupPolicy !== "disabled" &&
|
||||
(groupExplicitlyConfigured ||
|
||||
isFeishuGroupAllowed({
|
||||
groupPolicy,
|
||||
allowFrom: groupAllowFrom,
|
||||
senderId: ctx.chatId, // Check group ID, not sender ID
|
||||
senderName: undefined,
|
||||
}));
|
||||
const groupIngress = await resolveFeishuGroupConversationIngressAccess({
|
||||
cfg,
|
||||
accountId: account.accountId,
|
||||
chatId: ctx.chatId,
|
||||
groupPolicy,
|
||||
groupAllowFrom,
|
||||
groupExplicitlyConfigured,
|
||||
});
|
||||
|
||||
if (!groupAllowed) {
|
||||
if (groupIngress.ingress.admission !== "dispatch") {
|
||||
log(
|
||||
`feishu[${account.accountId}]: group ${ctx.chatId} not in groupAllowFrom (groupPolicy=${groupPolicy})`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Sender-level allowlist: per-group allowFrom takes precedence, then global groupSenderAllowFrom
|
||||
if (effectiveGroupSenderAllowFrom.length > 0) {
|
||||
const senderAllowed = isFeishuGroupAllowed({
|
||||
groupPolicy: "allowlist",
|
||||
allowFrom: effectiveGroupSenderAllowFrom,
|
||||
senderId: ctx.senderOpenId,
|
||||
senderIds: [senderUserId],
|
||||
senderName: ctx.senderName,
|
||||
});
|
||||
if (!senderAllowed) {
|
||||
log(`feishu: sender ${ctx.senderOpenId} not in group ${ctx.chatId} sender allowlist`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
({ requireMention } = resolveFeishuReplyPolicy({
|
||||
isDirectMessage: false,
|
||||
cfg,
|
||||
@@ -680,7 +659,21 @@ export async function handleFeishuMessage(params: {
|
||||
groupPolicy,
|
||||
}));
|
||||
|
||||
if (requireMention && !ctx.mentionedBot) {
|
||||
const groupSenderActivationIngress = await resolveFeishuGroupSenderActivationIngressAccess({
|
||||
cfg,
|
||||
accountId: account.accountId,
|
||||
chatId: ctx.chatId,
|
||||
allowFrom: effectiveGroupSenderAllowFrom,
|
||||
senderOpenId: ctx.senderOpenId,
|
||||
senderUserId,
|
||||
requireMention,
|
||||
mentionedBot: ctx.mentionedBot,
|
||||
});
|
||||
if (groupSenderActivationIngress.senderAccess.decision !== "allow") {
|
||||
log(`feishu: sender ${ctx.senderOpenId} not in group ${ctx.chatId} sender allowlist`);
|
||||
return;
|
||||
}
|
||||
if (groupSenderActivationIngress.ingress.admission !== "dispatch") {
|
||||
log(`feishu[${account.accountId}]: message in group ${ctx.chatId} did not mention bot`);
|
||||
// Record to pending history for non-broadcast groups only. For broadcast groups,
|
||||
// the mentioned handler's broadcast dispatch writes the turn directly into all
|
||||
@@ -715,34 +708,22 @@ export async function handleFeishuMessage(params: {
|
||||
commandProbeBody,
|
||||
cfg,
|
||||
);
|
||||
const storeAllowFrom =
|
||||
!isGroup && dmPolicy !== "allowlist" && dmPolicy !== "open"
|
||||
? await pairing.readAllowFromStore().catch(() => [])
|
||||
: [];
|
||||
const effectiveDmAllowFrom = [...configAllowFrom, ...storeAllowFrom];
|
||||
const dmAllowed = resolveFeishuAllowlistMatch({
|
||||
allowFrom: effectiveDmAllowFrom,
|
||||
senderId: ctx.senderOpenId,
|
||||
senderIds: [senderUserId],
|
||||
senderName: ctx.senderName,
|
||||
}).allowed;
|
||||
|
||||
const dmAccessAllowed =
|
||||
dmPolicy === "open"
|
||||
? resolveOpenDmAllowlistAccess({
|
||||
effectiveAllowFrom: effectiveDmAllowFrom,
|
||||
isSenderAllowed: (allowFrom) =>
|
||||
resolveFeishuAllowlistMatch({
|
||||
allowFrom,
|
||||
senderId: ctx.senderOpenId,
|
||||
senderIds: [senderUserId],
|
||||
senderName: ctx.senderName,
|
||||
}).allowed,
|
||||
}).decision === "allow"
|
||||
: dmAllowed;
|
||||
|
||||
if (isDirect && !dmAccessAllowed) {
|
||||
if (dmPolicy === "pairing") {
|
||||
const dmIngress = isDirect
|
||||
? await resolveFeishuDmIngressAccess({
|
||||
cfg,
|
||||
accountId: account.accountId,
|
||||
dmPolicy,
|
||||
allowFrom: configAllowFrom,
|
||||
readAllowFromStore: pairing.readAllowFromStore,
|
||||
senderOpenId: ctx.senderOpenId,
|
||||
senderUserId,
|
||||
conversationId: ctx.senderOpenId,
|
||||
mayPair: true,
|
||||
...(shouldComputeCommandAuthorized ? { command: { hasControlCommand: true } } : {}),
|
||||
})
|
||||
: null;
|
||||
if (isDirect && dmIngress?.ingress.admission !== "dispatch") {
|
||||
if (dmIngress?.ingress.admission === "pairing-required") {
|
||||
await pairing.issueChallenge({
|
||||
senderId: ctx.senderOpenId,
|
||||
senderIdLine: `Your Feishu user id: ${ctx.senderOpenId}`,
|
||||
@@ -774,13 +755,7 @@ export async function handleFeishuMessage(params: {
|
||||
|
||||
const commandAllowFrom = isGroup
|
||||
? (groupConfig?.allowFrom ?? configAllowFrom)
|
||||
: effectiveDmAllowFrom;
|
||||
const senderAllowedForCommands = resolveFeishuAllowlistMatch({
|
||||
allowFrom: commandAllowFrom,
|
||||
senderId: ctx.senderOpenId,
|
||||
senderIds: [senderUserId],
|
||||
senderName: ctx.senderName,
|
||||
}).allowed;
|
||||
: (dmIngress?.senderAccess.effectiveAllowFrom ?? configAllowFrom);
|
||||
|
||||
// In group chats, the session is scoped to the group, but the *speaker* is the sender.
|
||||
// Using a group-scoped From causes the agent to treat different users as the same person.
|
||||
@@ -982,12 +957,36 @@ export async function handleFeishuMessage(params: {
|
||||
? shouldComputeCommandAuthorized
|
||||
: core.channel.commands.shouldComputeCommandAuthorized(effectiveCommandProbeBody, cfg);
|
||||
const commandAuthorized = shouldComputeEffectiveCommandAuthorized
|
||||
? core.channel.commands.resolveCommandAuthorizedFromAuthorizers({
|
||||
useAccessGroups,
|
||||
authorizers: [
|
||||
{ configured: commandAllowFrom.length > 0, allowed: senderAllowedForCommands },
|
||||
],
|
||||
})
|
||||
? isDirect && audioTranscript === undefined && dmIngress
|
||||
? dmIngress.commandAccess.authorized
|
||||
: isGroup
|
||||
? (
|
||||
await resolveFeishuGroupSenderActivationIngressAccess({
|
||||
cfg,
|
||||
accountId: account.accountId,
|
||||
chatId: ctx.chatId,
|
||||
allowFrom: commandAllowFrom,
|
||||
senderOpenId: ctx.senderOpenId,
|
||||
senderUserId,
|
||||
requireMention: false,
|
||||
mentionedBot: true,
|
||||
command: { hasControlCommand: true },
|
||||
})
|
||||
).commandAccess.authorized
|
||||
: (
|
||||
await resolveFeishuDmIngressAccess({
|
||||
cfg,
|
||||
accountId: account.accountId,
|
||||
dmPolicy,
|
||||
allowFrom: configAllowFrom,
|
||||
readAllowFromStore: pairing.readAllowFromStore,
|
||||
senderOpenId: ctx.senderOpenId,
|
||||
senderUserId,
|
||||
conversationId: ctx.senderOpenId,
|
||||
mayPair: false,
|
||||
command: { hasControlCommand: true },
|
||||
})
|
||||
).commandAccess.authorized
|
||||
: undefined;
|
||||
|
||||
// Fetch quoted/replied message content if parentId exists
|
||||
@@ -1002,14 +1001,17 @@ export async function handleFeishuMessage(params: {
|
||||
});
|
||||
if (
|
||||
quotedMessageInfo &&
|
||||
shouldIncludeFetchedGroupContextMessage({
|
||||
(await shouldIncludeFetchedGroupContextMessage({
|
||||
cfg,
|
||||
accountId: account.accountId,
|
||||
chatId: ctx.chatId,
|
||||
isGroup,
|
||||
allowFrom: effectiveGroupSenderAllowFrom,
|
||||
mode: contextVisibilityMode,
|
||||
kind: "quote",
|
||||
senderId: quotedMessageInfo.senderId,
|
||||
senderType: quotedMessageInfo.senderType,
|
||||
})
|
||||
}))
|
||||
) {
|
||||
quotedContent = quotedMessageInfo.content;
|
||||
log(
|
||||
@@ -1115,14 +1117,17 @@ export async function handleFeishuMessage(params: {
|
||||
rootMessageThreadId = rootMessageInfo?.threadId;
|
||||
if (
|
||||
rootMessageInfo &&
|
||||
!shouldIncludeFetchedGroupContextMessage({
|
||||
!(await shouldIncludeFetchedGroupContextMessage({
|
||||
cfg,
|
||||
accountId: account.accountId,
|
||||
chatId: ctx.chatId,
|
||||
isGroup,
|
||||
allowFrom: effectiveGroupSenderAllowFrom,
|
||||
mode: contextVisibilityMode,
|
||||
kind: "thread",
|
||||
senderId: rootMessageInfo.senderId,
|
||||
senderType: rootMessageInfo.senderType,
|
||||
})
|
||||
}))
|
||||
) {
|
||||
log(
|
||||
`feishu[${account.accountId}]: skipped thread starter from sender ${rootMessageInfo.senderId ?? "unknown"} (mode=${contextVisibilityMode})`,
|
||||
@@ -1208,7 +1213,10 @@ export async function handleFeishuMessage(params: {
|
||||
.map((id) => id?.trim())
|
||||
.filter((id): id is string => id !== undefined && id.length > 0),
|
||||
);
|
||||
const allowlistedMessages = filterFetchedGroupContextMessages(threadMessages, {
|
||||
const allowlistedMessages = await filterFetchedGroupContextMessages(threadMessages, {
|
||||
cfg,
|
||||
accountId: account.accountId,
|
||||
chatId: ctx.chatId,
|
||||
isGroup,
|
||||
allowFrom: effectiveGroupSenderAllowFrom,
|
||||
mode: contextVisibilityMode,
|
||||
|
||||
Reference in New Issue
Block a user