mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-07 18:03:51 +02:00
refactor: centralize inbound mention policy
This commit is contained in:
@@ -594,6 +594,45 @@ describe("preflightDiscordMessage", () => {
|
||||
expect(result).not.toBeNull();
|
||||
});
|
||||
|
||||
it("still drops bot control commands without a real mention when allowBots=mentions", async () => {
|
||||
const channelId = "channel-bot-command-no-mention";
|
||||
const guildId = "guild-bot-command-no-mention";
|
||||
const message = createDiscordMessage({
|
||||
id: "m-bot-command-no-mention",
|
||||
channelId,
|
||||
content: "/new incident room",
|
||||
author: {
|
||||
id: "relay-bot-1",
|
||||
bot: true,
|
||||
username: "Relay",
|
||||
},
|
||||
});
|
||||
|
||||
const result = await runMentionOnlyBotPreflight({ channelId, guildId, message });
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("still allows bot control commands with an explicit mention when allowBots=mentions", async () => {
|
||||
const channelId = "channel-bot-command-with-mention";
|
||||
const guildId = "guild-bot-command-with-mention";
|
||||
const message = createDiscordMessage({
|
||||
id: "m-bot-command-with-mention",
|
||||
channelId,
|
||||
content: "<@openclaw-bot> /new incident room",
|
||||
mentionedUsers: [{ id: "openclaw-bot" }],
|
||||
author: {
|
||||
id: "relay-bot-1",
|
||||
bot: true,
|
||||
username: "Relay",
|
||||
},
|
||||
});
|
||||
|
||||
const result = await runMentionOnlyBotPreflight({ channelId, guildId, message });
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
});
|
||||
|
||||
it("treats @everyone as a mention when requireMention is true", async () => {
|
||||
const channelId = "channel-everyone-mention";
|
||||
const guildId = "guild-everyone-mention";
|
||||
|
||||
@@ -3,9 +3,10 @@ import { Routes, type APIMessage } from "discord-api-types/v10";
|
||||
import { formatAllowlistMatchMeta } from "openclaw/plugin-sdk/allow-from";
|
||||
import {
|
||||
buildMentionRegexes,
|
||||
implicitMentionKindWhen,
|
||||
logInboundDrop,
|
||||
matchesMentionWithExplicit,
|
||||
resolveMentionGatingWithBypass,
|
||||
resolveInboundMentionDecision,
|
||||
} from "openclaw/plugin-sdk/channel-inbound";
|
||||
import { resolveControlCommandGate } from "openclaw/plugin-sdk/command-auth-native";
|
||||
import { hasControlCommand } from "openclaw/plugin-sdk/command-detection";
|
||||
@@ -181,10 +182,10 @@ function resolveDiscordMentionState(params: {
|
||||
referencedAuthorId?: string;
|
||||
senderIsPluralKit: boolean;
|
||||
transcript?: string;
|
||||
}): { implicitMention: boolean; wasMentioned: boolean } {
|
||||
}) {
|
||||
if (params.isDirectMessage) {
|
||||
return {
|
||||
implicitMention: false,
|
||||
implicitMentionKinds: [],
|
||||
wasMentioned: false,
|
||||
};
|
||||
}
|
||||
@@ -203,12 +204,15 @@ function resolveDiscordMentionState(params: {
|
||||
},
|
||||
transcript: params.transcript,
|
||||
});
|
||||
const implicitMention = Boolean(
|
||||
params.botId && params.referencedAuthorId && params.referencedAuthorId === params.botId,
|
||||
const implicitMentionKinds = implicitMentionKindWhen(
|
||||
"reply_to_bot",
|
||||
Boolean(params.botId) &&
|
||||
Boolean(params.referencedAuthorId) &&
|
||||
params.referencedAuthorId === params.botId,
|
||||
);
|
||||
|
||||
return {
|
||||
implicitMention,
|
||||
implicitMentionKinds,
|
||||
wasMentioned,
|
||||
};
|
||||
}
|
||||
@@ -887,7 +891,7 @@ export async function preflightDiscordMessage(
|
||||
}
|
||||
|
||||
const mentionText = hasTypedText ? baseText : "";
|
||||
const { implicitMention, wasMentioned } = resolveDiscordMentionState({
|
||||
const { implicitMentionKinds, wasMentioned } = resolveDiscordMentionState({
|
||||
authorIsBot: Boolean(author.bot),
|
||||
botId,
|
||||
hasAnyMention,
|
||||
@@ -946,23 +950,27 @@ export async function preflightDiscordMessage(
|
||||
}
|
||||
|
||||
const canDetectMention = Boolean(botId) || mentionRegexes.length > 0;
|
||||
const mentionGate = resolveMentionGatingWithBypass({
|
||||
isGroup: isGuildMessage,
|
||||
requireMention: Boolean(shouldRequireMention),
|
||||
canDetectMention,
|
||||
wasMentioned,
|
||||
implicitMention,
|
||||
hasAnyMention,
|
||||
allowTextCommands,
|
||||
hasControlCommand: hasControlCommandInMessage,
|
||||
commandAuthorized,
|
||||
const mentionDecision = resolveInboundMentionDecision({
|
||||
facts: {
|
||||
canDetectMention,
|
||||
wasMentioned,
|
||||
hasAnyMention,
|
||||
implicitMentionKinds,
|
||||
},
|
||||
policy: {
|
||||
isGroup: isGuildMessage,
|
||||
requireMention: Boolean(shouldRequireMention),
|
||||
allowTextCommands,
|
||||
hasControlCommand: hasControlCommandInMessage,
|
||||
commandAuthorized,
|
||||
},
|
||||
});
|
||||
const effectiveWasMentioned = mentionGate.effectiveWasMentioned;
|
||||
const effectiveWasMentioned = mentionDecision.effectiveWasMentioned;
|
||||
logDebug(
|
||||
`[discord-preflight] shouldRequireMention=${shouldRequireMention} baseRequireMention=${shouldRequireMentionByConfig} boundThreadSession=${isBoundThreadSession} mentionGate.shouldSkip=${mentionGate.shouldSkip} wasMentioned=${wasMentioned}`,
|
||||
`[discord-preflight] shouldRequireMention=${shouldRequireMention} baseRequireMention=${shouldRequireMentionByConfig} boundThreadSession=${isBoundThreadSession} mentionDecision.shouldSkip=${mentionDecision.shouldSkip} wasMentioned=${wasMentioned}`,
|
||||
);
|
||||
if (isGuildMessage && shouldRequireMention) {
|
||||
if (botId && mentionGate.shouldSkip) {
|
||||
if (botId && mentionDecision.shouldSkip) {
|
||||
logDebug(`[discord-preflight] drop: no-mention`);
|
||||
logVerbose(`discord: drop guild message (mention required, botId=${botId})`);
|
||||
logger.info(
|
||||
@@ -983,7 +991,7 @@ export async function preflightDiscordMessage(
|
||||
}
|
||||
|
||||
if (author.bot && !sender.isPluralKit && allowBotsMode === "mentions") {
|
||||
const botMentioned = isDirectMessage || wasMentioned || implicitMention;
|
||||
const botMentioned = isDirectMessage || wasMentioned || mentionDecision.implicitMention;
|
||||
if (!botMentioned) {
|
||||
logDebug(`[discord-preflight] drop: bot message missing mention (allowBots=mentions)`);
|
||||
logVerbose("discord: drop bot message (allowBots=mentions, missing mention)");
|
||||
@@ -998,7 +1006,7 @@ export async function preflightDiscordMessage(
|
||||
ignoreOtherMentions &&
|
||||
hasUserOrRoleMention &&
|
||||
!wasMentioned &&
|
||||
!implicitMention
|
||||
!mentionDecision.implicitMention
|
||||
) {
|
||||
logDebug(`[discord-preflight] drop: other-mention`);
|
||||
logVerbose(
|
||||
@@ -1103,7 +1111,7 @@ export async function preflightDiscordMessage(
|
||||
shouldRequireMention,
|
||||
hasAnyMention,
|
||||
allowTextCommands,
|
||||
shouldBypassMention: mentionGate.shouldBypassMention,
|
||||
shouldBypassMention: mentionDecision.shouldBypassMention,
|
||||
effectiveWasMentioned,
|
||||
canDetectMention,
|
||||
historyEntry,
|
||||
|
||||
@@ -47,7 +47,7 @@ export {
|
||||
type GoogleChatConfig,
|
||||
} from "openclaw/plugin-sdk/googlechat-runtime-shared";
|
||||
export { extractToolSend } from "openclaw/plugin-sdk/tool-send";
|
||||
export { resolveMentionGatingWithBypass } from "openclaw/plugin-sdk/channel-inbound";
|
||||
export { resolveInboundMentionDecision } from "openclaw/plugin-sdk/channel-inbound";
|
||||
export { resolveInboundRouteEnvelopeBuilderWithRuntime } from "openclaw/plugin-sdk/inbound-envelope";
|
||||
export { resolveWebhookPath } from "openclaw/plugin-sdk/webhook-path";
|
||||
export {
|
||||
|
||||
@@ -6,11 +6,15 @@ const isDangerousNameMatchingEnabled = vi.hoisted(() => vi.fn());
|
||||
const resolveAllowlistProviderRuntimeGroupPolicy = vi.hoisted(() => vi.fn());
|
||||
const resolveDefaultGroupPolicy = vi.hoisted(() => vi.fn());
|
||||
const resolveDmGroupAccessWithLists = vi.hoisted(() => vi.fn());
|
||||
const resolveMentionGatingWithBypass = vi.hoisted(() => vi.fn());
|
||||
const resolveInboundMentionDecision = vi.hoisted(() => vi.fn());
|
||||
const resolveSenderScopedGroupPolicy = vi.hoisted(() => vi.fn());
|
||||
const warnMissingProviderGroupPolicyFallbackOnce = vi.hoisted(() => vi.fn());
|
||||
const sendGoogleChatMessage = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/channel-inbound", () => ({
|
||||
resolveInboundMentionDecision,
|
||||
}));
|
||||
|
||||
vi.mock("../runtime-api.js", () => ({
|
||||
GROUP_POLICY_BLOCKED_LABEL: { space: "space" },
|
||||
createChannelPairingController,
|
||||
@@ -19,7 +23,6 @@ vi.mock("../runtime-api.js", () => ({
|
||||
resolveAllowlistProviderRuntimeGroupPolicy,
|
||||
resolveDefaultGroupPolicy,
|
||||
resolveDmGroupAccessWithLists,
|
||||
resolveMentionGatingWithBypass,
|
||||
resolveSenderScopedGroupPolicy,
|
||||
warnMissingProviderGroupPolicyFallbackOnce,
|
||||
}));
|
||||
@@ -84,7 +87,7 @@ function allowInboundGroupTraffic(options?: {
|
||||
effectiveAllowFrom: [],
|
||||
effectiveGroupAllowFrom: options?.effectiveGroupAllowFrom ?? ["users/alice"],
|
||||
});
|
||||
resolveMentionGatingWithBypass.mockReturnValue({
|
||||
resolveInboundMentionDecision.mockReturnValue({
|
||||
shouldSkip: false,
|
||||
effectiveWasMentioned: options?.effectiveWasMentioned ?? true,
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { resolveInboundMentionDecision } from "openclaw/plugin-sdk/channel-inbound";
|
||||
import {
|
||||
GROUP_POLICY_BLOCKED_LABEL,
|
||||
createChannelPairingController,
|
||||
@@ -6,7 +7,6 @@ import {
|
||||
resolveAllowlistProviderRuntimeGroupPolicy,
|
||||
resolveDefaultGroupPolicy,
|
||||
resolveDmGroupAccessWithLists,
|
||||
resolveMentionGatingWithBypass,
|
||||
resolveSenderScopedGroupPolicy,
|
||||
warnMissingProviderGroupPolicyFallbackOnce,
|
||||
type OpenClawConfig,
|
||||
@@ -321,19 +321,23 @@ export async function applyGoogleChatInboundAccessPolicy(params: {
|
||||
cfg: config,
|
||||
surface: "googlechat",
|
||||
});
|
||||
const mentionGate = resolveMentionGatingWithBypass({
|
||||
isGroup: true,
|
||||
requireMention,
|
||||
canDetectMention: true,
|
||||
wasMentioned: mentionInfo.wasMentioned,
|
||||
implicitMention: false,
|
||||
hasAnyMention: mentionInfo.hasAnyMention,
|
||||
allowTextCommands,
|
||||
hasControlCommand: core.channel.text.hasControlCommand(rawBody, config),
|
||||
commandAuthorized: commandAuthorized === true,
|
||||
const mentionDecision = resolveInboundMentionDecision({
|
||||
facts: {
|
||||
canDetectMention: true,
|
||||
wasMentioned: mentionInfo.wasMentioned,
|
||||
hasAnyMention: mentionInfo.hasAnyMention,
|
||||
implicitMentionKinds: [],
|
||||
},
|
||||
policy: {
|
||||
isGroup: true,
|
||||
requireMention,
|
||||
allowTextCommands,
|
||||
hasControlCommand: core.channel.text.hasControlCommand(rawBody, config),
|
||||
commandAuthorized: commandAuthorized === true,
|
||||
},
|
||||
});
|
||||
effectiveWasMentioned = mentionGate.effectiveWasMentioned;
|
||||
if (mentionGate.shouldSkip) {
|
||||
effectiveWasMentioned = mentionDecision.effectiveWasMentioned;
|
||||
if (mentionDecision.shouldSkip) {
|
||||
logVerbose(`drop group message (mention required, space=${spaceId})`);
|
||||
return { ok: false };
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
logInboundDrop,
|
||||
matchesMentionPatterns,
|
||||
resolveEnvelopeFormatOptions,
|
||||
resolveInboundMentionDecision,
|
||||
} from "openclaw/plugin-sdk/channel-inbound";
|
||||
import { hasControlCommand } from "openclaw/plugin-sdk/command-auth";
|
||||
import { resolveDualTextControlCommandGate } from "openclaw/plugin-sdk/command-auth";
|
||||
@@ -468,10 +469,23 @@ export function resolveIMessageInboundDecision(params: {
|
||||
return { kind: "drop", reason: "control command (unauthorized)" };
|
||||
}
|
||||
|
||||
const shouldBypassMention =
|
||||
isGroup && requireMention && !mentioned && commandAuthorized && hasControlCommandInMessage;
|
||||
const effectiveWasMentioned = mentioned || shouldBypassMention;
|
||||
if (isGroup && requireMention && canDetectMention && !mentioned && !shouldBypassMention) {
|
||||
const mentionDecision = resolveInboundMentionDecision({
|
||||
facts: {
|
||||
canDetectMention,
|
||||
wasMentioned: mentioned,
|
||||
hasAnyMention: false,
|
||||
implicitMentionKinds: [],
|
||||
},
|
||||
policy: {
|
||||
isGroup,
|
||||
requireMention,
|
||||
allowTextCommands: true,
|
||||
hasControlCommand: hasControlCommandInMessage,
|
||||
commandAuthorized,
|
||||
},
|
||||
});
|
||||
const effectiveWasMentioned = mentionDecision.effectiveWasMentioned;
|
||||
if (isGroup && requireMention && canDetectMention && mentionDecision.shouldSkip) {
|
||||
params.logVerbose?.(`imessage: skipping group message (no mention)`);
|
||||
recordPendingHistoryEntryIfEnabled({
|
||||
historyMap: params.groupHistories,
|
||||
|
||||
@@ -11,32 +11,76 @@ type PostbackEvent = webhook.PostbackEvent;
|
||||
vi.mock("openclaw/plugin-sdk/channel-inbound", () => ({
|
||||
buildMentionRegexes: () => [],
|
||||
matchesMentionPatterns: () => false,
|
||||
resolveMentionGatingWithBypass: ({
|
||||
isGroup,
|
||||
requireMention,
|
||||
canDetectMention,
|
||||
wasMentioned,
|
||||
hasAnyMention,
|
||||
allowTextCommands,
|
||||
hasControlCommand,
|
||||
commandAuthorized,
|
||||
}: {
|
||||
isGroup: boolean;
|
||||
requireMention: boolean;
|
||||
canDetectMention: boolean;
|
||||
wasMentioned: boolean;
|
||||
hasAnyMention: boolean;
|
||||
allowTextCommands: boolean;
|
||||
hasControlCommand: boolean;
|
||||
commandAuthorized: boolean;
|
||||
}) => ({
|
||||
shouldSkip:
|
||||
isGroup &&
|
||||
requireMention &&
|
||||
canDetectMention &&
|
||||
!wasMentioned &&
|
||||
!(allowTextCommands && hasControlCommand && commandAuthorized && !hasAnyMention),
|
||||
}),
|
||||
resolveInboundMentionDecision: (params: {
|
||||
facts?: {
|
||||
canDetectMention: boolean;
|
||||
wasMentioned: boolean;
|
||||
hasAnyMention?: boolean;
|
||||
};
|
||||
policy?: {
|
||||
isGroup: boolean;
|
||||
requireMention: boolean;
|
||||
allowTextCommands: boolean;
|
||||
hasControlCommand: boolean;
|
||||
commandAuthorized: boolean;
|
||||
};
|
||||
isGroup?: boolean;
|
||||
requireMention?: boolean;
|
||||
canDetectMention?: boolean;
|
||||
wasMentioned?: boolean;
|
||||
hasAnyMention?: boolean;
|
||||
allowTextCommands?: boolean;
|
||||
hasControlCommand?: boolean;
|
||||
commandAuthorized?: boolean;
|
||||
}) => {
|
||||
const facts =
|
||||
"facts" in params && params.facts
|
||||
? params.facts
|
||||
: {
|
||||
canDetectMention: Boolean(params.canDetectMention),
|
||||
wasMentioned: Boolean(params.wasMentioned),
|
||||
hasAnyMention: params.hasAnyMention,
|
||||
};
|
||||
const policy =
|
||||
"policy" in params && params.policy
|
||||
? params.policy
|
||||
: {
|
||||
isGroup: Boolean(params.isGroup),
|
||||
requireMention: Boolean(params.requireMention),
|
||||
allowTextCommands: Boolean(params.allowTextCommands),
|
||||
hasControlCommand: Boolean(params.hasControlCommand),
|
||||
commandAuthorized: Boolean(params.commandAuthorized),
|
||||
};
|
||||
return {
|
||||
effectiveWasMentioned:
|
||||
facts.wasMentioned ||
|
||||
(policy.allowTextCommands &&
|
||||
policy.hasControlCommand &&
|
||||
policy.commandAuthorized &&
|
||||
!facts.hasAnyMention),
|
||||
shouldSkip:
|
||||
policy.isGroup &&
|
||||
policy.requireMention &&
|
||||
facts.canDetectMention &&
|
||||
!facts.wasMentioned &&
|
||||
!(
|
||||
policy.allowTextCommands &&
|
||||
policy.hasControlCommand &&
|
||||
policy.commandAuthorized &&
|
||||
!facts.hasAnyMention
|
||||
),
|
||||
shouldBypassMention:
|
||||
policy.isGroup &&
|
||||
policy.requireMention &&
|
||||
!facts.wasMentioned &&
|
||||
!facts.hasAnyMention &&
|
||||
policy.allowTextCommands &&
|
||||
policy.hasControlCommand &&
|
||||
policy.commandAuthorized,
|
||||
implicitMention: false,
|
||||
matchedImplicitMentionKinds: [],
|
||||
};
|
||||
},
|
||||
}));
|
||||
vi.mock("openclaw/plugin-sdk/channel-pairing", () => ({
|
||||
createChannelPairingChallengeIssuer:
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { webhook } from "@line/bot-sdk";
|
||||
import {
|
||||
buildMentionRegexes,
|
||||
matchesMentionPatterns,
|
||||
resolveMentionGatingWithBypass,
|
||||
resolveInboundMentionDecision,
|
||||
} from "openclaw/plugin-sdk/channel-inbound";
|
||||
import { createChannelPairingChallengeIssuer } from "openclaw/plugin-sdk/channel-pairing";
|
||||
import { hasControlCommand, resolveControlCommandGate } from "openclaw/plugin-sdk/command-auth";
|
||||
@@ -501,17 +501,22 @@ async function handleMessageEvent(event: MessageEvent, context: LineHandlerConte
|
||||
const wasMentionedByPattern =
|
||||
message.type === "text" ? matchesMentionPatterns(rawText, mentionRegexes) : false;
|
||||
const wasMentioned = wasMentionedByNative || wasMentionedByPattern;
|
||||
const mentionGate = resolveMentionGatingWithBypass({
|
||||
isGroup: true,
|
||||
requireMention,
|
||||
canDetectMention: message.type === "text",
|
||||
wasMentioned,
|
||||
hasAnyMention: hasAnyLineMention(message),
|
||||
allowTextCommands: true,
|
||||
hasControlCommand: hasControlCommand(rawText, cfg),
|
||||
commandAuthorized: decision.commandAuthorized,
|
||||
const mentionDecision = resolveInboundMentionDecision({
|
||||
facts: {
|
||||
canDetectMention: message.type === "text",
|
||||
wasMentioned,
|
||||
hasAnyMention: hasAnyLineMention(message),
|
||||
implicitMentionKinds: [],
|
||||
},
|
||||
policy: {
|
||||
isGroup: true,
|
||||
requireMention,
|
||||
allowTextCommands: true,
|
||||
hasControlCommand: hasControlCommand(rawText, cfg),
|
||||
commandAuthorized: decision.commandAuthorized,
|
||||
},
|
||||
});
|
||||
if (mentionGate.shouldSkip) {
|
||||
if (mentionDecision.shouldSkip) {
|
||||
logVerbose(`line: skipping group message (requireMention, not mentioned)`);
|
||||
const historyKey = groupId ?? roomId;
|
||||
const senderId = sourceInfo.userId ?? "unknown";
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
implicitMentionKindWhen,
|
||||
resolveInboundMentionDecision,
|
||||
} from "openclaw/plugin-sdk/channel-inbound";
|
||||
import { vi } from "vitest";
|
||||
import type { PluginRuntime } from "./runtime-api.js";
|
||||
import { setMatrixRuntime } from "./runtime.js";
|
||||
@@ -56,6 +60,8 @@ export function installMatrixMonitorTestRuntime(
|
||||
options.matchesMentionPatterns ??
|
||||
((text: string, patterns: RegExp[]) => patterns.some((pattern) => pattern.test(text))),
|
||||
matchesMentionWithExplicit: () => false,
|
||||
implicitMentionKindWhen,
|
||||
resolveInboundMentionDecision,
|
||||
},
|
||||
media: {
|
||||
fetchRemoteMedia: vi.fn(),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { resolveInboundMentionDecision } from "openclaw/plugin-sdk/channel-inbound";
|
||||
import {
|
||||
buildPendingHistoryContextFromMap,
|
||||
clearHistoryEntriesIfEnabled,
|
||||
@@ -9,7 +10,6 @@ import {
|
||||
recordPendingHistoryEntryIfEnabled,
|
||||
resolveChannelContextVisibilityMode,
|
||||
resolveDualTextControlCommandGate,
|
||||
resolveMentionGating,
|
||||
resolveInboundSessionEnvelopeContext,
|
||||
shouldIncludeSupplementalContext,
|
||||
formatAllowlistMatchMeta,
|
||||
@@ -160,7 +160,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
||||
text: string;
|
||||
attachments: MSTeamsAttachmentLike[];
|
||||
wasMentioned: boolean;
|
||||
implicitMention: boolean;
|
||||
implicitMentionKinds: Array<"reply_to_bot">;
|
||||
};
|
||||
|
||||
const handleTeamsMessageNow = async (params: MSTeamsDebounceEntry) => {
|
||||
@@ -458,17 +458,24 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
||||
channelConfig,
|
||||
});
|
||||
const timestamp = parseMSTeamsActivityTimestamp(activity.timestamp);
|
||||
|
||||
if (!isDirectMessage) {
|
||||
const mentionGate = resolveMentionGating({
|
||||
requireMention: Boolean(requireMention),
|
||||
const mentionDecision = resolveInboundMentionDecision({
|
||||
facts: {
|
||||
canDetectMention: true,
|
||||
wasMentioned: params.wasMentioned,
|
||||
implicitMention: params.implicitMention,
|
||||
shouldBypassMention: false,
|
||||
});
|
||||
const mentioned = mentionGate.effectiveWasMentioned;
|
||||
if (requireMention && mentionGate.shouldSkip) {
|
||||
implicitMentionKinds: params.implicitMentionKinds,
|
||||
},
|
||||
policy: {
|
||||
isGroup: !isDirectMessage,
|
||||
requireMention: Boolean(requireMention),
|
||||
allowTextCommands: false,
|
||||
hasControlCommand: false,
|
||||
commandAuthorized: false,
|
||||
},
|
||||
});
|
||||
|
||||
if (!isDirectMessage) {
|
||||
const mentioned = mentionDecision.effectiveWasMentioned;
|
||||
if (requireMention && mentionDecision.shouldSkip) {
|
||||
log.debug?.("skipping message (mention required)", {
|
||||
teamId,
|
||||
channelId,
|
||||
@@ -647,7 +654,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
||||
Surface: "msteams" as const,
|
||||
MessageSid: activity.id,
|
||||
Timestamp: timestamp?.getTime() ?? Date.now(),
|
||||
WasMentioned: isDirectMessage || params.wasMentioned || params.implicitMention,
|
||||
WasMentioned: isDirectMessage || mentionDecision.effectiveWasMentioned,
|
||||
CommandAuthorized: commandAuthorized,
|
||||
OriginatingChannel: "msteams" as const,
|
||||
OriginatingTo: teamsTo,
|
||||
@@ -796,14 +803,14 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
const wasMentioned = entries.some((entry) => entry.wasMentioned);
|
||||
const implicitMention = entries.some((entry) => entry.implicitMention);
|
||||
const implicitMentionKinds = entries.flatMap((entry) => entry.implicitMentionKinds);
|
||||
await handleTeamsMessageNow({
|
||||
context: last.context,
|
||||
rawText: combinedRawText,
|
||||
text: combinedText,
|
||||
attachments: [],
|
||||
wasMentioned,
|
||||
implicitMention,
|
||||
implicitMentionKinds,
|
||||
});
|
||||
},
|
||||
onError: (err) => {
|
||||
@@ -822,9 +829,10 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
||||
const wasMentioned = wasMSTeamsBotMentioned(activity);
|
||||
const conversationId = normalizeMSTeamsConversationId(activity.conversation?.id ?? "");
|
||||
const replyToId = activity.replyToId ?? undefined;
|
||||
const implicitMention = Boolean(
|
||||
conversationId && replyToId && wasMSTeamsMessageSent(conversationId, replyToId),
|
||||
);
|
||||
const implicitMentionKinds: Array<"reply_to_bot"> =
|
||||
conversationId && replyToId && wasMSTeamsMessageSent(conversationId, replyToId)
|
||||
? ["reply_to_bot"]
|
||||
: [];
|
||||
|
||||
await inboundDebouncer.enqueue({
|
||||
context,
|
||||
@@ -832,7 +840,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
||||
text,
|
||||
attachments,
|
||||
wasMentioned,
|
||||
implicitMention,
|
||||
implicitMentionKinds,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { resolveInboundMentionDecision } from "openclaw/plugin-sdk/channel-inbound";
|
||||
import type {
|
||||
AllowlistMatch,
|
||||
ChannelGroupContext,
|
||||
@@ -9,7 +10,6 @@ import {
|
||||
evaluateMatchedGroupAccessForPolicy,
|
||||
normalizeChannelSlug,
|
||||
resolveChannelEntryMatchWithFallback,
|
||||
resolveMentionGatingWithBypass,
|
||||
resolveNestedAllowlistDecision,
|
||||
} from "../runtime-api.js";
|
||||
import type { NextcloudTalkRoomConfig } from "./types.js";
|
||||
@@ -167,14 +167,19 @@ export function resolveNextcloudTalkMentionGate(params: {
|
||||
hasControlCommand: boolean;
|
||||
commandAuthorized: boolean;
|
||||
}): { shouldSkip: boolean; shouldBypassMention: boolean } {
|
||||
const result = resolveMentionGatingWithBypass({
|
||||
isGroup: params.isGroup,
|
||||
requireMention: params.requireMention,
|
||||
canDetectMention: true,
|
||||
wasMentioned: params.wasMentioned,
|
||||
allowTextCommands: params.allowTextCommands,
|
||||
hasControlCommand: params.hasControlCommand,
|
||||
commandAuthorized: params.commandAuthorized,
|
||||
const result = resolveInboundMentionDecision({
|
||||
facts: {
|
||||
canDetectMention: true,
|
||||
wasMentioned: params.wasMentioned,
|
||||
implicitMentionKinds: [],
|
||||
},
|
||||
policy: {
|
||||
isGroup: params.isGroup,
|
||||
requireMention: params.requireMention,
|
||||
allowTextCommands: params.allowTextCommands,
|
||||
hasControlCommand: params.hasControlCommand,
|
||||
commandAuthorized: params.commandAuthorized,
|
||||
},
|
||||
});
|
||||
return { shouldSkip: result.shouldSkip, shouldBypassMention: result.shouldBypassMention };
|
||||
}
|
||||
|
||||
@@ -6,13 +6,11 @@ import {
|
||||
formatInboundEnvelope,
|
||||
formatInboundFromLabel,
|
||||
matchesMentionPatterns,
|
||||
resolveInboundMentionDecision,
|
||||
resolveEnvelopeFormatOptions,
|
||||
shouldDebounceTextInbound,
|
||||
} from "openclaw/plugin-sdk/channel-inbound";
|
||||
import {
|
||||
logInboundDrop,
|
||||
resolveMentionGatingWithBypass,
|
||||
} from "openclaw/plugin-sdk/channel-inbound";
|
||||
import { logInboundDrop } from "openclaw/plugin-sdk/channel-inbound";
|
||||
import { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline";
|
||||
import { resolveControlCommandGate } from "openclaw/plugin-sdk/command-auth";
|
||||
import { hasControlCommand } from "openclaw/plugin-sdk/command-auth";
|
||||
@@ -672,19 +670,23 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) {
|
||||
accountId: deps.accountId,
|
||||
});
|
||||
const canDetectMention = mentionRegexes.length > 0;
|
||||
const mentionGate = resolveMentionGatingWithBypass({
|
||||
isGroup,
|
||||
requireMention: Boolean(requireMention),
|
||||
canDetectMention,
|
||||
wasMentioned,
|
||||
implicitMention: false,
|
||||
hasAnyMention: false,
|
||||
allowTextCommands: true,
|
||||
hasControlCommand: hasControlCommandInMessage,
|
||||
commandAuthorized,
|
||||
const mentionDecision = resolveInboundMentionDecision({
|
||||
facts: {
|
||||
canDetectMention,
|
||||
wasMentioned,
|
||||
hasAnyMention: false,
|
||||
implicitMentionKinds: [],
|
||||
},
|
||||
policy: {
|
||||
isGroup,
|
||||
requireMention: Boolean(requireMention),
|
||||
allowTextCommands: true,
|
||||
hasControlCommand: hasControlCommandInMessage,
|
||||
commandAuthorized,
|
||||
},
|
||||
});
|
||||
const effectiveWasMentioned = mentionGate.effectiveWasMentioned;
|
||||
if (isGroup && requireMention && canDetectMention && mentionGate.shouldSkip) {
|
||||
const effectiveWasMentioned = mentionDecision.effectiveWasMentioned;
|
||||
if (isGroup && requireMention && canDetectMention && mentionDecision.shouldSkip) {
|
||||
logInboundDrop({
|
||||
log: logVerbose,
|
||||
channel: "signal",
|
||||
|
||||
@@ -6,10 +6,11 @@ import {
|
||||
import {
|
||||
buildMentionRegexes,
|
||||
formatInboundEnvelope,
|
||||
implicitMentionKindWhen,
|
||||
logInboundDrop,
|
||||
matchesMentionWithExplicit,
|
||||
resolveEnvelopeFormatOptions,
|
||||
resolveMentionGatingWithBypass,
|
||||
resolveInboundMentionDecision,
|
||||
} from "openclaw/plugin-sdk/channel-inbound";
|
||||
import { resolveControlCommandGate } from "openclaw/plugin-sdk/command-auth";
|
||||
import { hasControlCommand } from "openclaw/plugin-sdk/command-auth";
|
||||
@@ -383,14 +384,16 @@ export async function prepareSlackMessage(params: {
|
||||
canResolveExplicit: Boolean(ctx.botUserId),
|
||||
},
|
||||
}));
|
||||
const implicitMention = Boolean(
|
||||
!ctx.threadRequireExplicitMention &&
|
||||
!isDirectMessage &&
|
||||
ctx.botUserId &&
|
||||
message.thread_ts &&
|
||||
(message.parent_user_id === ctx.botUserId ||
|
||||
hasSlackThreadParticipation(account.accountId, message.channel, message.thread_ts)),
|
||||
);
|
||||
const implicitMentionKinds =
|
||||
isDirectMessage || !ctx.botUserId || !message.thread_ts
|
||||
? []
|
||||
: [
|
||||
...implicitMentionKindWhen("reply_to_bot", message.parent_user_id === ctx.botUserId),
|
||||
...implicitMentionKindWhen(
|
||||
"bot_thread_participant",
|
||||
hasSlackThreadParticipation(account.accountId, message.channel, message.thread_ts),
|
||||
),
|
||||
];
|
||||
|
||||
let resolvedSenderName = message.username?.trim() || undefined;
|
||||
const resolveSenderName = async (): Promise<string> => {
|
||||
@@ -492,19 +495,24 @@ export async function prepareSlackMessage(params: {
|
||||
|
||||
// Allow "control commands" to bypass mention gating if sender is authorized.
|
||||
const canDetectMention = Boolean(ctx.botUserId) || mentionRegexes.length > 0;
|
||||
const mentionGate = resolveMentionGatingWithBypass({
|
||||
isGroup: isRoom,
|
||||
requireMention: Boolean(shouldRequireMention),
|
||||
canDetectMention,
|
||||
wasMentioned,
|
||||
implicitMention,
|
||||
hasAnyMention,
|
||||
allowTextCommands,
|
||||
hasControlCommand: hasControlCommandInMessage,
|
||||
commandAuthorized,
|
||||
const mentionDecision = resolveInboundMentionDecision({
|
||||
facts: {
|
||||
canDetectMention,
|
||||
wasMentioned,
|
||||
hasAnyMention,
|
||||
implicitMentionKinds,
|
||||
},
|
||||
policy: {
|
||||
isGroup: isRoom,
|
||||
requireMention: Boolean(shouldRequireMention),
|
||||
allowedImplicitMentionKinds: ctx.threadRequireExplicitMention ? [] : undefined,
|
||||
allowTextCommands,
|
||||
hasControlCommand: hasControlCommandInMessage,
|
||||
commandAuthorized,
|
||||
},
|
||||
});
|
||||
const effectiveWasMentioned = mentionGate.effectiveWasMentioned;
|
||||
if (isRoom && shouldRequireMention && mentionGate.shouldSkip) {
|
||||
const effectiveWasMentioned = mentionDecision.effectiveWasMentioned;
|
||||
if (isRoom && shouldRequireMention && mentionDecision.shouldSkip) {
|
||||
ctx.logger.info({ channel: message.channel, reason: "no-mention" }, "skipping channel message");
|
||||
const pendingText = (message.text ?? "").trim();
|
||||
const fallbackFile = message.files?.[0]?.name
|
||||
@@ -567,7 +575,7 @@ export async function prepareSlackMessage(params: {
|
||||
requireMention: Boolean(shouldRequireMention),
|
||||
canDetectMention,
|
||||
effectiveWasMentioned,
|
||||
shouldBypassMention: mentionGate.shouldBypassMention,
|
||||
shouldBypassMention: mentionDecision.shouldBypassMention,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import {
|
||||
buildMentionRegexes,
|
||||
formatLocationText,
|
||||
implicitMentionKindWhen,
|
||||
logInboundDrop,
|
||||
matchesMentionWithExplicit,
|
||||
resolveMentionGatingWithBypass,
|
||||
resolveInboundMentionDecision,
|
||||
type NormalizedLocation,
|
||||
} from "openclaw/plugin-sdk/channel-inbound";
|
||||
import { resolveControlCommandGate } from "openclaw/plugin-sdk/command-auth-native";
|
||||
@@ -250,21 +251,28 @@ export async function resolveTelegramInboundBody(params: {
|
||||
const replyToBotMessage = botId != null && replyFromId === botId;
|
||||
const isReplyToServiceMessage =
|
||||
replyToBotMessage && isTelegramForumServiceMessage(msg.reply_to_message);
|
||||
const implicitMention = replyToBotMessage && !isReplyToServiceMessage;
|
||||
const implicitMentionKinds = implicitMentionKindWhen(
|
||||
"reply_to_bot",
|
||||
replyToBotMessage && !isReplyToServiceMessage,
|
||||
);
|
||||
const canDetectMention = Boolean(botUsername) || mentionRegexes.length > 0;
|
||||
const mentionGate = resolveMentionGatingWithBypass({
|
||||
isGroup,
|
||||
requireMention: Boolean(requireMention),
|
||||
canDetectMention,
|
||||
wasMentioned,
|
||||
implicitMention: isGroup && Boolean(requireMention) && implicitMention,
|
||||
hasAnyMention,
|
||||
allowTextCommands: true,
|
||||
hasControlCommand: hasControlCommandInMessage,
|
||||
commandAuthorized,
|
||||
const mentionDecision = resolveInboundMentionDecision({
|
||||
facts: {
|
||||
canDetectMention,
|
||||
wasMentioned,
|
||||
hasAnyMention,
|
||||
implicitMentionKinds: isGroup && Boolean(requireMention) ? implicitMentionKinds : [],
|
||||
},
|
||||
policy: {
|
||||
isGroup,
|
||||
requireMention: Boolean(requireMention),
|
||||
allowTextCommands: true,
|
||||
hasControlCommand: hasControlCommandInMessage,
|
||||
commandAuthorized,
|
||||
},
|
||||
});
|
||||
const effectiveWasMentioned = mentionGate.effectiveWasMentioned;
|
||||
if (isGroup && requireMention && canDetectMention && mentionGate.shouldSkip) {
|
||||
const effectiveWasMentioned = mentionDecision.effectiveWasMentioned;
|
||||
if (isGroup && requireMention && canDetectMention && mentionDecision.shouldSkip) {
|
||||
logger.info({ chatId, reason: "no-mention" }, "skipping group message");
|
||||
recordPendingHistoryEntryIfEnabled({
|
||||
historyMap: groupHistories,
|
||||
@@ -331,7 +339,7 @@ export async function resolveTelegramInboundBody(params: {
|
||||
commandAuthorized,
|
||||
effectiveWasMentioned,
|
||||
canDetectMention,
|
||||
shouldBypassMention: mentionGate.shouldBypassMention,
|
||||
shouldBypassMention: mentionDecision.shouldBypassMention,
|
||||
stickerCacheHit,
|
||||
locationData: locationData ?? undefined,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
export { resolveMentionGating } from "openclaw/plugin-sdk/channel-inbound";
|
||||
export {
|
||||
implicitMentionKindWhen,
|
||||
resolveInboundMentionDecision,
|
||||
} from "openclaw/plugin-sdk/channel-inbound";
|
||||
export { hasControlCommand } from "openclaw/plugin-sdk/command-detection";
|
||||
export { recordPendingHistoryEntryIfEnabled } from "openclaw/plugin-sdk/reply-history";
|
||||
export { parseActivationCommand } from "openclaw/plugin-sdk/reply-runtime";
|
||||
|
||||
@@ -13,10 +13,11 @@ import { stripMentionsForCommand } from "./commands.js";
|
||||
import { resolveGroupActivationFor, resolveGroupPolicyFor } from "./group-activation.js";
|
||||
import {
|
||||
hasControlCommand,
|
||||
implicitMentionKindWhen,
|
||||
normalizeE164,
|
||||
parseActivationCommand,
|
||||
recordPendingHistoryEntryIfEnabled,
|
||||
resolveMentionGating,
|
||||
resolveInboundMentionDecision,
|
||||
} from "./group-gating.runtime.js";
|
||||
import { noteGroupMember } from "./group-members.js";
|
||||
|
||||
@@ -152,16 +153,28 @@ export function applyGroupGating(params: ApplyGroupGatingParams) {
|
||||
// should not count as implicit bot mentions unless the message explicitly
|
||||
// mentioned the bot in text.
|
||||
const implicitReplyToSelf = sharedNumberSelfChat && identitiesOverlap(self, sender);
|
||||
const implicitMention = !implicitReplyToSelf && identitiesOverlap(self, replyContext?.sender);
|
||||
const mentionGate = resolveMentionGating({
|
||||
requireMention,
|
||||
canDetectMention: true,
|
||||
wasMentioned,
|
||||
implicitMention,
|
||||
shouldBypassMention,
|
||||
const implicitMentionKinds = implicitMentionKindWhen(
|
||||
"quoted_bot",
|
||||
!implicitReplyToSelf && identitiesOverlap(self, replyContext?.sender),
|
||||
);
|
||||
const mentionDecision = resolveInboundMentionDecision({
|
||||
facts: {
|
||||
canDetectMention: true,
|
||||
wasMentioned,
|
||||
implicitMentionKinds,
|
||||
},
|
||||
policy: {
|
||||
isGroup: true,
|
||||
requireMention,
|
||||
allowTextCommands: false,
|
||||
hasControlCommand: false,
|
||||
commandAuthorized: false,
|
||||
},
|
||||
});
|
||||
params.msg.wasMentioned = mentionGate.effectiveWasMentioned;
|
||||
if (!shouldBypassMention && requireMention && mentionGate.shouldSkip) {
|
||||
const effectiveWasMentioned =
|
||||
mentionDecision.effectiveWasMentioned || Boolean(shouldBypassMention);
|
||||
params.msg.wasMentioned = effectiveWasMentioned;
|
||||
if (!shouldBypassMention && requireMention && mentionDecision.shouldSkip) {
|
||||
return skipGroupMessageAndStoreHistory(
|
||||
params,
|
||||
`Group message stored for context (no mention detected) in ${params.conversationId}: ${params.msg.body}`,
|
||||
|
||||
@@ -409,17 +409,54 @@ vi.mock("./auto-reply/monitor/group-gating.runtime.js", () => ({
|
||||
const next = [...current, params.entry].slice(-params.limit);
|
||||
params.historyMap.set(params.historyKey, next);
|
||||
},
|
||||
resolveMentionGating: (params: {
|
||||
requireMention: boolean;
|
||||
wasMentioned: boolean;
|
||||
implicitMention?: boolean;
|
||||
shouldBypassMention?: boolean;
|
||||
resolveInboundMentionDecision: (params: {
|
||||
facts?: {
|
||||
canDetectMention: boolean;
|
||||
wasMentioned: boolean;
|
||||
implicitMentionKinds?: string[];
|
||||
};
|
||||
policy?: {
|
||||
isGroup: boolean;
|
||||
requireMention: boolean;
|
||||
allowTextCommands: boolean;
|
||||
hasControlCommand: boolean;
|
||||
commandAuthorized: boolean;
|
||||
};
|
||||
isGroup?: boolean;
|
||||
requireMention?: boolean;
|
||||
canDetectMention?: boolean;
|
||||
wasMentioned?: boolean;
|
||||
implicitMentionKinds?: string[];
|
||||
allowTextCommands?: boolean;
|
||||
hasControlCommand?: boolean;
|
||||
commandAuthorized?: boolean;
|
||||
}) => {
|
||||
const effectiveWasMentioned =
|
||||
params.wasMentioned || Boolean(params.implicitMention) || Boolean(params.shouldBypassMention);
|
||||
const facts =
|
||||
"facts" in params && params.facts
|
||||
? params.facts
|
||||
: {
|
||||
canDetectMention: Boolean(params.canDetectMention),
|
||||
wasMentioned: Boolean(params.wasMentioned),
|
||||
implicitMentionKinds: params.implicitMentionKinds,
|
||||
};
|
||||
const policy =
|
||||
"policy" in params && params.policy
|
||||
? params.policy
|
||||
: {
|
||||
isGroup: Boolean(params.isGroup),
|
||||
requireMention: Boolean(params.requireMention),
|
||||
allowTextCommands: Boolean(params.allowTextCommands),
|
||||
hasControlCommand: Boolean(params.hasControlCommand),
|
||||
commandAuthorized: Boolean(params.commandAuthorized),
|
||||
};
|
||||
const effectiveWasMentioned = facts.wasMentioned || Boolean(facts.implicitMentionKinds?.length);
|
||||
return {
|
||||
effectiveWasMentioned,
|
||||
shouldSkip: params.requireMention && !effectiveWasMentioned,
|
||||
shouldSkip:
|
||||
policy.isGroup && policy.requireMention && facts.canDetectMention && !effectiveWasMentioned,
|
||||
shouldBypassMention: false,
|
||||
implicitMention: Boolean(facts.implicitMentionKinds?.length),
|
||||
matchedImplicitMentionKinds: facts.implicitMentionKinds ?? [],
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -41,7 +41,7 @@ export {
|
||||
summarizeMapping,
|
||||
formatAllowFromLowercase,
|
||||
} from "openclaw/plugin-sdk/allow-from";
|
||||
export { resolveMentionGatingWithBypass } from "openclaw/plugin-sdk/channel-inbound";
|
||||
export { resolveInboundMentionDecision } from "openclaw/plugin-sdk/channel-inbound";
|
||||
export { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
|
||||
export { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline";
|
||||
export { buildBaseAccountStatusSnapshot } from "openclaw/plugin-sdk/status-helpers";
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { mergeAllowlist, summarizeMapping } from "openclaw/plugin-sdk/allow-from";
|
||||
import { resolveMentionGatingWithBypass } from "openclaw/plugin-sdk/channel-inbound";
|
||||
import {
|
||||
implicitMentionKindWhen,
|
||||
resolveInboundMentionDecision,
|
||||
} from "openclaw/plugin-sdk/channel-inbound";
|
||||
import { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
|
||||
import {
|
||||
DM_GROUP_ACCESS_REASON,
|
||||
@@ -488,28 +491,32 @@ async function processMessage(
|
||||
})
|
||||
: true;
|
||||
const canDetectMention = mentionRegexes.length > 0 || explicitMention.canResolveExplicit;
|
||||
const mentionGate = resolveMentionGatingWithBypass({
|
||||
isGroup,
|
||||
requireMention,
|
||||
canDetectMention,
|
||||
wasMentioned,
|
||||
implicitMention: message.implicitMention === true,
|
||||
hasAnyMention: explicitMention.hasAnyMention,
|
||||
allowTextCommands: core.channel.commands.shouldHandleTextCommands({
|
||||
cfg: config,
|
||||
surface: "zalouser",
|
||||
}),
|
||||
hasControlCommand,
|
||||
commandAuthorized: commandAuthorized === true,
|
||||
const mentionDecision = resolveInboundMentionDecision({
|
||||
facts: {
|
||||
canDetectMention,
|
||||
wasMentioned,
|
||||
hasAnyMention: explicitMention.hasAnyMention,
|
||||
implicitMentionKinds: implicitMentionKindWhen("quoted_bot", message.implicitMention === true),
|
||||
},
|
||||
policy: {
|
||||
isGroup,
|
||||
requireMention,
|
||||
allowTextCommands: core.channel.commands.shouldHandleTextCommands({
|
||||
cfg: config,
|
||||
surface: "zalouser",
|
||||
}),
|
||||
hasControlCommand,
|
||||
commandAuthorized: commandAuthorized === true,
|
||||
},
|
||||
});
|
||||
if (isGroup && requireMention && !canDetectMention && !mentionGate.effectiveWasMentioned) {
|
||||
if (isGroup && requireMention && !canDetectMention && !mentionDecision.effectiveWasMentioned) {
|
||||
runtime.error?.(
|
||||
`[${account.accountId}] zalouser mention required but detection unavailable ` +
|
||||
`(missing mention regexes and bot self id); dropping group ${chatId}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (isGroup && mentionGate.shouldSkip) {
|
||||
if (isGroup && mentionDecision.shouldSkip) {
|
||||
recordPendingHistoryEntryIfEnabled({
|
||||
historyMap: historyState.groupHistories,
|
||||
historyKey: historyKey ?? "",
|
||||
@@ -605,7 +612,7 @@ async function processMessage(
|
||||
GroupMembers: isGroup ? groupMembers : undefined,
|
||||
SenderName: senderName || undefined,
|
||||
SenderId: senderId,
|
||||
WasMentioned: isGroup ? mentionGate.effectiveWasMentioned : undefined,
|
||||
WasMentioned: isGroup ? mentionDecision.effectiveWasMentioned : undefined,
|
||||
CommandAuthorized: commandAuthorized,
|
||||
Provider: "zalouser",
|
||||
Surface: "zalouser",
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveMentionGating, resolveMentionGatingWithBypass } from "./mention-gating.js";
|
||||
import {
|
||||
implicitMentionKindWhen,
|
||||
resolveInboundMentionDecision,
|
||||
resolveMentionGating,
|
||||
resolveMentionGatingWithBypass,
|
||||
} from "./mention-gating.js";
|
||||
|
||||
describe("resolveMentionGating", () => {
|
||||
it("combines explicit, implicit, and bypass mentions", () => {
|
||||
@@ -65,3 +70,192 @@ describe("resolveMentionGatingWithBypass", () => {
|
||||
expect(res.shouldSkip).toBe(shouldSkip);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveInboundMentionDecision", () => {
|
||||
it("allows matching implicit mention kinds by default", () => {
|
||||
const res = resolveInboundMentionDecision({
|
||||
facts: {
|
||||
canDetectMention: true,
|
||||
wasMentioned: false,
|
||||
implicitMentionKinds: ["reply_to_bot"],
|
||||
},
|
||||
policy: {
|
||||
isGroup: true,
|
||||
requireMention: true,
|
||||
allowTextCommands: true,
|
||||
hasControlCommand: false,
|
||||
commandAuthorized: false,
|
||||
},
|
||||
});
|
||||
expect(res.implicitMention).toBe(true);
|
||||
expect(res.matchedImplicitMentionKinds).toEqual(["reply_to_bot"]);
|
||||
expect(res.effectiveWasMentioned).toBe(true);
|
||||
expect(res.shouldSkip).toBe(false);
|
||||
});
|
||||
|
||||
it("filters implicit mention kinds through the allowlist", () => {
|
||||
const res = resolveInboundMentionDecision({
|
||||
facts: {
|
||||
canDetectMention: true,
|
||||
wasMentioned: false,
|
||||
implicitMentionKinds: ["reply_to_bot", "bot_thread_participant"],
|
||||
},
|
||||
policy: {
|
||||
isGroup: true,
|
||||
requireMention: true,
|
||||
allowedImplicitMentionKinds: ["reply_to_bot"],
|
||||
allowTextCommands: true,
|
||||
hasControlCommand: false,
|
||||
commandAuthorized: false,
|
||||
},
|
||||
});
|
||||
expect(res.implicitMention).toBe(true);
|
||||
expect(res.matchedImplicitMentionKinds).toEqual(["reply_to_bot"]);
|
||||
expect(res.shouldSkip).toBe(false);
|
||||
});
|
||||
|
||||
it("blocks implicit mention kinds excluded by policy", () => {
|
||||
const res = resolveInboundMentionDecision({
|
||||
facts: {
|
||||
canDetectMention: true,
|
||||
wasMentioned: false,
|
||||
implicitMentionKinds: ["reply_to_bot"],
|
||||
},
|
||||
policy: {
|
||||
isGroup: true,
|
||||
requireMention: true,
|
||||
allowedImplicitMentionKinds: [],
|
||||
allowTextCommands: true,
|
||||
hasControlCommand: false,
|
||||
commandAuthorized: false,
|
||||
},
|
||||
});
|
||||
expect(res.implicitMention).toBe(false);
|
||||
expect(res.matchedImplicitMentionKinds).toEqual([]);
|
||||
expect(res.effectiveWasMentioned).toBe(false);
|
||||
expect(res.shouldSkip).toBe(true);
|
||||
});
|
||||
|
||||
it("dedupes repeated implicit mention kinds", () => {
|
||||
const res = resolveInboundMentionDecision({
|
||||
facts: {
|
||||
canDetectMention: true,
|
||||
wasMentioned: false,
|
||||
implicitMentionKinds: ["reply_to_bot", "reply_to_bot", "native"],
|
||||
},
|
||||
policy: {
|
||||
isGroup: true,
|
||||
requireMention: true,
|
||||
allowTextCommands: true,
|
||||
hasControlCommand: false,
|
||||
commandAuthorized: false,
|
||||
},
|
||||
});
|
||||
expect(res.matchedImplicitMentionKinds).toEqual(["reply_to_bot", "native"]);
|
||||
});
|
||||
|
||||
it("keeps command bypass behavior unchanged", () => {
|
||||
const res = resolveInboundMentionDecision({
|
||||
facts: {
|
||||
canDetectMention: true,
|
||||
wasMentioned: false,
|
||||
hasAnyMention: false,
|
||||
implicitMentionKinds: [],
|
||||
},
|
||||
policy: {
|
||||
isGroup: true,
|
||||
requireMention: true,
|
||||
allowTextCommands: true,
|
||||
hasControlCommand: true,
|
||||
commandAuthorized: true,
|
||||
},
|
||||
});
|
||||
expect(res.shouldBypassMention).toBe(true);
|
||||
expect(res.effectiveWasMentioned).toBe(true);
|
||||
expect(res.shouldSkip).toBe(false);
|
||||
});
|
||||
|
||||
it("does not allow command bypass when some other mention is present", () => {
|
||||
const res = resolveInboundMentionDecision({
|
||||
facts: {
|
||||
canDetectMention: true,
|
||||
wasMentioned: false,
|
||||
hasAnyMention: true,
|
||||
implicitMentionKinds: [],
|
||||
},
|
||||
policy: {
|
||||
isGroup: true,
|
||||
requireMention: true,
|
||||
allowTextCommands: true,
|
||||
hasControlCommand: true,
|
||||
commandAuthorized: true,
|
||||
},
|
||||
});
|
||||
expect(res.shouldBypassMention).toBe(false);
|
||||
expect(res.effectiveWasMentioned).toBe(false);
|
||||
expect(res.shouldSkip).toBe(true);
|
||||
});
|
||||
|
||||
it("does not allow command bypass outside groups", () => {
|
||||
const res = resolveInboundMentionDecision({
|
||||
facts: {
|
||||
canDetectMention: true,
|
||||
wasMentioned: false,
|
||||
hasAnyMention: false,
|
||||
implicitMentionKinds: [],
|
||||
},
|
||||
policy: {
|
||||
isGroup: false,
|
||||
requireMention: true,
|
||||
allowTextCommands: true,
|
||||
hasControlCommand: true,
|
||||
commandAuthorized: true,
|
||||
},
|
||||
});
|
||||
expect(res.shouldBypassMention).toBe(false);
|
||||
expect(res.effectiveWasMentioned).toBe(false);
|
||||
expect(res.shouldSkip).toBe(true);
|
||||
});
|
||||
|
||||
it("does not skip when mention detection is unavailable", () => {
|
||||
const res = resolveInboundMentionDecision({
|
||||
facts: {
|
||||
canDetectMention: false,
|
||||
wasMentioned: false,
|
||||
implicitMentionKinds: [],
|
||||
},
|
||||
policy: {
|
||||
isGroup: true,
|
||||
requireMention: true,
|
||||
allowTextCommands: true,
|
||||
hasControlCommand: false,
|
||||
commandAuthorized: false,
|
||||
},
|
||||
});
|
||||
expect(res.shouldSkip).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps the flat call shape for compatibility", () => {
|
||||
const res = resolveInboundMentionDecision({
|
||||
isGroup: true,
|
||||
requireMention: true,
|
||||
canDetectMention: true,
|
||||
wasMentioned: false,
|
||||
implicitMentionKinds: ["reply_to_bot"],
|
||||
allowTextCommands: true,
|
||||
hasControlCommand: false,
|
||||
commandAuthorized: false,
|
||||
});
|
||||
expect(res.effectiveWasMentioned).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("implicitMentionKindWhen", () => {
|
||||
it("returns a one-item list when enabled", () => {
|
||||
expect(implicitMentionKindWhen("reply_to_bot", true)).toEqual(["reply_to_bot"]);
|
||||
});
|
||||
|
||||
it("returns an empty list when disabled", () => {
|
||||
expect(implicitMentionKindWhen("reply_to_bot", false)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/** @deprecated Prefer `resolveInboundMentionDecision({ facts, policy })`. */
|
||||
export type MentionGateParams = {
|
||||
requireMention: boolean;
|
||||
canDetectMention: boolean;
|
||||
@@ -6,11 +7,13 @@ export type MentionGateParams = {
|
||||
shouldBypassMention?: boolean;
|
||||
};
|
||||
|
||||
/** @deprecated Prefer `InboundMentionDecision`. */
|
||||
export type MentionGateResult = {
|
||||
effectiveWasMentioned: boolean;
|
||||
shouldSkip: boolean;
|
||||
};
|
||||
|
||||
/** @deprecated Prefer `resolveInboundMentionDecision({ facts, policy })`. */
|
||||
export type MentionGateWithBypassParams = {
|
||||
isGroup: boolean;
|
||||
requireMention: boolean;
|
||||
@@ -23,37 +26,207 @@ export type MentionGateWithBypassParams = {
|
||||
commandAuthorized: boolean;
|
||||
};
|
||||
|
||||
/** @deprecated Prefer `InboundMentionDecision`. */
|
||||
export type MentionGateWithBypassResult = MentionGateResult & {
|
||||
shouldBypassMention: boolean;
|
||||
};
|
||||
|
||||
export function resolveMentionGating(params: MentionGateParams): MentionGateResult {
|
||||
const implicit = params.implicitMention === true;
|
||||
const bypass = params.shouldBypassMention === true;
|
||||
const effectiveWasMentioned = params.wasMentioned || implicit || bypass;
|
||||
const shouldSkip = params.requireMention && params.canDetectMention && !effectiveWasMentioned;
|
||||
return { effectiveWasMentioned, shouldSkip };
|
||||
export type InboundImplicitMentionKind =
|
||||
| "reply_to_bot"
|
||||
| "quoted_bot"
|
||||
| "bot_thread_participant"
|
||||
| "native";
|
||||
|
||||
export type InboundMentionFacts = {
|
||||
canDetectMention: boolean;
|
||||
wasMentioned: boolean;
|
||||
hasAnyMention?: boolean;
|
||||
implicitMentionKinds?: readonly InboundImplicitMentionKind[];
|
||||
};
|
||||
|
||||
export type InboundMentionPolicy = {
|
||||
isGroup: boolean;
|
||||
requireMention: boolean;
|
||||
allowedImplicitMentionKinds?: readonly InboundImplicitMentionKind[];
|
||||
allowTextCommands: boolean;
|
||||
hasControlCommand: boolean;
|
||||
commandAuthorized: boolean;
|
||||
};
|
||||
|
||||
/** @deprecated Prefer the nested `{ facts, policy }` call shape for new code. */
|
||||
export type ResolveInboundMentionDecisionFlatParams = InboundMentionFacts & InboundMentionPolicy;
|
||||
|
||||
export type ResolveInboundMentionDecisionNestedParams = {
|
||||
facts: InboundMentionFacts;
|
||||
policy: InboundMentionPolicy;
|
||||
};
|
||||
|
||||
export type ResolveInboundMentionDecisionParams =
|
||||
| ResolveInboundMentionDecisionFlatParams
|
||||
| ResolveInboundMentionDecisionNestedParams;
|
||||
|
||||
export type InboundMentionDecision = MentionGateResult & {
|
||||
implicitMention: boolean;
|
||||
matchedImplicitMentionKinds: InboundImplicitMentionKind[];
|
||||
shouldBypassMention: boolean;
|
||||
};
|
||||
|
||||
export function implicitMentionKindWhen(
|
||||
kind: InboundImplicitMentionKind,
|
||||
enabled: boolean,
|
||||
): InboundImplicitMentionKind[] {
|
||||
return enabled ? [kind] : [];
|
||||
}
|
||||
|
||||
function resolveMatchedImplicitMentionKinds(params: {
|
||||
implicitMentionKinds?: readonly InboundImplicitMentionKind[];
|
||||
allowedImplicitMentionKinds?: readonly InboundImplicitMentionKind[];
|
||||
}): InboundImplicitMentionKind[] {
|
||||
const inputKinds = params.implicitMentionKinds ?? [];
|
||||
if (inputKinds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const allowedKinds = params.allowedImplicitMentionKinds
|
||||
? new Set(params.allowedImplicitMentionKinds)
|
||||
: null;
|
||||
const matched: InboundImplicitMentionKind[] = [];
|
||||
for (const kind of inputKinds) {
|
||||
if (allowedKinds && !allowedKinds.has(kind)) {
|
||||
continue;
|
||||
}
|
||||
if (!matched.includes(kind)) {
|
||||
matched.push(kind);
|
||||
}
|
||||
}
|
||||
return matched;
|
||||
}
|
||||
|
||||
function resolveMentionDecisionCore(params: {
|
||||
requireMention: boolean;
|
||||
canDetectMention: boolean;
|
||||
wasMentioned: boolean;
|
||||
implicitMentionKinds?: readonly InboundImplicitMentionKind[];
|
||||
allowedImplicitMentionKinds?: readonly InboundImplicitMentionKind[];
|
||||
shouldBypassMention: boolean;
|
||||
}): InboundMentionDecision {
|
||||
const matchedImplicitMentionKinds = resolveMatchedImplicitMentionKinds({
|
||||
implicitMentionKinds: params.implicitMentionKinds,
|
||||
allowedImplicitMentionKinds: params.allowedImplicitMentionKinds,
|
||||
});
|
||||
const implicitMention = matchedImplicitMentionKinds.length > 0;
|
||||
const effectiveWasMentioned =
|
||||
params.wasMentioned || implicitMention || params.shouldBypassMention;
|
||||
const shouldSkip = params.requireMention && params.canDetectMention && !effectiveWasMentioned;
|
||||
return {
|
||||
implicitMention,
|
||||
matchedImplicitMentionKinds,
|
||||
effectiveWasMentioned,
|
||||
shouldBypassMention: params.shouldBypassMention,
|
||||
shouldSkip,
|
||||
};
|
||||
}
|
||||
|
||||
function hasNestedMentionDecisionParams(
|
||||
params: ResolveInboundMentionDecisionParams,
|
||||
): params is ResolveInboundMentionDecisionNestedParams {
|
||||
return "facts" in params && "policy" in params;
|
||||
}
|
||||
|
||||
function normalizeMentionDecisionParams(
|
||||
params: ResolveInboundMentionDecisionParams,
|
||||
): ResolveInboundMentionDecisionNestedParams {
|
||||
if (hasNestedMentionDecisionParams(params)) {
|
||||
return params;
|
||||
}
|
||||
const {
|
||||
canDetectMention,
|
||||
wasMentioned,
|
||||
hasAnyMention,
|
||||
implicitMentionKinds,
|
||||
isGroup,
|
||||
requireMention,
|
||||
allowedImplicitMentionKinds,
|
||||
allowTextCommands,
|
||||
hasControlCommand,
|
||||
commandAuthorized,
|
||||
} = params;
|
||||
return {
|
||||
facts: {
|
||||
canDetectMention,
|
||||
wasMentioned,
|
||||
hasAnyMention,
|
||||
implicitMentionKinds,
|
||||
},
|
||||
policy: {
|
||||
isGroup,
|
||||
requireMention,
|
||||
allowedImplicitMentionKinds,
|
||||
allowTextCommands,
|
||||
hasControlCommand,
|
||||
commandAuthorized,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveInboundMentionDecision(
|
||||
params: ResolveInboundMentionDecisionParams,
|
||||
): InboundMentionDecision {
|
||||
const { facts, policy } = normalizeMentionDecisionParams(params);
|
||||
const shouldBypassMention =
|
||||
policy.isGroup &&
|
||||
policy.requireMention &&
|
||||
!facts.wasMentioned &&
|
||||
!(facts.hasAnyMention ?? false) &&
|
||||
policy.allowTextCommands &&
|
||||
policy.commandAuthorized &&
|
||||
policy.hasControlCommand;
|
||||
return resolveMentionDecisionCore({
|
||||
requireMention: policy.requireMention,
|
||||
canDetectMention: facts.canDetectMention,
|
||||
wasMentioned: facts.wasMentioned,
|
||||
implicitMentionKinds: facts.implicitMentionKinds,
|
||||
allowedImplicitMentionKinds: policy.allowedImplicitMentionKinds,
|
||||
shouldBypassMention,
|
||||
});
|
||||
}
|
||||
|
||||
/** @deprecated Prefer `resolveInboundMentionDecision({ facts, policy })`. */
|
||||
export function resolveMentionGating(params: MentionGateParams): MentionGateResult {
|
||||
const result = resolveMentionDecisionCore({
|
||||
requireMention: params.requireMention,
|
||||
canDetectMention: params.canDetectMention,
|
||||
wasMentioned: params.wasMentioned,
|
||||
implicitMentionKinds: implicitMentionKindWhen("native", params.implicitMention === true),
|
||||
shouldBypassMention: params.shouldBypassMention === true,
|
||||
});
|
||||
return {
|
||||
effectiveWasMentioned: result.effectiveWasMentioned,
|
||||
shouldSkip: result.shouldSkip,
|
||||
};
|
||||
}
|
||||
|
||||
/** @deprecated Prefer `resolveInboundMentionDecision({ facts, policy })`. */
|
||||
export function resolveMentionGatingWithBypass(
|
||||
params: MentionGateWithBypassParams,
|
||||
): MentionGateWithBypassResult {
|
||||
const shouldBypassMention =
|
||||
params.isGroup &&
|
||||
params.requireMention &&
|
||||
!params.wasMentioned &&
|
||||
!(params.hasAnyMention ?? false) &&
|
||||
params.allowTextCommands &&
|
||||
params.commandAuthorized &&
|
||||
params.hasControlCommand;
|
||||
return {
|
||||
...resolveMentionGating({
|
||||
requireMention: params.requireMention,
|
||||
const result = resolveInboundMentionDecision({
|
||||
facts: {
|
||||
canDetectMention: params.canDetectMention,
|
||||
wasMentioned: params.wasMentioned,
|
||||
implicitMention: params.implicitMention,
|
||||
shouldBypassMention,
|
||||
}),
|
||||
shouldBypassMention,
|
||||
hasAnyMention: params.hasAnyMention,
|
||||
implicitMentionKinds: implicitMentionKindWhen("native", params.implicitMention === true),
|
||||
},
|
||||
policy: {
|
||||
isGroup: params.isGroup,
|
||||
requireMention: params.requireMention,
|
||||
allowTextCommands: params.allowTextCommands,
|
||||
hasControlCommand: params.hasControlCommand,
|
||||
commandAuthorized: params.commandAuthorized,
|
||||
},
|
||||
});
|
||||
return {
|
||||
effectiveWasMentioned: result.effectiveWasMentioned,
|
||||
shouldSkip: result.shouldSkip,
|
||||
shouldBypassMention: result.shouldBypassMention,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -26,13 +26,24 @@ export {
|
||||
shouldDebounceTextInbound,
|
||||
} from "../channels/inbound-debounce-policy.js";
|
||||
export type {
|
||||
InboundMentionFacts,
|
||||
InboundMentionPolicy,
|
||||
InboundImplicitMentionKind,
|
||||
InboundMentionDecision,
|
||||
MentionGateParams,
|
||||
MentionGateResult,
|
||||
MentionGateWithBypassParams,
|
||||
MentionGateWithBypassResult,
|
||||
ResolveInboundMentionDecisionFlatParams,
|
||||
ResolveInboundMentionDecisionNestedParams,
|
||||
ResolveInboundMentionDecisionParams,
|
||||
} from "../channels/mention-gating.js";
|
||||
export {
|
||||
implicitMentionKindWhen,
|
||||
resolveInboundMentionDecision,
|
||||
// @deprecated Prefer `resolveInboundMentionDecision({ facts, policy })`.
|
||||
resolveMentionGating,
|
||||
// @deprecated Prefer `resolveInboundMentionDecision({ facts, policy })`.
|
||||
resolveMentionGatingWithBypass,
|
||||
} from "../channels/mention-gating.js";
|
||||
export type { NormalizedLocation } from "../channels/location.js";
|
||||
|
||||
@@ -11,7 +11,11 @@ export {
|
||||
readReactionParams,
|
||||
readStringParam,
|
||||
} from "../agents/tools/common.js";
|
||||
export { resolveMentionGatingWithBypass } from "../channels/mention-gating.js";
|
||||
export {
|
||||
resolveMentionGating,
|
||||
resolveMentionGatingWithBypass,
|
||||
resolveInboundMentionDecision,
|
||||
} from "../channels/mention-gating.js";
|
||||
export {
|
||||
deleteAccountFromConfigSection,
|
||||
setAccountEnabledInConfigSection,
|
||||
|
||||
@@ -19,7 +19,11 @@ export {
|
||||
resolveDualTextControlCommandGate,
|
||||
} from "../channels/command-gating.js";
|
||||
export { logInboundDrop, logTypingFailure } from "../channels/logging.js";
|
||||
export { resolveMentionGating } from "../channels/mention-gating.js";
|
||||
export {
|
||||
resolveInboundMentionDecision,
|
||||
resolveMentionGating,
|
||||
resolveMentionGatingWithBypass,
|
||||
} from "../channels/mention-gating.js";
|
||||
export type { AllowlistMatch } from "../channels/plugins/allowlist-match.js";
|
||||
export {
|
||||
formatAllowlistMatchMeta,
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
|
||||
export { logInboundDrop } from "../channels/logging.js";
|
||||
export { createAuthRateLimiter } from "../gateway/auth-rate-limit.js";
|
||||
export { resolveMentionGatingWithBypass } from "../channels/mention-gating.js";
|
||||
export {
|
||||
resolveMentionGating,
|
||||
resolveMentionGatingWithBypass,
|
||||
resolveInboundMentionDecision,
|
||||
} from "../channels/mention-gating.js";
|
||||
export type { AllowlistMatch } from "../channels/plugins/allowlist-match.js";
|
||||
export {
|
||||
buildChannelKeyCandidates,
|
||||
|
||||
@@ -5,7 +5,11 @@ import { createOptionalChannelSetupSurface } from "./channel-setup.js";
|
||||
|
||||
export type { ReplyPayload } from "../auto-reply/types.js";
|
||||
export { mergeAllowlist, summarizeMapping } from "../channels/allowlists/resolve-utils.js";
|
||||
export { resolveMentionGatingWithBypass } from "../channels/mention-gating.js";
|
||||
export {
|
||||
resolveMentionGating,
|
||||
resolveMentionGatingWithBypass,
|
||||
resolveInboundMentionDecision,
|
||||
} from "../channels/mention-gating.js";
|
||||
export {
|
||||
deleteAccountFromConfigSection,
|
||||
setAccountEnabledInConfigSection,
|
||||
|
||||
@@ -82,7 +82,7 @@ const RUNTIME_API_EXPORT_GUARDS: Record<string, readonly string[]> = {
|
||||
'export { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";',
|
||||
'export { GoogleChatConfigSchema, type GoogleChatAccountConfig, type GoogleChatConfig } from "openclaw/plugin-sdk/googlechat-runtime-shared";',
|
||||
'export { extractToolSend } from "openclaw/plugin-sdk/tool-send";',
|
||||
'export { resolveMentionGatingWithBypass } from "openclaw/plugin-sdk/channel-inbound";',
|
||||
'export { resolveInboundMentionDecision } from "openclaw/plugin-sdk/channel-inbound";',
|
||||
'export { resolveInboundRouteEnvelopeBuilderWithRuntime } from "openclaw/plugin-sdk/inbound-envelope";',
|
||||
'export { resolveWebhookPath } from "openclaw/plugin-sdk/webhook-path";',
|
||||
'export { registerWebhookTargetWithPluginRoute, resolveWebhookTargetWithAuthOrReject, withResolvedWebhookRequestPipeline } from "openclaw/plugin-sdk/webhook-targets";',
|
||||
|
||||
@@ -290,6 +290,13 @@ describe("plugin-sdk subpath exports", () => {
|
||||
]) {
|
||||
expectSourceMentions(subpath, ["chunkTextForOutbound"]);
|
||||
}
|
||||
for (const subpath of ["googlechat", "msteams", "nextcloud-talk", "zalouser"]) {
|
||||
expectSourceMentions(subpath, [
|
||||
"resolveInboundMentionDecision",
|
||||
"resolveMentionGating",
|
||||
"resolveMentionGatingWithBypass",
|
||||
]);
|
||||
}
|
||||
expectSourceMentions("approval-auth-runtime", [
|
||||
"createResolvedApproverActionAuthAdapter",
|
||||
"resolveApprovalApprovers",
|
||||
@@ -453,6 +460,7 @@ describe("plugin-sdk subpath exports", () => {
|
||||
"recordInboundSession",
|
||||
"recordInboundSessionMetaSafe",
|
||||
"resolveInboundSessionEnvelopeContext",
|
||||
"resolveInboundMentionDecision",
|
||||
"resolveMentionGating",
|
||||
"resolveMentionGatingWithBypass",
|
||||
"resolveOutboundSendDep",
|
||||
@@ -536,9 +544,11 @@ describe("plugin-sdk subpath exports", () => {
|
||||
"formatInboundEnvelope",
|
||||
"formatInboundFromLabel",
|
||||
"formatLocationText",
|
||||
"implicitMentionKindWhen",
|
||||
"logInboundDrop",
|
||||
"matchesMentionPatterns",
|
||||
"matchesMentionWithExplicit",
|
||||
"resolveInboundMentionDecision",
|
||||
"normalizeMentionText",
|
||||
"resolveInboundDebounceMs",
|
||||
"resolveEnvelopeFormatOptions",
|
||||
|
||||
@@ -35,6 +35,10 @@ import { dispatchReplyWithBufferedBlockDispatcher } from "../../auto-reply/reply
|
||||
import { createReplyDispatcherWithTyping } from "../../auto-reply/reply/reply-dispatcher.js";
|
||||
import { removeAckReactionAfterReply, shouldAckReaction } from "../../channels/ack-reactions.js";
|
||||
import { resolveCommandAuthorizedFromAuthorizers } from "../../channels/command-gating.js";
|
||||
import {
|
||||
implicitMentionKindWhen,
|
||||
resolveInboundMentionDecision,
|
||||
} from "../../channels/mention-gating.js";
|
||||
import {
|
||||
setChannelConversationBindingIdleTimeoutBySessionKey,
|
||||
setChannelConversationBindingMaxAgeBySessionKey,
|
||||
@@ -128,6 +132,8 @@ export function createRuntimeChannel(): PluginRuntime["channel"] {
|
||||
buildMentionRegexes,
|
||||
matchesMentionPatterns,
|
||||
matchesMentionWithExplicit,
|
||||
implicitMentionKindWhen,
|
||||
resolveInboundMentionDecision,
|
||||
},
|
||||
reactions: {
|
||||
shouldAckReaction,
|
||||
|
||||
@@ -83,6 +83,8 @@ export type PluginRuntimeChannel = {
|
||||
buildMentionRegexes: typeof import("../../auto-reply/reply/mentions.js").buildMentionRegexes;
|
||||
matchesMentionPatterns: typeof import("../../auto-reply/reply/mentions.js").matchesMentionPatterns;
|
||||
matchesMentionWithExplicit: typeof import("../../auto-reply/reply/mentions.js").matchesMentionWithExplicit;
|
||||
implicitMentionKindWhen: typeof import("../../channels/mention-gating.js").implicitMentionKindWhen;
|
||||
resolveInboundMentionDecision: typeof import("../../channels/mention-gating.js").resolveInboundMentionDecision;
|
||||
};
|
||||
reactions: {
|
||||
shouldAckReaction: typeof import("../../channels/ack-reactions.js").shouldAckReaction;
|
||||
|
||||
@@ -4,6 +4,10 @@ import {
|
||||
removeAckReactionAfterReply,
|
||||
shouldAckReaction,
|
||||
} from "../../../src/channels/ack-reactions.js";
|
||||
import {
|
||||
implicitMentionKindWhen,
|
||||
resolveInboundMentionDecision,
|
||||
} from "../../../src/channels/mention-gating.js";
|
||||
import type { PluginRuntime } from "../../../src/plugins/runtime/types.js";
|
||||
|
||||
type DeepPartial<T> = {
|
||||
@@ -298,6 +302,8 @@ export function createPluginRuntimeMock(overrides: DeepPartial<PluginRuntime> =
|
||||
? true
|
||||
: params.mentionRegexes.some((regex) => regex.test(params.text)),
|
||||
) as unknown as PluginRuntime["channel"]["mentions"]["matchesMentionWithExplicit"],
|
||||
implicitMentionKindWhen,
|
||||
resolveInboundMentionDecision,
|
||||
},
|
||||
reactions: {
|
||||
shouldAckReaction,
|
||||
|
||||
Reference in New Issue
Block a user