






















@@ -1,12 +1,19 @@
11import type { Message, ReactionTypeEmoji } from "@grammyjs/types";
22import { parseExecApprovalCommandText } from "openclaw/plugin-sdk/approval-reply-runtime";
33import { resolveChannelConfigWrites } from "openclaw/plugin-sdk/channel-config-helpers";
4-import { shouldDebounceTextInbound } from "openclaw/plugin-sdk/channel-inbound";
4+import {
5+buildMentionRegexes,
6+implicitMentionKindWhen,
7+matchesMentionWithExplicit,
8+resolveInboundMentionDecision,
9+shouldDebounceTextInbound,
10+} from "openclaw/plugin-sdk/channel-inbound";
511import {
612createInboundDebouncer,
713resolveInboundDebounceMs,
814} from "openclaw/plugin-sdk/channel-inbound-debounce";
915import { resolveStoredModelOverride } from "openclaw/plugin-sdk/command-auth-native";
16+import { hasControlCommand } from "openclaw/plugin-sdk/command-detection";
1017import { buildCommandsMessagePaginated } from "openclaw/plugin-sdk/command-status";
1118import type { DmPolicy, OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
1219import type {
@@ -35,6 +42,7 @@ import { resolveTelegramAccount, resolveTelegramMediaRuntimeOptions } from "./ac
3542import { withTelegramApiErrorLogging } from "./api-logging.js";
3643import {
3744normalizeDmAllowFromWithStore,
45+firstDefined,
3846resolveTelegramEffectiveDmPolicy,
3947type NormalizedAllowFrom,
4048} from "./bot-access.js";
@@ -71,6 +79,7 @@ import {
7179import { resolveMedia } from "./bot/delivery.resolve-media.js";
7280import {
7381getTelegramTextParts,
82+hasBotMention,
7483buildTelegramGroupPeerId,
7584buildTelegramParentPeer,
7685isTelegramCommandsAllowFromConfigured,
@@ -94,6 +103,7 @@ import {
94103isTelegramExecApprovalAuthorizedSender,
95104shouldEnableTelegramExecApprovalButtons,
96105} from "./exec-approvals.js";
106+import { isTelegramForumServiceMessage } from "./forum-service-message.js";
97107import {
98108evaluateTelegramGroupBaseAccess,
99109evaluateTelegramGroupPolicyAccess,
@@ -141,6 +151,8 @@ export const registerTelegramHandlers = ({
141151 processMessage,
142152 logger,
143153 telegramDeps,
154+ resolveGroupActivation,
155+ resolveGroupRequireMention,
144156}: RegisterTelegramHandlerParams) => {
145157const mediaRuntimeOptions = resolveTelegramMediaRuntimeOptions({
146158 cfg,
@@ -167,7 +179,20 @@ export const registerTelegramHandlers = ({
167179 ? Math.max(10, Math.floor(telegramCfg.mediaGroupFlushMs))
168180 : MEDIA_GROUP_TIMEOUT_MS;
169181170-const mediaGroupBuffer = new Map<string, MediaGroupEntry>();
182+type BufferedMediaGroupEntry = MediaGroupEntry & {
183+storeAllowFrom: string[];
184+isGroup: boolean;
185+isForum: boolean;
186+resolvedThreadId?: number;
187+dmThreadId?: number;
188+senderId: string;
189+effectiveGroupAllow: NormalizedAllowFrom;
190+effectiveDmAllow: NormalizedAllowFrom;
191+groupConfig?: TelegramGroupConfig;
192+topicConfig?: TelegramTopicConfig;
193+};
194+195+const mediaGroupBuffer = new Map<string, BufferedMediaGroupEntry>();
171196let mediaGroupProcessing: Promise<void> = Promise.resolve();
172197const messageCache = createTelegramMessageCache({
173198persistedPath: resolveTelegramMessageCachePath(
@@ -575,12 +600,167 @@ export const registerTelegramHandlers = ({
575600};
576601};
577602578-const processMediaGroup = async (entry: MediaGroupEntry) => {
603+const mediaMayNeedDownloadForMentionDetection = (msg: Message): boolean => {
604+const textParts = getTelegramTextParts(msg);
605+if (textParts.text.trim()) {
606+return false;
607+}
608+const documentMime = msg.document?.mime_type?.split(";")[0]?.trim().toLowerCase();
609+return Boolean(msg.audio ?? msg.voice ?? documentMime?.startsWith("audio/"));
610+};
611+612+const shouldSkipMediaDownloadForUnaddressedMentionGroup = async (params: {
613+ctx: TelegramContext;
614+msg: Message;
615+chatId: number;
616+isGroup: boolean;
617+isForum: boolean;
618+resolvedThreadId?: number;
619+dmThreadId?: number;
620+senderId: string;
621+effectiveGroupAllow: NormalizedAllowFrom;
622+effectiveDmAllow: NormalizedAllowFrom;
623+groupConfig?: TelegramGroupConfig;
624+topicConfig?: TelegramTopicConfig;
625+}): Promise<boolean> => {
626+const {
627+ ctx,
628+ msg,
629+ chatId,
630+ isGroup,
631+ isForum,
632+ resolvedThreadId,
633+ dmThreadId,
634+ senderId,
635+ effectiveGroupAllow,
636+ effectiveDmAllow,
637+ groupConfig,
638+ topicConfig,
639+} = params;
640+if (!isGroup || mediaMayNeedDownloadForMentionDetection(msg)) {
641+return false;
642+}
643+644+const runtimeCfg = telegramDeps.getRuntimeConfig();
645+const sessionState = resolveTelegramSessionState({
646+ chatId,
647+ isGroup,
648+ isForum,
649+ resolvedThreadId,
650+messageThreadId: resolvedThreadId ?? dmThreadId,
651+ senderId,
652+ runtimeCfg,
653+});
654+const activationOverride = resolveGroupActivation({
655+ chatId,
656+messageThreadId: resolvedThreadId,
657+sessionKey: sessionState.sessionKey,
658+agentId: sessionState.agentId,
659+});
660+const requireMention = firstDefined(
661+topicConfig?.requireMention,
662+activationOverride,
663+groupConfig?.requireMention,
664+resolveGroupRequireMention(chatId),
665+);
666+if (!requireMention) {
667+return false;
668+}
669+670+const botUsername = ctx.me?.username?.trim().toLowerCase();
671+const mentionRegexes = buildMentionRegexes(runtimeCfg, sessionState.agentId);
672+const messageTextParts = getTelegramTextParts(msg);
673+const hasAnyMention = messageTextParts.entities.some((ent) => ent.type === "mention");
674+const explicitlyMentioned = botUsername ? hasBotMention(msg, botUsername) : false;
675+const wasMentioned = matchesMentionWithExplicit({
676+text: messageTextParts.text,
677+ mentionRegexes,
678+explicit: {
679+ hasAnyMention,
680+isExplicitlyMentioned: explicitlyMentioned,
681+canResolveExplicit: Boolean(botUsername),
682+},
683+});
684+const botId = ctx.me?.id;
685+const replyFromId = msg.reply_to_message?.from?.id;
686+const replyToBotMessage = botId != null && replyFromId === botId;
687+const isReplyToServiceMessage =
688+replyToBotMessage && isTelegramForumServiceMessage(msg.reply_to_message);
689+const implicitMentionKinds = implicitMentionKindWhen(
690+"reply_to_bot",
691+replyToBotMessage && !isReplyToServiceMessage,
692+);
693+const canDetectMention = Boolean(botUsername) || mentionRegexes.length > 0;
694+const hasControlCommandInMessage = hasControlCommand(messageTextParts.text, runtimeCfg, {
695+ botUsername,
696+});
697+const commandGate = await resolveTelegramCommandIngressAuthorization({
698+ accountId,
699+cfg: runtimeCfg,
700+dmPolicy: "pairing",
701+ isGroup,
702+ chatId,
703+ resolvedThreadId,
704+ senderId,
705+ effectiveDmAllow,
706+ effectiveGroupAllow,
707+ownerAccess: { ownerList: [], senderIsOwner: false },
708+eventKind: "message",
709+allowTextCommands: true,
710+hasControlCommand: hasControlCommandInMessage,
711+modeWhenAccessGroupsOff: "allow",
712+includeDmAllowForGroupCommands: false,
713+});
714+const mentionDecision = resolveInboundMentionDecision({
715+facts: {
716+ canDetectMention,
717+ wasMentioned,
718+ hasAnyMention,
719+ implicitMentionKinds,
720+},
721+policy: {
722+ isGroup,
723+requireMention: true,
724+allowTextCommands: true,
725+hasControlCommand: hasControlCommandInMessage,
726+commandAuthorized: commandGate.authorized,
727+},
728+});
729+if (mentionDecision.shouldSkip) {
730+logger.info({ chatId, reason: "no-mention" }, "skipping group media before download");
731+return true;
732+}
733+return false;
734+};
735+736+const processMediaGroup = async (entry: BufferedMediaGroupEntry) => {
579737try {
580738entry.messages.sort((a, b) => a.msg.message_id - b.msg.message_id);
581739582740const captionMsg = entry.messages.find((m) => m.msg.caption || m.msg.text);
583741const primaryEntry = captionMsg ?? entry.messages[0];
742+if (!primaryEntry) {
743+return;
744+}
745+746+if (
747+await shouldSkipMediaDownloadForUnaddressedMentionGroup({
748+ctx: primaryEntry.ctx,
749+msg: primaryEntry.msg,
750+chatId: primaryEntry.msg.chat.id,
751+isGroup: entry.isGroup,
752+isForum: entry.isForum,
753+resolvedThreadId: entry.resolvedThreadId,
754+dmThreadId: entry.dmThreadId,
755+senderId: entry.senderId,
756+effectiveGroupAllow: entry.effectiveGroupAllow,
757+effectiveDmAllow: entry.effectiveDmAllow,
758+groupConfig: entry.groupConfig,
759+topicConfig: entry.topicConfig,
760+})
761+) {
762+return;
763+}
584764585765const allMedia: TelegramMediaRef[] = [];
586766for (const { ctx } of entry.messages) {
@@ -609,12 +789,11 @@ export const registerTelegramHandlers = ({
609789}
610790}
611791612-const storeAllowFrom = await loadStoreAllowFrom();
613792await processMessageWithReplyChain(
614793primaryEntry.ctx,
615794primaryEntry.msg,
616795allMedia,
617-storeAllowFrom,
796+entry.storeAllowFrom,
618797promptContextBoundaryOptions(entry.promptContextMinTimestampMs),
619798);
620799} catch (err) {
@@ -1298,9 +1477,16 @@ export const registerTelegramHandlers = ({
12981477ctx: TelegramContext;
12991478msg: Message;
13001479chatId: number;
1480+isGroup: boolean;
1481+isForum: boolean;
13011482resolvedThreadId?: number;
13021483dmThreadId?: number;
13031484storeAllowFrom: string[];
1485+senderId: string;
1486+effectiveGroupAllow: NormalizedAllowFrom;
1487+effectiveDmAllow: NormalizedAllowFrom;
1488+groupConfig?: TelegramGroupConfig;
1489+topicConfig?: TelegramTopicConfig;
13041490sendOversizeWarning: boolean;
13051491oversizeLogMessage: string;
13061492promptContextMinTimestampMs?: number;
@@ -1309,9 +1495,16 @@ export const registerTelegramHandlers = ({
13091495 ctx,
13101496 msg,
13111497 chatId,
1498+ isGroup,
1499+ isForum,
13121500 resolvedThreadId,
13131501 dmThreadId,
13141502 storeAllowFrom,
1503+ senderId,
1504+ effectiveGroupAllow,
1505+ effectiveDmAllow,
1506+ groupConfig,
1507+ topicConfig,
13151508 sendOversizeWarning,
13161509 oversizeLogMessage,
13171510 promptContextMinTimestampMs,
@@ -1407,8 +1600,18 @@ export const registerTelegramHandlers = ({
14071600await mediaGroupProcessing;
14081601}, mediaGroupTimeoutMs);
14091602} else {
1410-const entry: MediaGroupEntry = {
1603+const entry: BufferedMediaGroupEntry = {
14111604messages: [{ msg, ctx }],
1605+ storeAllowFrom,
1606+ isGroup,
1607+ isForum,
1608+ resolvedThreadId,
1609+ dmThreadId,
1610+ senderId,
1611+ effectiveGroupAllow,
1612+ effectiveDmAllow,
1613+ groupConfig,
1614+ topicConfig,
14121615 ...promptContextBoundaryOptions(promptContextMinTimestampMs),
14131616timer: setTimeout(async () => {
14141617mediaGroupBuffer.delete(mediaGroupId);
@@ -1425,6 +1628,25 @@ export const registerTelegramHandlers = ({
14251628return;
14261629}
142716301631+if (
1632+await shouldSkipMediaDownloadForUnaddressedMentionGroup({
1633+ ctx,
1634+ msg,
1635+ chatId,
1636+ isGroup,
1637+ isForum,
1638+ resolvedThreadId,
1639+ dmThreadId,
1640+ senderId,
1641+ effectiveGroupAllow,
1642+ effectiveDmAllow,
1643+ groupConfig,
1644+ topicConfig,
1645+})
1646+) {
1647+return;
1648+}
1649+14281650let media: Awaited<ReturnType<typeof resolveMedia>> = null;
14291651try {
14301652media = await resolveMedia({
@@ -1483,7 +1705,6 @@ export const registerTelegramHandlers = ({
14831705},
14841706]
14851707 : [];
1486-const senderId = msg.from?.id ? String(msg.from.id) : "";
14871708const conversationKey = buildTelegramInboundDebounceConversationKey({
14881709 chatId,
14891710threadId: resolvedThreadId ?? dmThreadId,
@@ -2394,9 +2615,16 @@ export const registerTelegramHandlers = ({
23942615ctx: event.ctx,
23952616msg: event.msg,
23962617chatId: event.chatId,
2618+isGroup: event.isGroup,
2619+isForum: event.isForum,
23972620 resolvedThreadId,
23982621 dmThreadId,
23992622 storeAllowFrom,
2623+senderId: event.senderId,
2624+ effectiveGroupAllow,
2625+ effectiveDmAllow,
2626+groupConfig: event.isGroup ? (groupConfig as TelegramGroupConfig | undefined) : undefined,
2627+ topicConfig,
24002628sendOversizeWarning: event.sendOversizeWarning,
24012629oversizeLogMessage: event.oversizeLogMessage,
24022630 ...promptContextBoundaryOptions(promptContextMinTimestampMs),
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。