mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-18 21:40:53 +00:00
* refactor: move Telegram channel implementation to extensions/telegram/src/ Move all Telegram channel code (123 files + 10 bot/ files + 8 channel plugin files) from src/telegram/ and src/channels/plugins/*/telegram.ts to extensions/telegram/src/. Leave thin re-export shims at original locations so cross-cutting src/ imports continue to resolve. - Fix all relative import paths in moved files (../X/ -> ../../../src/X/) - Fix vi.mock paths in 60 test files - Fix inline typeof import() expressions - Update tsconfig.plugin-sdk.dts.json rootDir to "." for cross-directory DTS - Update write-plugin-sdk-entry-dts.ts for new rootDir structure - Move channel plugin files with correct path remapping * fix: support keyed telegram send deps * fix: sync telegram extension copies with latest main * fix: correct import paths and remove misplaced files in telegram extension * fix: sync outbound-adapter with main (add sendTelegramPayloadMessages) and fix delivery.test import path
289 lines
9.6 KiB
TypeScript
289 lines
9.6 KiB
TypeScript
import {
|
|
findModelInCatalog,
|
|
loadModelCatalog,
|
|
modelSupportsVision,
|
|
} from "../../../src/agents/model-catalog.js";
|
|
import { resolveDefaultModelForAgent } from "../../../src/agents/model-selection.js";
|
|
import { hasControlCommand } from "../../../src/auto-reply/command-detection.js";
|
|
import {
|
|
recordPendingHistoryEntryIfEnabled,
|
|
type HistoryEntry,
|
|
} from "../../../src/auto-reply/reply/history.js";
|
|
import {
|
|
buildMentionRegexes,
|
|
matchesMentionWithExplicit,
|
|
} from "../../../src/auto-reply/reply/mentions.js";
|
|
import type { MsgContext } from "../../../src/auto-reply/templating.js";
|
|
import { resolveControlCommandGate } from "../../../src/channels/command-gating.js";
|
|
import { formatLocationText, type NormalizedLocation } from "../../../src/channels/location.js";
|
|
import { logInboundDrop } from "../../../src/channels/logging.js";
|
|
import { resolveMentionGatingWithBypass } from "../../../src/channels/mention-gating.js";
|
|
import type { OpenClawConfig } from "../../../src/config/config.js";
|
|
import type {
|
|
TelegramDirectConfig,
|
|
TelegramGroupConfig,
|
|
TelegramTopicConfig,
|
|
} from "../../../src/config/types.js";
|
|
import { logVerbose } from "../../../src/globals.js";
|
|
import type { NormalizedAllowFrom } from "./bot-access.js";
|
|
import { isSenderAllowed } from "./bot-access.js";
|
|
import type {
|
|
TelegramLogger,
|
|
TelegramMediaRef,
|
|
TelegramMessageContextOptions,
|
|
} from "./bot-message-context.types.js";
|
|
import {
|
|
buildSenderLabel,
|
|
buildTelegramGroupPeerId,
|
|
expandTextLinks,
|
|
extractTelegramLocation,
|
|
getTelegramTextParts,
|
|
hasBotMention,
|
|
resolveTelegramMediaPlaceholder,
|
|
} from "./bot/helpers.js";
|
|
import type { TelegramContext } from "./bot/types.js";
|
|
import { isTelegramForumServiceMessage } from "./forum-service-message.js";
|
|
|
|
export type TelegramInboundBodyResult = {
|
|
bodyText: string;
|
|
rawBody: string;
|
|
historyKey?: string;
|
|
commandAuthorized: boolean;
|
|
effectiveWasMentioned: boolean;
|
|
canDetectMention: boolean;
|
|
shouldBypassMention: boolean;
|
|
stickerCacheHit: boolean;
|
|
locationData?: NormalizedLocation;
|
|
};
|
|
|
|
async function resolveStickerVisionSupport(params: {
|
|
cfg: OpenClawConfig;
|
|
agentId?: string;
|
|
}): Promise<boolean> {
|
|
try {
|
|
const catalog = await loadModelCatalog({ config: params.cfg });
|
|
const defaultModel = resolveDefaultModelForAgent({
|
|
cfg: params.cfg,
|
|
agentId: params.agentId,
|
|
});
|
|
const entry = findModelInCatalog(catalog, defaultModel.provider, defaultModel.model);
|
|
if (!entry) {
|
|
return false;
|
|
}
|
|
return modelSupportsVision(entry);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export async function resolveTelegramInboundBody(params: {
|
|
cfg: OpenClawConfig;
|
|
primaryCtx: TelegramContext;
|
|
msg: TelegramContext["message"];
|
|
allMedia: TelegramMediaRef[];
|
|
isGroup: boolean;
|
|
chatId: number | string;
|
|
senderId: string;
|
|
senderUsername: string;
|
|
resolvedThreadId?: number;
|
|
routeAgentId?: string;
|
|
effectiveGroupAllow: NormalizedAllowFrom;
|
|
effectiveDmAllow: NormalizedAllowFrom;
|
|
groupConfig?: TelegramGroupConfig | TelegramDirectConfig;
|
|
topicConfig?: TelegramTopicConfig;
|
|
requireMention?: boolean;
|
|
options?: TelegramMessageContextOptions;
|
|
groupHistories: Map<string, HistoryEntry[]>;
|
|
historyLimit: number;
|
|
logger: TelegramLogger;
|
|
}): Promise<TelegramInboundBodyResult | null> {
|
|
const {
|
|
cfg,
|
|
primaryCtx,
|
|
msg,
|
|
allMedia,
|
|
isGroup,
|
|
chatId,
|
|
senderId,
|
|
senderUsername,
|
|
resolvedThreadId,
|
|
routeAgentId,
|
|
effectiveGroupAllow,
|
|
effectiveDmAllow,
|
|
groupConfig,
|
|
topicConfig,
|
|
requireMention,
|
|
options,
|
|
groupHistories,
|
|
historyLimit,
|
|
logger,
|
|
} = params;
|
|
const botUsername = primaryCtx.me?.username?.toLowerCase();
|
|
const mentionRegexes = buildMentionRegexes(cfg, routeAgentId);
|
|
const messageTextParts = getTelegramTextParts(msg);
|
|
const allowForCommands = isGroup ? effectiveGroupAllow : effectiveDmAllow;
|
|
const senderAllowedForCommands = isSenderAllowed({
|
|
allow: allowForCommands,
|
|
senderId,
|
|
senderUsername,
|
|
});
|
|
const useAccessGroups = cfg.commands?.useAccessGroups !== false;
|
|
const hasControlCommandInMessage = hasControlCommand(messageTextParts.text, cfg, {
|
|
botUsername,
|
|
});
|
|
const commandGate = resolveControlCommandGate({
|
|
useAccessGroups,
|
|
authorizers: [{ configured: allowForCommands.hasEntries, allowed: senderAllowedForCommands }],
|
|
allowTextCommands: true,
|
|
hasControlCommand: hasControlCommandInMessage,
|
|
});
|
|
const commandAuthorized = commandGate.commandAuthorized;
|
|
const historyKey = isGroup ? buildTelegramGroupPeerId(chatId, resolvedThreadId) : undefined;
|
|
|
|
let placeholder = resolveTelegramMediaPlaceholder(msg) ?? "";
|
|
const cachedStickerDescription = allMedia[0]?.stickerMetadata?.cachedDescription;
|
|
const stickerSupportsVision = msg.sticker
|
|
? await resolveStickerVisionSupport({ cfg, agentId: routeAgentId })
|
|
: false;
|
|
const stickerCacheHit = Boolean(cachedStickerDescription) && !stickerSupportsVision;
|
|
if (stickerCacheHit) {
|
|
const emoji = allMedia[0]?.stickerMetadata?.emoji;
|
|
const setName = allMedia[0]?.stickerMetadata?.setName;
|
|
const stickerContext = [emoji, setName ? `from "${setName}"` : null].filter(Boolean).join(" ");
|
|
placeholder = `[Sticker${stickerContext ? ` ${stickerContext}` : ""}] ${cachedStickerDescription}`;
|
|
}
|
|
|
|
const locationData = extractTelegramLocation(msg);
|
|
const locationText = locationData ? formatLocationText(locationData) : undefined;
|
|
const rawText = expandTextLinks(messageTextParts.text, messageTextParts.entities).trim();
|
|
const hasUserText = Boolean(rawText || locationText);
|
|
let rawBody = [rawText, locationText].filter(Boolean).join("\n").trim();
|
|
if (!rawBody) {
|
|
rawBody = placeholder;
|
|
}
|
|
if (!rawBody && allMedia.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
let bodyText = rawBody;
|
|
const hasAudio = allMedia.some((media) => media.contentType?.startsWith("audio/"));
|
|
const disableAudioPreflight =
|
|
(topicConfig?.disableAudioPreflight ??
|
|
(groupConfig as TelegramGroupConfig | undefined)?.disableAudioPreflight) === true;
|
|
|
|
let preflightTranscript: string | undefined;
|
|
const needsPreflightTranscription =
|
|
isGroup &&
|
|
requireMention &&
|
|
hasAudio &&
|
|
!hasUserText &&
|
|
mentionRegexes.length > 0 &&
|
|
!disableAudioPreflight;
|
|
|
|
if (needsPreflightTranscription) {
|
|
try {
|
|
const { transcribeFirstAudio } =
|
|
await import("../../../src/media-understanding/audio-preflight.js");
|
|
const tempCtx: MsgContext = {
|
|
MediaPaths: allMedia.length > 0 ? allMedia.map((m) => m.path) : undefined,
|
|
MediaTypes:
|
|
allMedia.length > 0
|
|
? (allMedia.map((m) => m.contentType).filter(Boolean) as string[])
|
|
: undefined,
|
|
};
|
|
preflightTranscript = await transcribeFirstAudio({
|
|
ctx: tempCtx,
|
|
cfg,
|
|
agentDir: undefined,
|
|
});
|
|
} catch (err) {
|
|
logVerbose(`telegram: audio preflight transcription failed: ${String(err)}`);
|
|
}
|
|
}
|
|
|
|
if (hasAudio && bodyText === "<media:audio>" && preflightTranscript) {
|
|
bodyText = preflightTranscript;
|
|
}
|
|
|
|
if (!bodyText && allMedia.length > 0) {
|
|
if (hasAudio) {
|
|
bodyText = preflightTranscript || "<media:audio>";
|
|
} else {
|
|
bodyText = `<media:image>${allMedia.length > 1 ? ` (${allMedia.length} images)` : ""}`;
|
|
}
|
|
}
|
|
|
|
const hasAnyMention = messageTextParts.entities.some((ent) => ent.type === "mention");
|
|
const explicitlyMentioned = botUsername ? hasBotMention(msg, botUsername) : false;
|
|
const computedWasMentioned = matchesMentionWithExplicit({
|
|
text: messageTextParts.text,
|
|
mentionRegexes,
|
|
explicit: {
|
|
hasAnyMention,
|
|
isExplicitlyMentioned: explicitlyMentioned,
|
|
canResolveExplicit: Boolean(botUsername),
|
|
},
|
|
transcript: preflightTranscript,
|
|
});
|
|
const wasMentioned = options?.forceWasMentioned === true ? true : computedWasMentioned;
|
|
|
|
if (isGroup && commandGate.shouldBlock) {
|
|
logInboundDrop({
|
|
log: logVerbose,
|
|
channel: "telegram",
|
|
reason: "control command (unauthorized)",
|
|
target: senderId ?? "unknown",
|
|
});
|
|
return null;
|
|
}
|
|
|
|
const botId = primaryCtx.me?.id;
|
|
const replyFromId = msg.reply_to_message?.from?.id;
|
|
const replyToBotMessage = botId != null && replyFromId === botId;
|
|
const isReplyToServiceMessage =
|
|
replyToBotMessage && isTelegramForumServiceMessage(msg.reply_to_message);
|
|
const implicitMention = 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 effectiveWasMentioned = mentionGate.effectiveWasMentioned;
|
|
if (isGroup && requireMention && canDetectMention && mentionGate.shouldSkip) {
|
|
logger.info({ chatId, reason: "no-mention" }, "skipping group message");
|
|
recordPendingHistoryEntryIfEnabled({
|
|
historyMap: groupHistories,
|
|
historyKey: historyKey ?? "",
|
|
limit: historyLimit,
|
|
entry: historyKey
|
|
? {
|
|
sender: buildSenderLabel(msg, senderId || chatId),
|
|
body: rawBody,
|
|
timestamp: msg.date ? msg.date * 1000 : undefined,
|
|
messageId: typeof msg.message_id === "number" ? String(msg.message_id) : undefined,
|
|
}
|
|
: null,
|
|
});
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
bodyText,
|
|
rawBody,
|
|
historyKey,
|
|
commandAuthorized,
|
|
effectiveWasMentioned,
|
|
canDetectMention,
|
|
shouldBypassMention: mentionGate.shouldBypassMention,
|
|
stickerCacheHit,
|
|
locationData: locationData ?? undefined,
|
|
};
|
|
}
|