


























@@ -1,8 +1,11 @@
1+import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
2+import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
13import { readStoreAllowFromForDmPolicy } from "openclaw/plugin-sdk/security-runtime";
24import {
35allowListMatches,
46normalizeAllowList,
57normalizeAllowListLower,
8+normalizeSlackAllowOwnerEntry,
69resolveSlackAllowListMatch,
710resolveSlackUserAllowed,
811} from "./allow-list.js";
@@ -24,8 +27,20 @@ type SlackAllowFromCacheState = {
2427pairingPending?: Promise<ResolvedAllowFromLists>;
2528};
262930+type SlackChannelMembersCacheEntry = {
31+expiresAtMs: number;
32+members?: Set<string>;
33+pending?: Promise<Set<string>>;
34+};
35+2736let slackAllowFromCache = new WeakMap<SlackMonitorContext, SlackAllowFromCacheState>();
37+let slackChannelMembersCache = new WeakMap<
38+SlackMonitorContext,
39+Map<string, SlackChannelMembersCacheEntry>
40+>();
2841const DEFAULT_PAIRING_ALLOW_FROM_CACHE_TTL_MS = 5000;
42+const DEFAULT_CHANNEL_MEMBERS_CACHE_TTL_MS = 60_000;
43+const CHANNEL_MEMBERS_CACHE_MAX = 512;
29443045function getPairingAllowFromCacheTtlMs(): number {
3146const raw = process.env.OPENCLAW_SLACK_PAIRING_ALLOWFROM_CACHE_TTL_MS?.trim();
@@ -39,6 +54,18 @@ function getPairingAllowFromCacheTtlMs(): number {
3954return Math.max(0, Math.floor(parsed));
4055}
415657+function getChannelMembersCacheTtlMs(): number {
58+const raw = process.env.OPENCLAW_SLACK_CHANNEL_MEMBERS_CACHE_TTL_MS?.trim();
59+if (!raw) {
60+return DEFAULT_CHANNEL_MEMBERS_CACHE_TTL_MS;
61+}
62+const parsed = Number(raw);
63+if (!Number.isFinite(parsed)) {
64+return DEFAULT_CHANNEL_MEMBERS_CACHE_TTL_MS;
65+}
66+return Math.max(0, Math.floor(parsed));
67+}
68+4269function getAllowFromCacheState(ctx: SlackMonitorContext): SlackAllowFromCacheState {
4370const existing = slackAllowFromCache.get(ctx);
4471if (existing) {
@@ -49,6 +76,28 @@ function getAllowFromCacheState(ctx: SlackMonitorContext): SlackAllowFromCacheSt
4976return next;
5077}
517879+function getChannelMembersCache(
80+ctx: SlackMonitorContext,
81+): Map<string, SlackChannelMembersCacheEntry> {
82+const existing = slackChannelMembersCache.get(ctx);
83+if (existing) {
84+return existing;
85+}
86+const next = new Map<string, SlackChannelMembersCacheEntry>();
87+slackChannelMembersCache.set(ctx, next);
88+return next;
89+}
90+91+function pruneChannelMembersCache(cache: Map<string, SlackChannelMembersCacheEntry>): void {
92+while (cache.size > CHANNEL_MEMBERS_CACHE_MAX) {
93+const oldest = cache.keys().next();
94+if (oldest.done) {
95+return;
96+}
97+cache.delete(oldest.value);
98+}
99+}
100+52101function buildBaseAllowFrom(ctx: SlackMonitorContext): ResolvedAllowFromLists {
53102const allowFrom = normalizeAllowList(ctx.allowFrom);
54103return {
@@ -131,6 +180,10 @@ export async function resolveSlackEffectiveAllowFrom(
131180132181export function clearSlackAllowFromCacheForTest(): void {
133182slackAllowFromCache = new WeakMap<SlackMonitorContext, SlackAllowFromCacheState>();
183+slackChannelMembersCache = new WeakMap<
184+SlackMonitorContext,
185+Map<string, SlackChannelMembersCacheEntry>
186+>();
134187}
135188136189export function isSlackSenderAllowListed(params: {
@@ -151,6 +204,128 @@ export function isSlackSenderAllowListed(params: {
151204);
152205}
153206207+async function fetchSlackChannelMemberIds(
208+ctx: SlackMonitorContext,
209+channelId: string,
210+): Promise<Set<string>> {
211+const members = new Set<string>();
212+let cursor: string | undefined;
213+do {
214+const response = await ctx.app.client.conversations.members({
215+token: ctx.botToken,
216+channel: channelId,
217+limit: 999,
218+ ...(cursor ? { cursor } : {}),
219+});
220+for (const member of normalizeAllowListLower(response.members)) {
221+members.add(member);
222+}
223+const nextCursor = response.response_metadata?.next_cursor?.trim();
224+cursor = nextCursor ? nextCursor : undefined;
225+} while (cursor);
226+return members;
227+}
228+229+async function resolveSlackChannelMemberIds(
230+ctx: SlackMonitorContext,
231+channelId: string,
232+): Promise<Set<string>> {
233+const cache = getChannelMembersCache(ctx);
234+const key = `${ctx.accountId}:${channelId}`;
235+const ttlMs = getChannelMembersCacheTtlMs();
236+const nowMs = Date.now();
237+const cached = cache.get(key);
238+if (ttlMs > 0 && cached?.members && cached.expiresAtMs >= nowMs) {
239+return cached.members;
240+}
241+if (cached?.pending) {
242+return await cached.pending;
243+}
244+245+const pending = fetchSlackChannelMemberIds(ctx, channelId);
246+cache.set(key, {
247+expiresAtMs: ttlMs > 0 ? nowMs + ttlMs : 0,
248+ pending,
249+});
250+pruneChannelMembersCache(cache);
251+try {
252+const members = await pending;
253+if (ttlMs > 0) {
254+cache.set(key, {
255+expiresAtMs: Date.now() + ttlMs,
256+ members,
257+});
258+pruneChannelMembersCache(cache);
259+} else {
260+cache.delete(key);
261+}
262+return members;
263+} finally {
264+const latest = cache.get(key);
265+if (latest?.pending === pending) {
266+cache.delete(key);
267+}
268+}
269+}
270+271+function resolveExplicitSlackOwnerIds(allowFromLower: string[]): string[] {
272+const ownerIds = new Set<string>();
273+for (const entry of allowFromLower) {
274+const ownerId = normalizeSlackAllowOwnerEntry(entry);
275+if (ownerId) {
276+ownerIds.add(ownerId);
277+}
278+}
279+return [...ownerIds];
280+}
281+282+export async function authorizeSlackBotRoomMessage(params: {
283+ctx: SlackMonitorContext;
284+channelId: string;
285+senderId: string;
286+senderName?: string;
287+channelUsers?: Array<string | number>;
288+allowFromLower: string[];
289+}): Promise<boolean> {
290+const channelUserAllowList = normalizeAllowListLower(params.channelUsers).filter(
291+(entry) => entry !== "*",
292+);
293+if (
294+channelUserAllowList.length > 0 &&
295+allowListMatches({
296+allowList: channelUserAllowList,
297+id: params.senderId,
298+name: params.senderName,
299+allowNameMatching: params.ctx.allowNameMatching,
300+})
301+) {
302+return true;
303+}
304+305+const explicitOwnerIds = resolveExplicitSlackOwnerIds(params.allowFromLower);
306+if (explicitOwnerIds.length === 0) {
307+logVerbose(
308+`slack: drop bot message ${params.senderId} in ${params.channelId} (no explicit owner id for presence check)`,
309+);
310+return false;
311+}
312+313+try {
314+const channelMemberIds = await resolveSlackChannelMemberIds(params.ctx, params.channelId);
315+if (explicitOwnerIds.some((ownerId) => channelMemberIds.has(ownerId))) {
316+return true;
317+}
318+logVerbose(
319+`slack: drop bot message ${params.senderId} in ${params.channelId} (no owner present)`,
320+);
321+} catch (error) {
322+logVerbose(
323+`slack: drop bot message ${params.senderId} in ${params.channelId} (owner presence lookup failed: ${formatErrorMessage(error)})`,
324+);
325+}
326+return false;
327+}
328+154329export type SlackSystemEventAuthResult = {
155330allowed: boolean;
156331reason?:
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。