diff --git a/extensions/discord/src/monitor/message-handler.context.ts b/extensions/discord/src/monitor/message-handler.context.ts index 82bac338e28..b12c1c4c101 100644 --- a/extensions/discord/src/monitor/message-handler.context.ts +++ b/extensions/discord/src/monitor/message-handler.context.ts @@ -1,11 +1,11 @@ import { + buildChannelTurnContext, formatInboundEnvelope, resolveEnvelopeFormatOptions, } from "openclaw/plugin-sdk/channel-inbound"; import { resolveChannelContextVisibilityMode } from "openclaw/plugin-sdk/context-visibility-runtime"; import { resolvePinnedMainDmOwnerFromAllowlist } from "openclaw/plugin-sdk/conversation-runtime"; import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/dangerous-name-runtime"; -import { finalizeInboundContext } from "openclaw/plugin-sdk/reply-dispatch-runtime"; import { createChannelHistoryWindow } from "openclaw/plugin-sdk/reply-history"; import { buildAgentSessionKey, resolveThreadSessionKeys } from "openclaw/plugin-sdk/routing"; import { danger, logVerbose, shouldLogVerbose } from "openclaw/plugin-sdk/runtime-env"; @@ -22,7 +22,6 @@ import { } from "./inbound-context.js"; import type { DiscordMessagePreflightContext } from "./message-handler.preflight.js"; import { - buildDiscordMediaPayload, resolveReferencedReplyMediaList, resolveDiscordMessageText, type DiscordMediaInfo, @@ -263,7 +262,6 @@ export async function buildDiscordMessageProcessContext(params: { parentSessionKey = undefined; } } - const mediaPayload = buildDiscordMediaPayload(mediaListForContext); const preflightAudioIndex = preflightAudioTranscript === undefined ? -1 @@ -329,59 +327,102 @@ export async function buildDiscordMessageProcessContext(params: { sessionKey: effectiveSessionKey, }); - const ctxPayload = finalizeInboundContext({ - Body: combinedBody, - BodyForAgent: preflightAudioTranscript ?? baseText ?? text, - InboundHistory: inboundHistory, - RawBody: preflightAudioTranscript ?? baseText, - CommandBody: preflightAudioTranscript ?? baseText, - ...(preflightAudioTranscript !== undefined ? { Transcript: preflightAudioTranscript } : {}), - From: effectiveFrom, - To: effectiveTo, - SessionKey: effectiveSessionKey, - AccountId: route.accountId, - ChatType: isDirectMessage ? "direct" : "channel", - ConversationLabel: fromLabel, - SenderName: senderName, - SenderId: sender.id, - SenderUsername: senderUsername, - SenderTag: sender.tag, - GroupSubject: groupSubject, - GroupChannel: groupChannel, - MemberRoleIds: memberRoleIds, - UntrustedContext: untrustedContext, - GroupSystemPrompt: isGuildMessage ? groupSystemPrompt : undefined, - GroupSpace: isGuildMessage ? (guildInfo?.id ?? guildSlug) || undefined : undefined, - OwnerAllowFrom: ownerAllowFrom, - Provider: "discord" as const, - Surface: "discord" as const, - WasMentioned: ctx.effectiveWasMentioned, - MessageSid: canonicalMessageId ?? message.id, - ...(canonicalMessageId && canonicalMessageId !== message.id - ? { MessageSidFull: message.id } - : {}), - ReplyToId: filteredReplyContext?.id, - ReplyToBody: filteredReplyContext?.body, - ReplyToSender: filteredReplyContext?.sender, - ParentSessionKey: autoThreadContext?.ParentSessionKey ?? threadKeys.parentSessionKey, - ModelParentSessionKey: - autoThreadContext?.ModelParentSessionKey ?? modelParentSessionKey ?? undefined, - MessageThreadId: threadChannel?.id ?? autoThreadContext?.createdThreadId ?? undefined, - ThreadStarterBody: !effectivePreviousTimestamp ? threadStarterBody : undefined, - ThreadLabel: threadLabel, - Timestamp: resolveTimestampMs(message.timestamp), - ...mediaPayload, - ...(preflightAudioIndex >= 0 ? { MediaTranscribedIndexes: [preflightAudioIndex] } : {}), - CommandAuthorized: commandAuthorized, - CommandTurn: { + const ctxPayload = buildChannelTurnContext({ + channel: "discord", + provider: "discord", + surface: "discord", + accountId: route.accountId, + messageId: canonicalMessageId ?? message.id, + messageIdFull: canonicalMessageId && canonicalMessageId !== message.id ? message.id : undefined, + timestamp: resolveTimestampMs(message.timestamp), + from: effectiveFrom, + sender: { + id: sender.id, + name: senderName, + username: senderUsername, + tag: sender.tag, + roles: memberRoleIds, + displayLabel: senderLabel, + }, + conversation: { + kind: isDirectMessage ? "direct" : "channel", + id: messageChannelId, + label: fromLabel, + spaceId: isGuildMessage ? (guildInfo?.id ?? guildSlug) || undefined : undefined, + threadId: threadChannel?.id ?? autoThreadContext?.createdThreadId ?? undefined, + routePeer: { + kind: isDirectMessage ? "direct" : "channel", + id: isDirectMessage ? author.id : messageChannelId, + }, + }, + route: { + agentId: route.agentId, + accountId: route.accountId, + routeSessionKey: route.sessionKey, + dispatchSessionKey: effectiveSessionKey, + parentSessionKey: autoThreadContext?.ParentSessionKey ?? threadKeys.parentSessionKey, + modelParentSessionKey: + autoThreadContext?.ModelParentSessionKey ?? modelParentSessionKey ?? undefined, + }, + reply: { + to: effectiveTo, + originatingTo, + }, + message: { + body: combinedBody, + rawBody: preflightAudioTranscript ?? baseText, + bodyForAgent: preflightAudioTranscript ?? baseText ?? text, + commandBody: preflightAudioTranscript ?? baseText, + envelopeFrom: fromLabel, + inboundHistory, + }, + access: { + mentions: { + canDetectMention: ctx.canDetectMention, + wasMentioned: ctx.effectiveWasMentioned, + hasAnyMention: ctx.hasAnyMention, + requireMention: ctx.shouldRequireMention, + effectiveWasMentioned: ctx.effectiveWasMentioned, + }, + commands: { + authorized: commandAuthorized, + allowTextCommands: ctx.allowTextCommands, + useAccessGroups: false, + authorizers: [], + }, + }, + commandTurn: { kind: "text-slash" as const, source: "text" as const, authorized: commandAuthorized, body: preflightAudioTranscript ?? baseText, }, - CommandSource: "text" as const, - OriginatingChannel: "discord" as const, - OriginatingTo: originatingTo, + media: mediaListForContext.map((media, index) => ({ + path: media.path, + contentType: media.contentType, + transcribed: index === preflightAudioIndex, + })), + supplemental: { + quote: filteredReplyContext + ? { + id: filteredReplyContext.id, + body: filteredReplyContext.body, + sender: filteredReplyContext.sender, + } + : undefined, + thread: { + starterBody: !effectivePreviousTimestamp ? threadStarterBody : undefined, + label: threadLabel, + }, + groupSystemPrompt: isGuildMessage ? groupSystemPrompt : undefined, + }, + extra: { + ...(preflightAudioTranscript !== undefined ? { Transcript: preflightAudioTranscript } : {}), + GroupSubject: groupSubject, + GroupChannel: groupChannel, + UntrustedContext: untrustedContext, + OwnerAllowFrom: ownerAllowFrom, + }, }); const persistedSessionKey = ctxPayload.SessionKey ?? route.sessionKey; diff --git a/extensions/slack/src/monitor/message-handler/prepare.ts b/extensions/slack/src/monitor/message-handler/prepare.ts index 3b1a3539232..17680f58fcb 100644 --- a/extensions/slack/src/monitor/message-handler/prepare.ts +++ b/extensions/slack/src/monitor/message-handler/prepare.ts @@ -4,6 +4,7 @@ import { type AckReactionScope, } from "openclaw/plugin-sdk/channel-feedback"; import { + buildChannelTurnContext, buildMentionRegexes, formatInboundEnvelope, implicitMentionKindWhen, @@ -18,7 +19,6 @@ import { ensureConfiguredBindingRouteReady } from "openclaw/plugin-sdk/conversat import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { recordDroppedChannelTurnHistory } from "openclaw/plugin-sdk/inbound-reply-dispatch"; import { mimeTypeFromFilePath } from "openclaw/plugin-sdk/media-mime"; -import { finalizeInboundContext } from "openclaw/plugin-sdk/reply-dispatch-runtime"; import { createChannelHistoryWindow } from "openclaw/plugin-sdk/reply-history"; import type { FinalizedMsgContext } from "openclaw/plugin-sdk/reply-runtime"; import { resolveInboundLastRouteSessionKey } from "openclaw/plugin-sdk/routing"; @@ -1056,8 +1056,6 @@ export async function prepareSlackMessage(params: { // Use direct media (including forwarded attachment media) if available, else thread starter media const effectiveMedia = effectiveDirectMedia ?? threadStarterMedia; - const firstMedia = effectiveMedia?.[0]; - const inboundHistory = isRoomish && ctx.historyLimit > 0 ? channelHistory.buildInboundHistory({ @@ -1067,63 +1065,98 @@ export async function prepareSlackMessage(params: { : dmHistoryContext.inboundHistory; const commandBody = textForCommandDetection.trim(); - const ctxPayload = finalizeInboundContext({ - Body: combinedBody, - BodyForAgent: rawBody, - InboundHistory: inboundHistory, - RawBody: rawBody, - CommandBody: commandBody, - BodyForCommands: commandBody, - From: slackFrom, - To: slackTo, - SessionKey: sessionKey, - AccountId: route.accountId, - ChatType: chatType, - ConversationLabel: envelopeFrom, - GroupSubject: isRoomish ? roomLabel : undefined, - GroupSpace: ctx.teamId || undefined, - GroupSystemPrompt: groupSystemPrompt, - UntrustedContext: untrustedChannelMetadata ? [untrustedChannelMetadata] : undefined, - SenderName: senderName, - SenderId: senderId, - Provider: "slack" as const, - Surface: "slack" as const, - MessageSid: message.ts, - ReplyToId: threadContext.replyToId, - // Preserve thread context for routed tool notifications. - MessageThreadId: threadContext.messageThreadId, - ParentSessionKey: threadKeys.parentSessionKey, - // Only include thread starter body for NEW sessions (existing sessions already have it in their transcript) - ThreadStarterBody: !threadSessionPreviousTimestamp ? threadStarterBody : undefined, - ThreadHistoryBody: threadHistoryBody, - IsFirstThreadTurn: - isThreadReply && threadTs && !threadSessionPreviousTimestamp ? true : undefined, - ThreadLabel: threadLabel, - Timestamp: message.ts ? Math.round(Number(message.ts) * 1000) : undefined, - ...buildSlackMentionContextPayload({ - isRoomish, - effectiveWasMentioned, - explicitlyMentioned, - mentionedUserIds, - mentionedSubteamIds, - matchedImplicitMentionKinds, - mentionSource, - }), - MediaPath: firstMedia?.path, - MediaType: firstMedia?.contentType, - MediaUrl: firstMedia?.path, - MediaPaths: - effectiveMedia && effectiveMedia.length > 0 ? effectiveMedia.map((m) => m.path) : undefined, - MediaUrls: - effectiveMedia && effectiveMedia.length > 0 ? effectiveMedia.map((m) => m.path) : undefined, - MediaTypes: - effectiveMedia && effectiveMedia.length > 0 - ? effectiveMedia.map((m) => m.contentType ?? "") - : undefined, - CommandAuthorized: commandAuthorized, - OriginatingChannel: "slack" as const, - OriginatingTo: slackTo, - NativeChannelId: message.channel, + const ctxPayload = buildChannelTurnContext({ + channel: "slack", + provider: "slack", + surface: "slack", + accountId: route.accountId, + messageId: message.ts, + timestamp: message.ts ? Math.round(Number(message.ts) * 1000) : undefined, + from: slackFrom, + sender: { + id: senderId, + name: senderName, + displayLabel: senderName, + }, + conversation: { + kind: chatType, + id: message.channel, + label: envelopeFrom, + spaceId: ctx.teamId || undefined, + threadId: threadContext.messageThreadId, + nativeChannelId: message.channel, + routePeer: { + kind: chatType, + id: message.channel, + }, + }, + route: { + agentId: route.agentId, + accountId: route.accountId, + routeSessionKey: sessionKey, + parentSessionKey: threadKeys.parentSessionKey, + }, + reply: { + to: slackTo, + originatingTo: slackTo, + replyToId: threadContext.replyToId, + messageThreadId: threadContext.messageThreadId, + nativeChannelId: message.channel, + }, + message: { + body: combinedBody, + bodyForAgent: rawBody, + rawBody, + commandBody, + envelopeFrom, + inboundHistory, + }, + access: { + mentions: { + canDetectMention: isRoomish, + wasMentioned: effectiveWasMentioned, + hasAnyMention: explicitlyMentioned || mentionedSubteamIds.length > 0, + implicitMentionKinds: matchedImplicitMentionKinds as Array< + "reply_to_bot" | "quoted_bot" | "bot_thread_participant" | "native" + >, + requireMention: shouldRequireMention, + effectiveWasMentioned, + }, + commands: { + authorized: commandAuthorized, + allowTextCommands, + useAccessGroups: false, + authorizers: [], + }, + }, + media: effectiveMedia?.map((media) => ({ + path: media.path, + contentType: media.contentType, + })), + supplemental: { + thread: { + // Only include thread starter body for NEW sessions (existing sessions already have it in their transcript) + starterBody: !threadSessionPreviousTimestamp ? threadStarterBody : undefined, + historyBody: threadHistoryBody, + label: threadLabel, + }, + groupSystemPrompt, + }, + extra: { + GroupSubject: isRoomish ? roomLabel : undefined, + UntrustedContext: untrustedChannelMetadata ? [untrustedChannelMetadata] : undefined, + IsFirstThreadTurn: + isThreadReply && threadTs && !threadSessionPreviousTimestamp ? true : undefined, + ...buildSlackMentionContextPayload({ + isRoomish, + effectiveWasMentioned, + explicitlyMentioned, + mentionedUserIds, + mentionedSubteamIds, + matchedImplicitMentionKinds, + mentionSource, + }), + }, }) satisfies FinalizedMsgContext; if (isRoomish && !shouldRequireMention) {