mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 16:14:31 +00:00
refactor(channels): centralize match metadata
This commit is contained in:
@@ -16,6 +16,8 @@ export type AllowlistMatch<TSource extends string = AllowlistMatchSource> = {
|
|||||||
matchSource?: TSource;
|
matchSource?: TSource;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function formatAllowlistMatchMeta(match?: AllowlistMatch | null): string {
|
export function formatAllowlistMatchMeta(
|
||||||
|
match?: { matchKey?: string; matchSource?: string } | null,
|
||||||
|
): string {
|
||||||
return `matchKey=${match?.matchKey ?? "none"} matchSource=${match?.matchSource ?? "none"}`;
|
return `matchKey=${match?.matchKey ?? "none"} matchSource=${match?.matchSource ?? "none"}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import {
|
|||||||
resolveChannelEntryMatch,
|
resolveChannelEntryMatch,
|
||||||
resolveChannelEntryMatchWithFallback,
|
resolveChannelEntryMatchWithFallback,
|
||||||
resolveNestedAllowlistDecision,
|
resolveNestedAllowlistDecision,
|
||||||
|
applyChannelMatchMeta,
|
||||||
|
resolveChannelMatchConfig,
|
||||||
} from "./channel-config.js";
|
} from "./channel-config.js";
|
||||||
|
|
||||||
describe("buildChannelKeyCandidates", () => {
|
describe("buildChannelKeyCandidates", () => {
|
||||||
@@ -90,6 +92,33 @@ describe("resolveChannelEntryMatchWithFallback", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("applyChannelMatchMeta", () => {
|
||||||
|
it("copies match metadata onto resolved configs", () => {
|
||||||
|
const resolved = applyChannelMatchMeta(
|
||||||
|
{ allowed: true },
|
||||||
|
{ matchKey: "general", matchSource: "direct" },
|
||||||
|
);
|
||||||
|
expect(resolved.matchKey).toBe("general");
|
||||||
|
expect(resolved.matchSource).toBe("direct");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("resolveChannelMatchConfig", () => {
|
||||||
|
it("returns null when no entry is matched", () => {
|
||||||
|
const resolved = resolveChannelMatchConfig({ matchKey: "x" }, () => ({ allowed: true }));
|
||||||
|
expect(resolved).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves entry and applies match metadata", () => {
|
||||||
|
const resolved = resolveChannelMatchConfig(
|
||||||
|
{ entry: { allow: true }, matchKey: "*", matchSource: "wildcard" },
|
||||||
|
() => ({ allowed: true }),
|
||||||
|
);
|
||||||
|
expect(resolved?.matchKey).toBe("*");
|
||||||
|
expect(resolved?.matchSource).toBe("wildcard");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("resolveNestedAllowlistDecision", () => {
|
describe("resolveNestedAllowlistDecision", () => {
|
||||||
it("allows when outer allowlist is disabled", () => {
|
it("allows when outer allowlist is disabled", () => {
|
||||||
expect(
|
expect(
|
||||||
|
|||||||
@@ -11,6 +11,24 @@ export type ChannelEntryMatch<T> = {
|
|||||||
matchSource?: ChannelMatchSource;
|
matchSource?: ChannelMatchSource;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function applyChannelMatchMeta<
|
||||||
|
TResult extends { matchKey?: string; matchSource?: ChannelMatchSource },
|
||||||
|
>(result: TResult, match: ChannelEntryMatch<unknown>): TResult {
|
||||||
|
if (match.matchKey && match.matchSource) {
|
||||||
|
result.matchKey = match.matchKey;
|
||||||
|
result.matchSource = match.matchSource;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveChannelMatchConfig<
|
||||||
|
TEntry,
|
||||||
|
TResult extends { matchKey?: string; matchSource?: ChannelMatchSource },
|
||||||
|
>(match: ChannelEntryMatch<TEntry>, resolveEntry: (entry: TEntry) => TResult): TResult | null {
|
||||||
|
if (!match.entry) return null;
|
||||||
|
return applyChannelMatchMeta(resolveEntry(match.entry), match);
|
||||||
|
}
|
||||||
|
|
||||||
export function normalizeChannelSlug(value: string): string {
|
export function normalizeChannelSlug(value: string): string {
|
||||||
return value
|
return value
|
||||||
.trim()
|
.trim()
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
export type { ChannelEntryMatch, ChannelMatchSource } from "../channel-config.js";
|
export type { ChannelEntryMatch, ChannelMatchSource } from "../channel-config.js";
|
||||||
export {
|
export {
|
||||||
|
applyChannelMatchMeta,
|
||||||
buildChannelKeyCandidates,
|
buildChannelKeyCandidates,
|
||||||
normalizeChannelSlug,
|
normalizeChannelSlug,
|
||||||
resolveChannelEntryMatch,
|
resolveChannelEntryMatch,
|
||||||
resolveChannelEntryMatchWithFallback,
|
resolveChannelEntryMatchWithFallback,
|
||||||
|
resolveChannelMatchConfig,
|
||||||
resolveNestedAllowlistDecision,
|
resolveNestedAllowlistDecision,
|
||||||
} from "../channel-config.js";
|
} from "../channel-config.js";
|
||||||
|
|||||||
@@ -60,10 +60,12 @@ export {
|
|||||||
listWhatsAppDirectoryPeersFromConfig,
|
listWhatsAppDirectoryPeersFromConfig,
|
||||||
} from "./directory-config.js";
|
} from "./directory-config.js";
|
||||||
export {
|
export {
|
||||||
|
applyChannelMatchMeta,
|
||||||
buildChannelKeyCandidates,
|
buildChannelKeyCandidates,
|
||||||
normalizeChannelSlug,
|
normalizeChannelSlug,
|
||||||
resolveChannelEntryMatch,
|
resolveChannelEntryMatch,
|
||||||
resolveChannelEntryMatchWithFallback,
|
resolveChannelEntryMatchWithFallback,
|
||||||
|
resolveChannelMatchConfig,
|
||||||
resolveNestedAllowlistDecision,
|
resolveNestedAllowlistDecision,
|
||||||
type ChannelEntryMatch,
|
type ChannelEntryMatch,
|
||||||
type ChannelMatchSource,
|
type ChannelMatchSource,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { Guild, User } from "@buape/carbon";
|
|||||||
import {
|
import {
|
||||||
buildChannelKeyCandidates,
|
buildChannelKeyCandidates,
|
||||||
resolveChannelEntryMatchWithFallback,
|
resolveChannelEntryMatchWithFallback,
|
||||||
|
resolveChannelMatchConfig,
|
||||||
type ChannelMatchSource,
|
type ChannelMatchSource,
|
||||||
} from "../../channels/channel-config.js";
|
} from "../../channels/channel-config.js";
|
||||||
import type { AllowlistMatch } from "../../channels/allowlist-match.js";
|
import type { AllowlistMatch } from "../../channels/allowlist-match.js";
|
||||||
@@ -205,8 +206,6 @@ function resolveDiscordChannelEntryMatch(
|
|||||||
|
|
||||||
function resolveDiscordChannelConfigEntry(
|
function resolveDiscordChannelConfigEntry(
|
||||||
entry: DiscordChannelEntry,
|
entry: DiscordChannelEntry,
|
||||||
matchKey: string | undefined,
|
|
||||||
matchSource: ChannelMatchSource,
|
|
||||||
): DiscordChannelConfigResolved {
|
): DiscordChannelConfigResolved {
|
||||||
const resolved: DiscordChannelConfigResolved = {
|
const resolved: DiscordChannelConfigResolved = {
|
||||||
allowed: entry.allow !== false,
|
allowed: entry.allow !== false,
|
||||||
@@ -217,8 +216,6 @@ function resolveDiscordChannelConfigEntry(
|
|||||||
systemPrompt: entry.systemPrompt,
|
systemPrompt: entry.systemPrompt,
|
||||||
autoThread: entry.autoThread,
|
autoThread: entry.autoThread,
|
||||||
};
|
};
|
||||||
if (matchKey) resolved.matchKey = matchKey;
|
|
||||||
resolved.matchSource = matchSource;
|
|
||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,8 +233,8 @@ export function resolveDiscordChannelConfig(params: {
|
|||||||
name: channelName,
|
name: channelName,
|
||||||
slug: channelSlug,
|
slug: channelSlug,
|
||||||
});
|
});
|
||||||
if (!match.entry || !match.matchKey || !match.matchSource) return { allowed: false };
|
const resolved = resolveChannelMatchConfig(match, resolveDiscordChannelConfigEntry);
|
||||||
return resolveDiscordChannelConfigEntry(match.entry, match.matchKey, match.matchSource);
|
return resolved ?? { allowed: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveDiscordChannelConfigWithFallback(params: {
|
export function resolveDiscordChannelConfigWithFallback(params: {
|
||||||
@@ -279,10 +276,7 @@ export function resolveDiscordChannelConfigWithFallback(params: {
|
|||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
);
|
);
|
||||||
if (match.entry && match.matchKey && match.matchSource) {
|
return resolveChannelMatchConfig(match, resolveDiscordChannelConfigEntry) ?? { allowed: false };
|
||||||
return resolveDiscordChannelConfigEntry(match.entry, match.matchKey, match.matchSource);
|
|
||||||
}
|
|
||||||
return { allowed: false };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveDiscordShouldRequireMention(params: {
|
export function resolveDiscordShouldRequireMention(params: {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
} from "../../pairing/pairing-store.js";
|
} from "../../pairing/pairing-store.js";
|
||||||
import { resolveAgentRoute } from "../../routing/resolve-route.js";
|
import { resolveAgentRoute } from "../../routing/resolve-route.js";
|
||||||
import { resolveMentionGatingWithBypass } from "../../channels/mention-gating.js";
|
import { resolveMentionGatingWithBypass } from "../../channels/mention-gating.js";
|
||||||
|
import { formatAllowlistMatchMeta } from "../../channels/allowlist-match.js";
|
||||||
import { sendMessageDiscord } from "../send.js";
|
import { sendMessageDiscord } from "../send.js";
|
||||||
import { resolveControlCommandGate } from "../../channels/command-gating.js";
|
import { resolveControlCommandGate } from "../../channels/command-gating.js";
|
||||||
import {
|
import {
|
||||||
@@ -100,9 +101,7 @@ export async function preflightDiscordMessage(
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
: { allowed: false };
|
: { allowed: false };
|
||||||
const allowMatchMeta = `matchKey=${allowMatch.matchKey ?? "none"} matchSource=${
|
const allowMatchMeta = formatAllowlistMatchMeta(allowMatch);
|
||||||
allowMatch.matchSource ?? "none"
|
|
||||||
}`;
|
|
||||||
const permitted = allowMatch.allowed;
|
const permitted = allowMatch.allowed;
|
||||||
if (!permitted) {
|
if (!permitted) {
|
||||||
commandAuthorized = false;
|
commandAuthorized = false;
|
||||||
@@ -262,9 +261,7 @@ export async function preflightDiscordMessage(
|
|||||||
scope: threadChannel ? "thread" : "channel",
|
scope: threadChannel ? "thread" : "channel",
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
const channelMatchMeta = `matchKey=${channelConfig?.matchKey ?? "none"} matchSource=${
|
const channelMatchMeta = formatAllowlistMatchMeta(channelConfig);
|
||||||
channelConfig?.matchSource ?? "none"
|
|
||||||
}`;
|
|
||||||
if (isGuildMessage && channelConfig?.enabled === false) {
|
if (isGuildMessage && channelConfig?.enabled === false) {
|
||||||
logVerbose(
|
logVerbose(
|
||||||
`Blocked discord channel ${message.channelId} (channel disabled, ${channelMatchMeta})`,
|
`Blocked discord channel ${message.channelId} (channel disabled, ${channelMatchMeta})`,
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import type { SlackReactionNotificationMode } from "../../config/config.js";
|
import type { SlackReactionNotificationMode } from "../../config/config.js";
|
||||||
import type { SlackMessageEvent } from "../types.js";
|
import type { SlackMessageEvent } from "../types.js";
|
||||||
import {
|
import {
|
||||||
|
applyChannelMatchMeta,
|
||||||
buildChannelKeyCandidates,
|
buildChannelKeyCandidates,
|
||||||
resolveChannelEntryMatchWithFallback,
|
resolveChannelEntryMatchWithFallback,
|
||||||
|
type ChannelMatchSource,
|
||||||
} from "../../channels/channel-config.js";
|
} from "../../channels/channel-config.js";
|
||||||
import { allowListMatches, normalizeAllowListLower, normalizeSlackSlug } from "./allow-list.js";
|
import { allowListMatches, normalizeAllowListLower, normalizeSlackSlug } from "./allow-list.js";
|
||||||
|
|
||||||
@@ -14,7 +16,7 @@ export type SlackChannelConfigResolved = {
|
|||||||
skills?: string[];
|
skills?: string[];
|
||||||
systemPrompt?: string;
|
systemPrompt?: string;
|
||||||
matchKey?: string;
|
matchKey?: string;
|
||||||
matchSource?: "direct" | "wildcard";
|
matchSource?: ChannelMatchSource;
|
||||||
};
|
};
|
||||||
|
|
||||||
function firstDefined<T>(...values: Array<T | undefined>) {
|
function firstDefined<T>(...values: Array<T | undefined>) {
|
||||||
@@ -89,16 +91,12 @@ export function resolveSlackChannelConfig(params: {
|
|||||||
directName,
|
directName,
|
||||||
normalizedName,
|
normalizedName,
|
||||||
);
|
);
|
||||||
const {
|
const match = resolveChannelEntryMatchWithFallback({
|
||||||
entry: matched,
|
|
||||||
wildcardEntry: fallback,
|
|
||||||
matchKey,
|
|
||||||
matchSource,
|
|
||||||
} = resolveChannelEntryMatchWithFallback({
|
|
||||||
entries,
|
entries,
|
||||||
keys: candidates,
|
keys: candidates,
|
||||||
wildcardKey: "*",
|
wildcardKey: "*",
|
||||||
});
|
});
|
||||||
|
const { entry: matched, wildcardEntry: fallback } = match;
|
||||||
|
|
||||||
const requireMentionDefault = defaultRequireMention ?? true;
|
const requireMentionDefault = defaultRequireMention ?? true;
|
||||||
if (keys.length === 0) {
|
if (keys.length === 0) {
|
||||||
@@ -127,11 +125,7 @@ export function resolveSlackChannelConfig(params: {
|
|||||||
skills,
|
skills,
|
||||||
systemPrompt,
|
systemPrompt,
|
||||||
};
|
};
|
||||||
if (matchKey) result.matchKey = matchKey;
|
return applyChannelMatchMeta(result, match);
|
||||||
if (matchSource === "direct" || matchSource === "wildcard") {
|
|
||||||
result.matchSource = matchSource;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type { SlackMessageEvent };
|
export type { SlackMessageEvent };
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { createDedupeCache } from "../../infra/dedupe.js";
|
|||||||
import { getChildLogger } from "../../logging.js";
|
import { getChildLogger } from "../../logging.js";
|
||||||
import type { RuntimeEnv } from "../../runtime.js";
|
import type { RuntimeEnv } from "../../runtime.js";
|
||||||
import type { SlackMessageEvent } from "../types.js";
|
import type { SlackMessageEvent } from "../types.js";
|
||||||
|
import { formatAllowlistMatchMeta } from "../../channels/allowlist-match.js";
|
||||||
|
|
||||||
import { normalizeAllowList, normalizeAllowListLower, normalizeSlackSlug } from "./allow-list.js";
|
import { normalizeAllowList, normalizeAllowListLower, normalizeSlackSlug } from "./allow-list.js";
|
||||||
import { resolveSlackChannelConfig } from "./channel-config.js";
|
import { resolveSlackChannelConfig } from "./channel-config.js";
|
||||||
@@ -310,9 +311,7 @@ export function createSlackMonitorContext(params: {
|
|||||||
channels: params.channelsConfig,
|
channels: params.channelsConfig,
|
||||||
defaultRequireMention,
|
defaultRequireMention,
|
||||||
});
|
});
|
||||||
const channelMatchMeta = `matchKey=${channelConfig?.matchKey ?? "none"} matchSource=${
|
const channelMatchMeta = formatAllowlistMatchMeta(channelConfig);
|
||||||
channelConfig?.matchSource ?? "none"
|
|
||||||
}`;
|
|
||||||
const channelAllowed = channelConfig?.allowed !== false;
|
const channelAllowed = channelConfig?.allowed !== false;
|
||||||
const channelAllowlistConfigured =
|
const channelAllowlistConfigured =
|
||||||
Boolean(params.channelsConfig) && Object.keys(params.channelsConfig ?? {}).length > 0;
|
Boolean(params.channelsConfig) && Object.keys(params.channelsConfig ?? {}).length > 0;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { resolveThreadSessionKeys } from "../../../routing/session-key.js";
|
|||||||
import { resolveMentionGatingWithBypass } from "../../../channels/mention-gating.js";
|
import { resolveMentionGatingWithBypass } from "../../../channels/mention-gating.js";
|
||||||
import { resolveConversationLabel } from "../../../channels/conversation-label.js";
|
import { resolveConversationLabel } from "../../../channels/conversation-label.js";
|
||||||
import { resolveControlCommandGate } from "../../../channels/command-gating.js";
|
import { resolveControlCommandGate } from "../../../channels/command-gating.js";
|
||||||
|
import { formatAllowlistMatchMeta } from "../../../channels/allowlist-match.js";
|
||||||
import {
|
import {
|
||||||
readSessionUpdatedAt,
|
readSessionUpdatedAt,
|
||||||
recordSessionMetaFromInbound,
|
recordSessionMetaFromInbound,
|
||||||
@@ -131,9 +132,7 @@ export async function prepareSlackMessage(params: {
|
|||||||
allowList: allowFromLower,
|
allowList: allowFromLower,
|
||||||
id: directUserId,
|
id: directUserId,
|
||||||
});
|
});
|
||||||
const allowMatchMeta = `matchKey=${allowMatch.matchKey ?? "none"} matchSource=${
|
const allowMatchMeta = formatAllowlistMatchMeta(allowMatch);
|
||||||
allowMatch.matchSource ?? "none"
|
|
||||||
}`;
|
|
||||||
if (!allowMatch.allowed) {
|
if (!allowMatch.allowed) {
|
||||||
if (ctx.dmPolicy === "pairing") {
|
if (ctx.dmPolicy === "pairing") {
|
||||||
const sender = await ctx.resolveUserName(directUserId);
|
const sender = await ctx.resolveUserName(directUserId);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
import { resolveAgentRoute } from "../../routing/resolve-route.js";
|
import { resolveAgentRoute } from "../../routing/resolve-route.js";
|
||||||
import { resolveConversationLabel } from "../../channels/conversation-label.js";
|
import { resolveConversationLabel } from "../../channels/conversation-label.js";
|
||||||
import { resolveCommandAuthorizedFromAuthorizers } from "../../channels/command-gating.js";
|
import { resolveCommandAuthorizedFromAuthorizers } from "../../channels/command-gating.js";
|
||||||
|
import { formatAllowlistMatchMeta } from "../../channels/allowlist-match.js";
|
||||||
|
|
||||||
import type { ResolvedSlackAccount } from "../accounts.js";
|
import type { ResolvedSlackAccount } from "../accounts.js";
|
||||||
|
|
||||||
@@ -206,9 +207,7 @@ export function registerSlackMonitorSlashCommands(params: {
|
|||||||
id: command.user_id,
|
id: command.user_id,
|
||||||
name: senderName,
|
name: senderName,
|
||||||
});
|
});
|
||||||
const allowMatchMeta = `matchKey=${allowMatch.matchKey ?? "none"} matchSource=${
|
const allowMatchMeta = formatAllowlistMatchMeta(allowMatch);
|
||||||
allowMatch.matchSource ?? "none"
|
|
||||||
}`;
|
|
||||||
if (!allowMatch.allowed) {
|
if (!allowMatch.allowed) {
|
||||||
if (ctx.dmPolicy === "pairing") {
|
if (ctx.dmPolicy === "pairing") {
|
||||||
const { code, created } = await upsertChannelPairingRequest({
|
const { code, created } = await upsertChannelPairingRequest({
|
||||||
|
|||||||
Reference in New Issue
Block a user