diff --git a/extensions/msteams/src/attachments/graph.ts b/extensions/msteams/src/attachments/graph.ts index 221a32fff69..cf346b03615 100644 --- a/extensions/msteams/src/attachments/graph.ts +++ b/extensions/msteams/src/attachments/graph.ts @@ -1,3 +1,7 @@ +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalLowercaseString, +} from "openclaw/plugin-sdk/text-runtime"; import { fetchWithSsrFGuard, type SsrFPolicy } from "../../runtime-api.js"; import { getMSTeamsRuntime } from "../runtime.js"; import { ensureUserAgentHeader } from "../user-agent.js"; @@ -46,7 +50,7 @@ export function buildMSTeamsGraphMessageUrls(params: { conversationMessageId?: string | null; channelData?: unknown; }): string[] { - const conversationType = params.conversationType?.trim().toLowerCase() ?? ""; + const conversationType = normalizeLowercaseStringOrEmpty(params.conversationType ?? ""); const messageIdCandidates = new Set(); const pushCandidate = (value: string | null | undefined) => { const trimmed = typeof value === "string" ? value.trim() : ""; @@ -382,7 +386,7 @@ export async function downloadMSTeamsGraphMedia(params: { const filteredAttachments = sharePointMedia.length > 0 ? normalizedAttachments.filter((att) => { - const contentType = att.contentType?.toLowerCase(); + const contentType = normalizeOptionalLowercaseString(att.contentType); if (contentType !== "reference") { return true; } diff --git a/extensions/msteams/src/attachments/shared.ts b/extensions/msteams/src/attachments/shared.ts index 7aa257c16a4..c8d1ec288b7 100644 --- a/extensions/msteams/src/attachments/shared.ts +++ b/extensions/msteams/src/attachments/shared.ts @@ -7,7 +7,11 @@ import { normalizeHostnameSuffixAllowlist, type SsrFPolicy, } from "openclaw/plugin-sdk/ssrf-policy"; -import { isRecord, normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; +import { + isRecord, + normalizeLowercaseStringOrEmpty, + normalizeOptionalString, +} from "openclaw/plugin-sdk/text-runtime"; import type { MSTeamsAttachmentLike } from "./types.js"; type InlineImageCandidate = @@ -119,9 +123,9 @@ export function inferPlaceholder(params: { fileName?: string; fileType?: string; }): string { - const mime = params.contentType?.toLowerCase() ?? ""; - const name = params.fileName?.toLowerCase() ?? ""; - const fileType = params.fileType?.toLowerCase() ?? ""; + const mime = normalizeLowercaseStringOrEmpty(params.contentType ?? ""); + const name = normalizeLowercaseStringOrEmpty(params.fileName ?? ""); + const fileType = normalizeLowercaseStringOrEmpty(params.fileType ?? ""); const looksLikeImage = mime.startsWith("image/") || IMAGE_EXT_RE.test(name) || IMAGE_EXT_RE.test(`x.${fileType}`); @@ -232,7 +236,7 @@ function decodeDataImageWithLimits( if (!match) { return { candidate: null, estimatedBytes: 0 }; } - const contentType = match[1]?.toLowerCase(); + const contentType = normalizeLowercaseStringOrEmpty(match[1] ?? ""); const isBase64 = Boolean(match[2]); if (!isBase64) { return { candidate: null, estimatedBytes: 0 }; diff --git a/extensions/msteams/src/feedback-reflection.ts b/extensions/msteams/src/feedback-reflection.ts index 6e3ce78fae3..605ae402e18 100644 --- a/extensions/msteams/src/feedback-reflection.ts +++ b/extensions/msteams/src/feedback-reflection.ts @@ -10,6 +10,7 @@ * 6. Optionally sends a proactive follow-up to the user */ +import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/text-runtime"; import { dispatchReplyFromConfigWithSettledDispatcher, type OpenClawConfig, @@ -241,7 +242,9 @@ export async function runFeedbackReflection(params: RunFeedbackReflectionParams) log.debug?.("failed to store reflection learning", { error: formatUnknownError(err) }); } - const conversationType = params.conversationRef.conversation?.conversationType?.toLowerCase(); + const conversationType = normalizeOptionalLowercaseString( + params.conversationRef.conversation?.conversationType, + ); const shouldNotify = conversationType === "personal" && parsedReflection.followUp && diff --git a/extensions/msteams/src/file-consent-helpers.ts b/extensions/msteams/src/file-consent-helpers.ts index e8a1bc0c696..2c9a7f932de 100644 --- a/extensions/msteams/src/file-consent-helpers.ts +++ b/extensions/msteams/src/file-consent-helpers.ts @@ -9,6 +9,7 @@ * and messenger.ts (reply path) to avoid duplication. */ +import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/text-runtime"; import { buildFileConsentCard } from "./file-consent.js"; import { storePendingUpload } from "./pending-uploads.js"; @@ -66,7 +67,7 @@ export function requiresFileConsent(params: { bufferSize: number; thresholdBytes: number; }): boolean { - const isPersonal = params.conversationType?.toLowerCase() === "personal"; + const isPersonal = normalizeOptionalLowercaseString(params.conversationType) === "personal"; const isImage = params.contentType?.startsWith("image/") ?? false; const isLargeFile = params.bufferSize >= params.thresholdBytes; return isPersonal && (isLargeFile || !isImage); diff --git a/extensions/msteams/src/reply-dispatcher.ts b/extensions/msteams/src/reply-dispatcher.ts index a1b0ca3d0d3..4ada78aab78 100644 --- a/extensions/msteams/src/reply-dispatcher.ts +++ b/extensions/msteams/src/reply-dispatcher.ts @@ -1,3 +1,4 @@ +import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/text-runtime"; import { createChannelReplyPipeline, logTypingFailure, @@ -47,7 +48,9 @@ export function createMSTeamsReplyDispatcher(params: { }) { const core = getMSTeamsRuntime(); const msteamsCfg = params.cfg.channels?.msteams; - const conversationType = params.conversationRef.conversation?.conversationType?.toLowerCase(); + const conversationType = normalizeOptionalLowercaseString( + params.conversationRef.conversation?.conversationType, + ); const isTypingSupported = conversationType === "personal" || conversationType === "groupchat"; const sendTypingIndicator = isTypingSupported diff --git a/extensions/msteams/src/reply-stream-controller.ts b/extensions/msteams/src/reply-stream-controller.ts index 4539f6a5ae3..13494feab34 100644 --- a/extensions/msteams/src/reply-stream-controller.ts +++ b/extensions/msteams/src/reply-stream-controller.ts @@ -1,3 +1,4 @@ +import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/text-runtime"; import type { ReplyPayload } from "../runtime-api.js"; import { formatUnknownError } from "./errors.js"; import type { MSTeamsMonitorLogger } from "./monitor-types.js"; @@ -23,7 +24,7 @@ export function createTeamsReplyStreamController(params: { log: MSTeamsMonitorLogger; random?: () => number; }) { - const isPersonal = params.conversationType?.toLowerCase() === "personal"; + const isPersonal = normalizeOptionalLowercaseString(params.conversationType) === "personal"; const stream = isPersonal ? new TeamsHttpStream({ sendActivity: (activity) => params.context.sendActivity(activity), diff --git a/extensions/msteams/src/resolve-allowlist.ts b/extensions/msteams/src/resolve-allowlist.ts index a5145bebf0f..f195130e141 100644 --- a/extensions/msteams/src/resolve-allowlist.ts +++ b/extensions/msteams/src/resolve-allowlist.ts @@ -1,4 +1,8 @@ import { mapAllowlistResolutionInputs } from "openclaw/plugin-sdk/allow-from"; +import { + normalizeLowercaseStringOrEmpty, + normalizeOptionalLowercaseString, +} from "openclaw/plugin-sdk/text-runtime"; import { searchGraphUsers } from "./graph-users.js"; import { listChannelsForTeam, @@ -135,7 +139,9 @@ export async function resolveMSTeamsChannelAllowlist(params: { } catch { // API failure (rate limit, network error) — fall back to Graph GUID as team key } - const generalChannel = teamChannels.find((ch) => ch.displayName?.toLowerCase() === "general"); + const generalChannel = teamChannels.find( + (ch) => normalizeOptionalLowercaseString(ch.displayName) === "general", + ); // Use the General channel's conversation ID as the team key — this // matches what Bot Framework sends at runtime. Fall back to the Graph // GUID if the General channel isn't found (renamed or deleted). @@ -150,11 +156,14 @@ export async function resolveMSTeamsChannelAllowlist(params: { }; } // Reuse teamChannels — already fetched above + const normalizedChannel = normalizeOptionalLowercaseString(channel); const channelMatch = teamChannels.find((item) => item.id === channel) ?? - teamChannels.find((item) => item.displayName?.toLowerCase() === channel.toLowerCase()) ?? + teamChannels.find( + (item) => normalizeOptionalLowercaseString(item.displayName) === normalizedChannel, + ) ?? teamChannels.find((item) => - item.displayName?.toLowerCase().includes(channel.toLowerCase() ?? ""), + normalizeLowercaseStringOrEmpty(item.displayName ?? "").includes(normalizedChannel ?? ""), ); if (!channelMatch?.id) { return { input, resolved: false, note: "channel not found" }; diff --git a/extensions/msteams/src/session-route.ts b/extensions/msteams/src/session-route.ts index 95f778b0249..11700878ebd 100644 --- a/extensions/msteams/src/session-route.ts +++ b/extensions/msteams/src/session-route.ts @@ -4,6 +4,7 @@ import { stripTargetKindPrefix, type ChannelOutboundSessionRouteParams, } from "openclaw/plugin-sdk/core"; +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; export function resolveMSTeamsOutboundSessionRoute(params: ChannelOutboundSessionRouteParams) { let trimmed = stripChannelTargetPrefix(params.target, "msteams", "teams"); @@ -11,7 +12,7 @@ export function resolveMSTeamsOutboundSessionRoute(params: ChannelOutboundSessio return null; } - const lower = trimmed.toLowerCase(); + const lower = normalizeLowercaseStringOrEmpty(trimmed); const isUser = lower.startsWith("user:"); const rawId = stripTargetKindPrefix(trimmed); if (!rawId) {