@openclaw/msteams 2026.3.12 → 2026.5.1-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api.ts +3 -0
- package/channel-config-api.ts +1 -0
- package/channel-plugin-api.ts +2 -0
- package/config-api.ts +4 -0
- package/contract-api.ts +4 -0
- package/index.ts +15 -12
- package/openclaw.plugin.json +553 -1
- package/package.json +46 -12
- package/runtime-api.ts +73 -0
- package/secret-contract-api.ts +5 -0
- package/setup-entry.ts +13 -0
- package/setup-plugin-api.ts +3 -0
- package/src/ai-entity.ts +7 -0
- package/src/approval-auth.ts +44 -0
- package/src/attachments/bot-framework.test.ts +461 -0
- package/src/attachments/bot-framework.ts +362 -0
- package/src/attachments/download.ts +63 -19
- package/src/attachments/graph.test.ts +416 -0
- package/src/attachments/graph.ts +163 -72
- package/src/attachments/html.ts +33 -1
- package/src/attachments/payload.ts +1 -1
- package/src/attachments/remote-media.test.ts +137 -0
- package/src/attachments/remote-media.ts +75 -8
- package/src/attachments/shared.test.ts +161 -9
- package/src/attachments/shared.ts +193 -26
- package/src/attachments/types.ts +10 -0
- package/src/attachments.graph.test.ts +342 -0
- package/src/attachments.helpers.test.ts +246 -0
- package/src/attachments.test-helpers.ts +17 -0
- package/src/attachments.test.ts +174 -437
- package/src/attachments.ts +5 -5
- package/src/block-streaming-config.test.ts +61 -0
- package/src/channel-api.ts +1 -0
- package/src/channel.actions.test.ts +742 -0
- package/src/channel.directory.test.ts +148 -14
- package/src/channel.runtime.ts +56 -0
- package/src/channel.setup.ts +77 -0
- package/src/channel.test.ts +128 -0
- package/src/channel.ts +1077 -395
- package/src/config-schema.ts +6 -0
- package/src/config-ui-hints.ts +12 -0
- package/src/conversation-store-fs.test.ts +4 -5
- package/src/conversation-store-fs.ts +35 -51
- package/src/conversation-store-helpers.test.ts +202 -0
- package/src/conversation-store-helpers.ts +105 -0
- package/src/conversation-store-memory.ts +27 -23
- package/src/conversation-store.shared.test.ts +225 -0
- package/src/conversation-store.ts +30 -0
- package/src/directory-live.test.ts +156 -0
- package/src/directory-live.ts +7 -4
- package/src/doctor.ts +27 -0
- package/src/errors.test.ts +64 -1
- package/src/errors.ts +50 -9
- package/src/feedback-reflection-prompt.ts +117 -0
- package/src/feedback-reflection-store.ts +114 -0
- package/src/feedback-reflection.test.ts +237 -0
- package/src/feedback-reflection.ts +283 -0
- package/src/file-consent-helpers.test.ts +83 -0
- package/src/file-consent-helpers.ts +64 -11
- package/src/file-consent-invoke.ts +150 -0
- package/src/file-consent.test.ts +363 -0
- package/src/file-consent.ts +165 -4
- package/src/graph-chat.ts +5 -3
- package/src/graph-group-management.test.ts +318 -0
- package/src/graph-group-management.ts +168 -0
- package/src/graph-members.test.ts +89 -0
- package/src/graph-members.ts +48 -0
- package/src/graph-messages.actions.test.ts +243 -0
- package/src/graph-messages.read.test.ts +391 -0
- package/src/graph-messages.search.test.ts +213 -0
- package/src/graph-messages.test-helpers.ts +50 -0
- package/src/graph-messages.ts +534 -0
- package/src/graph-teams.test.ts +215 -0
- package/src/graph-teams.ts +114 -0
- package/src/graph-thread.test.ts +246 -0
- package/src/graph-thread.ts +146 -0
- package/src/graph-upload.test.ts +258 -0
- package/src/graph-upload.ts +87 -8
- package/src/graph.test.ts +516 -0
- package/src/graph.ts +233 -21
- package/src/inbound.test.ts +156 -1
- package/src/inbound.ts +101 -1
- package/src/media-helpers.ts +1 -1
- package/src/mentions.test.ts +27 -18
- package/src/mentions.ts +2 -2
- package/src/messenger.test.ts +522 -45
- package/src/messenger.ts +133 -52
- package/src/monitor-handler/access.ts +125 -0
- package/src/monitor-handler/inbound-media.test.ts +289 -0
- package/src/monitor-handler/inbound-media.ts +57 -5
- package/src/monitor-handler/message-handler-mock-support.test-support.ts +28 -0
- package/src/monitor-handler/message-handler.authz.test.ts +588 -74
- package/src/monitor-handler/message-handler.dm-media.test.ts +54 -0
- package/src/monitor-handler/message-handler.test-support.ts +100 -0
- package/src/monitor-handler/message-handler.thread-parent.test.ts +223 -0
- package/src/monitor-handler/message-handler.thread-session.test.ts +77 -0
- package/src/monitor-handler/message-handler.ts +477 -174
- package/src/monitor-handler/reaction-handler.test.ts +267 -0
- package/src/monitor-handler/reaction-handler.ts +210 -0
- package/src/monitor-handler/thread-session.ts +17 -0
- package/src/monitor-handler.adaptive-card.test.ts +162 -0
- package/src/monitor-handler.feedback-authz.test.ts +314 -0
- package/src/monitor-handler.file-consent.test.ts +301 -106
- package/src/monitor-handler.sso.test.ts +563 -0
- package/src/monitor-handler.test-helpers.ts +180 -0
- package/src/monitor-handler.ts +459 -115
- package/src/monitor-handler.types.ts +27 -0
- package/src/monitor-types.ts +1 -0
- package/src/monitor.lifecycle.test.ts +74 -10
- package/src/monitor.test.ts +35 -1
- package/src/monitor.ts +143 -46
- package/src/oauth.flow.ts +77 -0
- package/src/oauth.shared.ts +37 -0
- package/src/oauth.test.ts +305 -0
- package/src/oauth.token.ts +158 -0
- package/src/oauth.ts +130 -0
- package/src/outbound.test.ts +10 -11
- package/src/outbound.ts +62 -44
- package/src/pending-uploads-fs.test.ts +246 -0
- package/src/pending-uploads-fs.ts +235 -0
- package/src/pending-uploads.test.ts +173 -0
- package/src/pending-uploads.ts +34 -2
- package/src/policy.test.ts +34 -40
- package/src/policy.ts +5 -5
- package/src/polls.test.ts +106 -5
- package/src/polls.ts +15 -7
- package/src/presentation.ts +68 -0
- package/src/probe.test.ts +27 -8
- package/src/probe.ts +43 -9
- package/src/reply-dispatcher.test.ts +437 -0
- package/src/reply-dispatcher.ts +259 -73
- package/src/reply-stream-controller.test.ts +235 -0
- package/src/reply-stream-controller.ts +147 -0
- package/src/resolve-allowlist.test.ts +105 -1
- package/src/resolve-allowlist.ts +112 -7
- package/src/runtime.ts +6 -3
- package/src/sdk-types.ts +43 -3
- package/src/sdk.test.ts +666 -0
- package/src/sdk.ts +867 -16
- package/src/secret-contract.ts +49 -0
- package/src/secret-input.ts +1 -1
- package/src/send-context.ts +76 -9
- package/src/send.test.ts +389 -5
- package/src/send.ts +140 -32
- package/src/sent-message-cache.ts +30 -18
- package/src/session-route.ts +40 -0
- package/src/setup-core.ts +160 -0
- package/src/setup-surface.test.ts +202 -0
- package/src/setup-surface.ts +320 -0
- package/src/sso-token-store.test.ts +72 -0
- package/src/sso-token-store.ts +166 -0
- package/src/sso.ts +300 -0
- package/src/storage.ts +1 -1
- package/src/store-fs.ts +2 -2
- package/src/streaming-message.test.ts +262 -0
- package/src/streaming-message.ts +297 -0
- package/src/test-runtime.ts +1 -1
- package/src/thread-parent-context.test.ts +224 -0
- package/src/thread-parent-context.ts +159 -0
- package/src/token.test.ts +237 -50
- package/src/token.ts +162 -7
- package/src/user-agent.test.ts +86 -0
- package/src/user-agent.ts +53 -0
- package/src/webhook-timeouts.ts +27 -0
- package/src/welcome-card.test.ts +81 -0
- package/src/welcome-card.ts +57 -0
- package/test-api.ts +1 -0
- package/tsconfig.json +16 -0
- package/CHANGELOG.md +0 -101
- package/src/file-lock.ts +0 -1
- package/src/graph-users.test.ts +0 -66
- package/src/onboarding.ts +0 -381
- package/src/polls-store.test.ts +0 -38
- package/src/revoked-context.test.ts +0 -39
- package/src/token-response.test.ts +0 -23
|
@@ -1,53 +1,150 @@
|
|
|
1
|
+
import { formatAllowlistMatchMeta } from "openclaw/plugin-sdk/allow-from";
|
|
2
|
+
import { resolveInboundMentionDecision } from "openclaw/plugin-sdk/channel-inbound";
|
|
3
|
+
import {
|
|
4
|
+
logInboundDrop,
|
|
5
|
+
resolveInboundSessionEnvelopeContext,
|
|
6
|
+
} from "openclaw/plugin-sdk/channel-inbound";
|
|
7
|
+
import { resolveDualTextControlCommandGate } from "openclaw/plugin-sdk/command-gating";
|
|
8
|
+
import {
|
|
9
|
+
filterSupplementalContextItems,
|
|
10
|
+
resolveChannelContextVisibilityMode,
|
|
11
|
+
shouldIncludeSupplementalContext,
|
|
12
|
+
} from "openclaw/plugin-sdk/context-visibility-runtime";
|
|
13
|
+
import { evaluateSenderGroupAccessForPolicy } from "openclaw/plugin-sdk/group-access";
|
|
1
14
|
import {
|
|
2
|
-
DEFAULT_ACCOUNT_ID,
|
|
3
|
-
buildPendingHistoryContextFromMap,
|
|
4
|
-
clearHistoryEntriesIfEnabled,
|
|
5
15
|
dispatchReplyFromConfigWithSettledDispatcher,
|
|
16
|
+
hasFinalInboundReplyDispatch,
|
|
17
|
+
resolveInboundReplyDispatchCounts,
|
|
18
|
+
} from "openclaw/plugin-sdk/inbound-reply-dispatch";
|
|
19
|
+
import {
|
|
20
|
+
buildPendingHistoryContextFromMap,
|
|
6
21
|
DEFAULT_GROUP_HISTORY_LIMIT,
|
|
7
|
-
createScopedPairingAccess,
|
|
8
|
-
logInboundDrop,
|
|
9
|
-
evaluateSenderGroupAccessForPolicy,
|
|
10
|
-
resolveSenderScopedGroupPolicy,
|
|
11
22
|
recordPendingHistoryEntryIfEnabled,
|
|
12
|
-
resolveControlCommandGate,
|
|
13
|
-
resolveDefaultGroupPolicy,
|
|
14
|
-
isDangerousNameMatchingEnabled,
|
|
15
|
-
readStoreAllowFromForDmPolicy,
|
|
16
|
-
resolveMentionGating,
|
|
17
|
-
resolveInboundSessionEnvelopeContext,
|
|
18
|
-
formatAllowlistMatchMeta,
|
|
19
|
-
resolveEffectiveAllowFromLists,
|
|
20
|
-
resolveDmGroupAccessWithLists,
|
|
21
23
|
type HistoryEntry,
|
|
22
|
-
} from "openclaw/plugin-sdk/
|
|
24
|
+
} from "openclaw/plugin-sdk/reply-history";
|
|
23
25
|
import {
|
|
24
26
|
buildMSTeamsAttachmentPlaceholder,
|
|
25
27
|
buildMSTeamsMediaPayload,
|
|
26
28
|
type MSTeamsAttachmentLike,
|
|
27
29
|
summarizeMSTeamsHtmlAttachments,
|
|
28
30
|
} from "../attachments.js";
|
|
31
|
+
import { isRecord } from "../attachments/shared.js";
|
|
29
32
|
import type { StoredConversationReference } from "../conversation-store.js";
|
|
30
33
|
import { formatUnknownError } from "../errors.js";
|
|
34
|
+
import {
|
|
35
|
+
fetchThreadReplies,
|
|
36
|
+
formatThreadContext,
|
|
37
|
+
resolveTeamGroupId,
|
|
38
|
+
type GraphThreadMessage,
|
|
39
|
+
} from "../graph-thread.js";
|
|
40
|
+
import { resolveGraphChatId } from "../graph-upload.js";
|
|
31
41
|
import {
|
|
32
42
|
extractMSTeamsConversationMessageId,
|
|
43
|
+
extractMSTeamsQuoteInfo,
|
|
33
44
|
normalizeMSTeamsConversationId,
|
|
34
45
|
parseMSTeamsActivityTimestamp,
|
|
35
46
|
stripMSTeamsMentionTags,
|
|
47
|
+
translateMSTeamsDmConversationIdForGraph,
|
|
36
48
|
wasMSTeamsBotMentioned,
|
|
37
49
|
} from "../inbound.js";
|
|
38
|
-
import
|
|
50
|
+
import {
|
|
51
|
+
fetchParentMessageCached,
|
|
52
|
+
formatParentContextEvent,
|
|
53
|
+
markParentContextInjected,
|
|
54
|
+
shouldInjectParentContext,
|
|
55
|
+
summarizeParentMessage,
|
|
56
|
+
} from "../thread-parent-context.js";
|
|
57
|
+
|
|
58
|
+
function extractTextFromHtmlAttachments(attachments: MSTeamsAttachmentLike[]): string {
|
|
59
|
+
for (const attachment of attachments) {
|
|
60
|
+
if (attachment.contentType !== "text/html") {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const content = attachment.content;
|
|
64
|
+
const raw =
|
|
65
|
+
typeof content === "string"
|
|
66
|
+
? content
|
|
67
|
+
: isRecord(content) && typeof content.text === "string"
|
|
68
|
+
? content.text
|
|
69
|
+
: isRecord(content) && typeof content.body === "string"
|
|
70
|
+
? content.body
|
|
71
|
+
: "";
|
|
72
|
+
if (!raw) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const text = raw
|
|
76
|
+
.replace(/<at[^>]*>.*?<\/at>/gis, " ")
|
|
77
|
+
.replace(/<a\b[^>]*href=["']([^"']+)["'][^>]*>(.*?)<\/a>/gis, "$2 $1")
|
|
78
|
+
.replace(/<br\s*\/?>/gi, "\n")
|
|
79
|
+
.replace(/<\/p>/gi, "\n")
|
|
80
|
+
.replace(/<[^>]+>/g, " ")
|
|
81
|
+
.replace(/ /gi, " ")
|
|
82
|
+
.replace(/&/gi, "&")
|
|
83
|
+
.replace(/\s+/g, " ")
|
|
84
|
+
.trim();
|
|
85
|
+
if (text) {
|
|
86
|
+
return text;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return "";
|
|
90
|
+
}
|
|
91
|
+
import type { MSTeamsMessageHandlerDeps } from "../monitor-handler.types.js";
|
|
39
92
|
import {
|
|
40
93
|
isMSTeamsGroupAllowed,
|
|
41
94
|
resolveMSTeamsAllowlistMatch,
|
|
42
95
|
resolveMSTeamsReplyPolicy,
|
|
43
|
-
resolveMSTeamsRouteConfig,
|
|
44
96
|
} from "../policy.js";
|
|
45
97
|
import { extractMSTeamsPollVote } from "../polls.js";
|
|
46
98
|
import { createMSTeamsReplyDispatcher } from "../reply-dispatcher.js";
|
|
47
99
|
import { getMSTeamsRuntime } from "../runtime.js";
|
|
48
100
|
import type { MSTeamsTurnContext } from "../sdk-types.js";
|
|
49
101
|
import { recordMSTeamsSentMessage, wasMSTeamsMessageSent } from "../sent-message-cache.js";
|
|
102
|
+
import { resolveMSTeamsSenderAccess } from "./access.js";
|
|
50
103
|
import { resolveMSTeamsInboundMedia } from "./inbound-media.js";
|
|
104
|
+
import { resolveMSTeamsRouteSessionKey } from "./thread-session.js";
|
|
105
|
+
|
|
106
|
+
function buildStoredConversationReference(params: {
|
|
107
|
+
activity: MSTeamsTurnContext["activity"];
|
|
108
|
+
conversationId: string;
|
|
109
|
+
conversationType: string;
|
|
110
|
+
teamId?: string;
|
|
111
|
+
/** Thread root message ID for channel thread messages. */
|
|
112
|
+
threadId?: string;
|
|
113
|
+
}): StoredConversationReference {
|
|
114
|
+
const { activity, conversationId, conversationType, teamId, threadId } = params;
|
|
115
|
+
const from = activity.from;
|
|
116
|
+
const conversation = activity.conversation;
|
|
117
|
+
const agent = activity.recipient;
|
|
118
|
+
const clientInfo = activity.entities?.find((e) => e.type === "clientInfo") as
|
|
119
|
+
| { timezone?: string }
|
|
120
|
+
| undefined;
|
|
121
|
+
// Bot Framework requires `tenantId` on outbound proactive activities so the
|
|
122
|
+
// connector can route them to the correct Azure AD tenant; missing it causes
|
|
123
|
+
// HTTP 403. Channel activities often leave `conversation.tenantId` unset, so
|
|
124
|
+
// prefer the canonical `channelData.tenant.id` source when available.
|
|
125
|
+
const channelDataTenantId = activity.channelData?.tenant?.id;
|
|
126
|
+
const tenantId = channelDataTenantId ?? conversation?.tenantId;
|
|
127
|
+
const aadObjectId = from?.aadObjectId;
|
|
128
|
+
return {
|
|
129
|
+
activityId: activity.id,
|
|
130
|
+
user: from ? { id: from.id, name: from.name, aadObjectId: from.aadObjectId } : undefined,
|
|
131
|
+
agent,
|
|
132
|
+
bot: agent ? { id: agent.id, name: agent.name } : undefined,
|
|
133
|
+
conversation: {
|
|
134
|
+
id: conversationId,
|
|
135
|
+
conversationType,
|
|
136
|
+
tenantId,
|
|
137
|
+
},
|
|
138
|
+
...(tenantId ? { tenantId } : {}),
|
|
139
|
+
...(aadObjectId ? { aadObjectId } : {}),
|
|
140
|
+
teamId,
|
|
141
|
+
channelId: activity.channelId,
|
|
142
|
+
serviceUrl: activity.serviceUrl,
|
|
143
|
+
locale: activity.locale,
|
|
144
|
+
...(clientInfo?.timezone ? { timezone: clientInfo.timezone } : {}),
|
|
145
|
+
...(threadId ? { threadId } : {}),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
51
148
|
|
|
52
149
|
export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
|
53
150
|
const {
|
|
@@ -63,17 +160,16 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
|
|
63
160
|
log,
|
|
64
161
|
} = deps;
|
|
65
162
|
const core = getMSTeamsRuntime();
|
|
66
|
-
const pairing = createScopedPairingAccess({
|
|
67
|
-
core,
|
|
68
|
-
channel: "msteams",
|
|
69
|
-
accountId: DEFAULT_ACCOUNT_ID,
|
|
70
|
-
});
|
|
71
163
|
const logVerboseMessage = (message: string) => {
|
|
72
164
|
if (core.logging.shouldLogVerbose()) {
|
|
73
165
|
log.debug?.(message);
|
|
74
166
|
}
|
|
75
167
|
};
|
|
76
168
|
const msteamsCfg = cfg.channels?.msteams;
|
|
169
|
+
const contextVisibilityMode = resolveChannelContextVisibilityMode({
|
|
170
|
+
cfg,
|
|
171
|
+
channel: "msteams",
|
|
172
|
+
});
|
|
77
173
|
const historyLimit = Math.max(
|
|
78
174
|
0,
|
|
79
175
|
msteamsCfg?.historyLimit ??
|
|
@@ -92,7 +188,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
|
|
92
188
|
text: string;
|
|
93
189
|
attachments: MSTeamsAttachmentLike[];
|
|
94
190
|
wasMentioned: boolean;
|
|
95
|
-
|
|
191
|
+
implicitMentionKinds: Array<"reply_to_bot">;
|
|
96
192
|
};
|
|
97
193
|
|
|
98
194
|
const handleTeamsMessageNow = async (params: MSTeamsDebounceEntry) => {
|
|
@@ -101,8 +197,14 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
|
|
101
197
|
const rawText = params.rawText;
|
|
102
198
|
const text = params.text;
|
|
103
199
|
const attachments = params.attachments;
|
|
104
|
-
const attachmentPlaceholder = buildMSTeamsAttachmentPlaceholder(attachments
|
|
200
|
+
const attachmentPlaceholder = buildMSTeamsAttachmentPlaceholder(attachments, {
|
|
201
|
+
maxInlineBytes: mediaMaxBytes,
|
|
202
|
+
maxInlineTotalBytes: mediaMaxBytes,
|
|
203
|
+
});
|
|
105
204
|
const rawBody = text || attachmentPlaceholder;
|
|
205
|
+
const quoteInfo = extractMSTeamsQuoteInfo(attachments);
|
|
206
|
+
let quoteSenderId: string | undefined;
|
|
207
|
+
let quoteSenderName: string | undefined;
|
|
106
208
|
const from = activity.from;
|
|
107
209
|
const conversation = activity.conversation;
|
|
108
210
|
|
|
@@ -134,73 +236,48 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
|
|
134
236
|
const conversationId = normalizeMSTeamsConversationId(rawConversationId);
|
|
135
237
|
const conversationMessageId = extractMSTeamsConversationMessageId(rawConversationId);
|
|
136
238
|
const conversationType = conversation?.conversationType ?? "personal";
|
|
137
|
-
const isGroupChat = conversationType === "groupChat" || conversation?.isGroup === true;
|
|
138
|
-
const isChannel = conversationType === "channel";
|
|
139
|
-
const isDirectMessage = !isGroupChat && !isChannel;
|
|
140
|
-
|
|
141
|
-
const senderName = from.name ?? from.id;
|
|
142
|
-
const senderId = from.aadObjectId ?? from.id;
|
|
143
|
-
const dmPolicy = msteamsCfg?.dmPolicy ?? "pairing";
|
|
144
|
-
const storedAllowFrom = await readStoreAllowFromForDmPolicy({
|
|
145
|
-
provider: "msteams",
|
|
146
|
-
accountId: pairing.accountId,
|
|
147
|
-
dmPolicy,
|
|
148
|
-
readStore: pairing.readStoreForDmPolicy,
|
|
149
|
-
});
|
|
150
|
-
const useAccessGroups = cfg.commands?.useAccessGroups !== false;
|
|
151
|
-
|
|
152
|
-
// Check DM policy for direct messages.
|
|
153
|
-
const dmAllowFrom = msteamsCfg?.allowFrom ?? [];
|
|
154
|
-
const configuredDmAllowFrom = dmAllowFrom.map((v) => String(v));
|
|
155
|
-
const groupAllowFrom = msteamsCfg?.groupAllowFrom;
|
|
156
|
-
const resolvedAllowFromLists = resolveEffectiveAllowFromLists({
|
|
157
|
-
allowFrom: configuredDmAllowFrom,
|
|
158
|
-
groupAllowFrom,
|
|
159
|
-
storeAllowFrom: storedAllowFrom,
|
|
160
|
-
dmPolicy,
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg);
|
|
164
|
-
const groupPolicy =
|
|
165
|
-
!isDirectMessage && msteamsCfg
|
|
166
|
-
? (msteamsCfg.groupPolicy ?? defaultGroupPolicy ?? "allowlist")
|
|
167
|
-
: "disabled";
|
|
168
|
-
const effectiveGroupAllowFrom = resolvedAllowFromLists.effectiveGroupAllowFrom;
|
|
169
239
|
const teamId = activity.channelData?.team?.id;
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
240
|
+
// For channel thread messages, resolve the thread root message ID so outbound
|
|
241
|
+
// replies land in the correct thread. The root ID comes from the `messageid=`
|
|
242
|
+
// portion of conversation.id (preferred) or from activity.replyToId.
|
|
243
|
+
const threadId =
|
|
244
|
+
conversationType === "channel"
|
|
245
|
+
? (conversationMessageId ?? activity.replyToId ?? undefined)
|
|
246
|
+
: undefined;
|
|
247
|
+
const conversationRef = buildStoredConversationReference({
|
|
248
|
+
activity,
|
|
176
249
|
conversationId,
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const senderGroupPolicy = resolveSenderScopedGroupPolicy({
|
|
181
|
-
groupPolicy,
|
|
182
|
-
groupAllowFrom: effectiveGroupAllowFrom,
|
|
250
|
+
conversationType,
|
|
251
|
+
teamId,
|
|
252
|
+
threadId,
|
|
183
253
|
});
|
|
184
|
-
|
|
185
|
-
|
|
254
|
+
|
|
255
|
+
const {
|
|
186
256
|
dmPolicy,
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
257
|
+
senderId,
|
|
258
|
+
senderName,
|
|
259
|
+
pairing,
|
|
260
|
+
isDirectMessage,
|
|
261
|
+
channelGate,
|
|
262
|
+
access,
|
|
263
|
+
configuredDmAllowFrom,
|
|
264
|
+
effectiveDmAllowFrom,
|
|
265
|
+
effectiveGroupAllowFrom,
|
|
266
|
+
allowNameMatching,
|
|
267
|
+
groupPolicy,
|
|
268
|
+
} = await resolveMSTeamsSenderAccess({
|
|
269
|
+
cfg,
|
|
270
|
+
activity,
|
|
199
271
|
});
|
|
200
|
-
const
|
|
272
|
+
const useAccessGroups = cfg.commands?.useAccessGroups !== false;
|
|
273
|
+
const isChannel = conversationType === "channel";
|
|
201
274
|
|
|
202
275
|
if (isDirectMessage && msteamsCfg && access.decision !== "allow") {
|
|
203
276
|
if (access.reason === "dmPolicy=disabled") {
|
|
277
|
+
log.info("dropping dm (dms disabled)", {
|
|
278
|
+
sender: senderId,
|
|
279
|
+
label: senderName,
|
|
280
|
+
});
|
|
204
281
|
log.debug?.("dropping dm (dms disabled)");
|
|
205
282
|
return;
|
|
206
283
|
}
|
|
@@ -208,9 +285,14 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
|
|
208
285
|
allowFrom: effectiveDmAllowFrom,
|
|
209
286
|
senderId,
|
|
210
287
|
senderName,
|
|
211
|
-
allowNameMatching
|
|
288
|
+
allowNameMatching,
|
|
212
289
|
});
|
|
213
290
|
if (access.decision === "pairing") {
|
|
291
|
+
conversationStore.upsert(conversationId, conversationRef).catch((err) => {
|
|
292
|
+
log.debug?.("failed to save conversation reference", {
|
|
293
|
+
error: formatUnknownError(err),
|
|
294
|
+
});
|
|
295
|
+
});
|
|
214
296
|
const request = await pairing.upsertPairingRequest({
|
|
215
297
|
id: senderId,
|
|
216
298
|
meta: { name: senderName },
|
|
@@ -227,11 +309,25 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
|
|
227
309
|
label: senderName,
|
|
228
310
|
allowlistMatch: formatAllowlistMatchMeta(allowMatch),
|
|
229
311
|
});
|
|
312
|
+
log.info("dropping dm (not allowlisted)", {
|
|
313
|
+
sender: senderId,
|
|
314
|
+
label: senderName,
|
|
315
|
+
dmPolicy,
|
|
316
|
+
reason: access.reason,
|
|
317
|
+
allowlistMatch: formatAllowlistMatchMeta(allowMatch),
|
|
318
|
+
});
|
|
230
319
|
return;
|
|
231
320
|
}
|
|
232
321
|
|
|
233
322
|
if (!isDirectMessage && msteamsCfg) {
|
|
234
323
|
if (channelGate.allowlistConfigured && !channelGate.allowed) {
|
|
324
|
+
log.info("dropping group message (not in team/channel allowlist)", {
|
|
325
|
+
conversationId,
|
|
326
|
+
teamKey: channelGate.teamKey ?? "none",
|
|
327
|
+
channelKey: channelGate.channelKey ?? "none",
|
|
328
|
+
channelMatchKey: channelGate.channelMatchKey ?? "none",
|
|
329
|
+
channelMatchSource: channelGate.channelMatchSource ?? "none",
|
|
330
|
+
});
|
|
235
331
|
log.debug?.("dropping group message (not in team/channel allowlist)", {
|
|
236
332
|
conversationId,
|
|
237
333
|
teamKey: channelGate.teamKey ?? "none",
|
|
@@ -250,17 +346,23 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
|
|
250
346
|
allowFrom,
|
|
251
347
|
senderId,
|
|
252
348
|
senderName,
|
|
253
|
-
allowNameMatching
|
|
349
|
+
allowNameMatching,
|
|
254
350
|
}).allowed,
|
|
255
351
|
});
|
|
256
352
|
|
|
257
353
|
if (!senderGroupAccess.allowed && senderGroupAccess.reason === "disabled") {
|
|
354
|
+
log.info("dropping group message (groupPolicy: disabled)", {
|
|
355
|
+
conversationId,
|
|
356
|
+
});
|
|
258
357
|
log.debug?.("dropping group message (groupPolicy: disabled)", {
|
|
259
358
|
conversationId,
|
|
260
359
|
});
|
|
261
360
|
return;
|
|
262
361
|
}
|
|
263
362
|
if (!senderGroupAccess.allowed && senderGroupAccess.reason === "empty_allowlist") {
|
|
363
|
+
log.info("dropping group message (groupPolicy: allowlist, no allowlist)", {
|
|
364
|
+
conversationId,
|
|
365
|
+
});
|
|
264
366
|
log.debug?.("dropping group message (groupPolicy: allowlist, no allowlist)", {
|
|
265
367
|
conversationId,
|
|
266
368
|
});
|
|
@@ -271,13 +373,18 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
|
|
271
373
|
allowFrom: effectiveGroupAllowFrom,
|
|
272
374
|
senderId,
|
|
273
375
|
senderName,
|
|
274
|
-
allowNameMatching
|
|
376
|
+
allowNameMatching,
|
|
275
377
|
});
|
|
276
378
|
log.debug?.("dropping group message (not in groupAllowFrom)", {
|
|
277
379
|
sender: senderId,
|
|
278
380
|
label: senderName,
|
|
279
381
|
allowlistMatch: formatAllowlistMatchMeta(allowMatch),
|
|
280
382
|
});
|
|
383
|
+
log.info("dropping group message (not in groupAllowFrom)", {
|
|
384
|
+
sender: senderId,
|
|
385
|
+
label: senderName,
|
|
386
|
+
allowlistMatch: formatAllowlistMatchMeta(allowMatch),
|
|
387
|
+
});
|
|
281
388
|
return;
|
|
282
389
|
}
|
|
283
390
|
}
|
|
@@ -288,27 +395,24 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
|
|
288
395
|
allowFrom: commandDmAllowFrom,
|
|
289
396
|
senderId,
|
|
290
397
|
senderName,
|
|
291
|
-
allowNameMatching
|
|
398
|
+
allowNameMatching,
|
|
292
399
|
});
|
|
293
400
|
const groupAllowedForCommands = isMSTeamsGroupAllowed({
|
|
294
401
|
groupPolicy: "allowlist",
|
|
295
402
|
allowFrom: effectiveGroupAllowFrom,
|
|
296
403
|
senderId,
|
|
297
404
|
senderName,
|
|
298
|
-
allowNameMatching
|
|
405
|
+
allowNameMatching,
|
|
299
406
|
});
|
|
300
|
-
const
|
|
301
|
-
const commandGate = resolveControlCommandGate({
|
|
407
|
+
const { commandAuthorized, shouldBlock } = resolveDualTextControlCommandGate({
|
|
302
408
|
useAccessGroups,
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
hasControlCommand: hasControlCommandInMessage,
|
|
409
|
+
primaryConfigured: commandDmAllowFrom.length > 0,
|
|
410
|
+
primaryAllowed: ownerAllowedForCommands,
|
|
411
|
+
secondaryConfigured: effectiveGroupAllowFrom.length > 0,
|
|
412
|
+
secondaryAllowed: groupAllowedForCommands,
|
|
413
|
+
hasControlCommand: core.channel.text.hasControlCommand(text, cfg),
|
|
309
414
|
});
|
|
310
|
-
|
|
311
|
-
if (commandGate.shouldBlock) {
|
|
415
|
+
if (shouldBlock) {
|
|
312
416
|
logInboundDrop({
|
|
313
417
|
log: logVerboseMessage,
|
|
314
418
|
channel: "msteams",
|
|
@@ -318,23 +422,6 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
|
|
318
422
|
return;
|
|
319
423
|
}
|
|
320
424
|
|
|
321
|
-
// Build conversation reference for proactive replies.
|
|
322
|
-
const agent = activity.recipient;
|
|
323
|
-
const conversationRef: StoredConversationReference = {
|
|
324
|
-
activityId: activity.id,
|
|
325
|
-
user: { id: from.id, name: from.name, aadObjectId: from.aadObjectId },
|
|
326
|
-
agent,
|
|
327
|
-
bot: agent ? { id: agent.id, name: agent.name } : undefined,
|
|
328
|
-
conversation: {
|
|
329
|
-
id: conversationId,
|
|
330
|
-
conversationType,
|
|
331
|
-
tenantId: conversation?.tenantId,
|
|
332
|
-
},
|
|
333
|
-
teamId,
|
|
334
|
-
channelId: activity.channelId,
|
|
335
|
-
serviceUrl: activity.serviceUrl,
|
|
336
|
-
locale: activity.locale,
|
|
337
|
-
};
|
|
338
425
|
conversationStore.upsert(conversationId, conversationRef).catch((err) => {
|
|
339
426
|
log.debug?.("failed to save conversation reference", {
|
|
340
427
|
error: formatUnknownError(err),
|
|
@@ -384,12 +471,25 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
|
|
384
471
|
const route = core.channel.routing.resolveAgentRoute({
|
|
385
472
|
cfg,
|
|
386
473
|
channel: "msteams",
|
|
474
|
+
teamId,
|
|
387
475
|
peer: {
|
|
388
476
|
kind: isDirectMessage ? "direct" : isChannel ? "channel" : "group",
|
|
389
477
|
id: isDirectMessage ? senderId : conversationId,
|
|
390
478
|
},
|
|
391
479
|
});
|
|
392
480
|
|
|
481
|
+
// Isolate channel thread sessions: each thread gets its own session key so
|
|
482
|
+
// context does not bleed across threads. Prefer conversationMessageId (the
|
|
483
|
+
// ;messageid= portion of conversation.id, i.e. the thread root) over
|
|
484
|
+
// activity.replyToId (which may point to a non-root parent in deep threads).
|
|
485
|
+
// DMs and group chats are unaffected — only channel thread replies fork.
|
|
486
|
+
route.sessionKey = resolveMSTeamsRouteSessionKey({
|
|
487
|
+
baseSessionKey: route.sessionKey,
|
|
488
|
+
isChannel,
|
|
489
|
+
conversationMessageId,
|
|
490
|
+
replyToId: activity.replyToId,
|
|
491
|
+
});
|
|
492
|
+
|
|
393
493
|
const preview = rawBody.replace(/\s+/g, " ").slice(0, 160);
|
|
394
494
|
const inboundLabel = isDirectMessage
|
|
395
495
|
? `Teams DM from ${senderName}`
|
|
@@ -409,17 +509,24 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
|
|
409
509
|
channelConfig,
|
|
410
510
|
});
|
|
411
511
|
const timestamp = parseMSTeamsActivityTimestamp(activity.timestamp);
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
const mentionGate = resolveMentionGating({
|
|
415
|
-
requireMention: Boolean(requireMention),
|
|
512
|
+
const mentionDecision = resolveInboundMentionDecision({
|
|
513
|
+
facts: {
|
|
416
514
|
canDetectMention: true,
|
|
417
515
|
wasMentioned: params.wasMentioned,
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
516
|
+
implicitMentionKinds: params.implicitMentionKinds,
|
|
517
|
+
},
|
|
518
|
+
policy: {
|
|
519
|
+
isGroup: !isDirectMessage,
|
|
520
|
+
requireMention,
|
|
521
|
+
allowTextCommands: false,
|
|
522
|
+
hasControlCommand: false,
|
|
523
|
+
commandAuthorized: false,
|
|
524
|
+
},
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
if (!isDirectMessage) {
|
|
528
|
+
const mentioned = mentionDecision.effectiveWasMentioned;
|
|
529
|
+
if (requireMention && mentionDecision.shouldSkip) {
|
|
423
530
|
log.debug?.("skipping message (mention required)", {
|
|
424
531
|
teamId,
|
|
425
532
|
channelId,
|
|
@@ -440,6 +547,42 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
|
|
440
547
|
return;
|
|
441
548
|
}
|
|
442
549
|
}
|
|
550
|
+
let graphConversationId = translateMSTeamsDmConversationIdForGraph({
|
|
551
|
+
isDirectMessage,
|
|
552
|
+
conversationId,
|
|
553
|
+
aadObjectId: from.aadObjectId,
|
|
554
|
+
appId,
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
// For personal DMs the Bot Framework conversation ID (`a:...`) and the
|
|
558
|
+
// synthetic `19:{userId}_{appId}@unq.gbl.spaces` format produced by
|
|
559
|
+
// translateMSTeamsDmConversationIdForGraph are not always accepted by the
|
|
560
|
+
// Graph `/chats/{chatId}/messages` endpoint. Resolve the real Graph chat
|
|
561
|
+
// ID via the API (with conversation store caching) so the Graph media
|
|
562
|
+
// download fallback works when the direct Bot Framework download fails.
|
|
563
|
+
if (isDirectMessage && conversationId.startsWith("a:")) {
|
|
564
|
+
const cached = await conversationStore.get(conversationId);
|
|
565
|
+
if (cached?.graphChatId) {
|
|
566
|
+
graphConversationId = cached.graphChatId;
|
|
567
|
+
} else {
|
|
568
|
+
try {
|
|
569
|
+
const resolved = await resolveGraphChatId({
|
|
570
|
+
botFrameworkConversationId: conversationId,
|
|
571
|
+
userAadObjectId: from.aadObjectId ?? undefined,
|
|
572
|
+
tokenProvider,
|
|
573
|
+
});
|
|
574
|
+
if (resolved) {
|
|
575
|
+
graphConversationId = resolved;
|
|
576
|
+
conversationStore
|
|
577
|
+
.upsert(conversationId, { ...conversationRef, graphChatId: resolved })
|
|
578
|
+
.catch(() => {});
|
|
579
|
+
}
|
|
580
|
+
} catch {
|
|
581
|
+
log.debug?.("failed to resolve Graph chat ID for inbound media", { conversationId });
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
443
586
|
const mediaList = await resolveMSTeamsInboundMedia({
|
|
444
587
|
attachments,
|
|
445
588
|
htmlSummary: htmlSummary ?? undefined,
|
|
@@ -448,8 +591,9 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
|
|
448
591
|
allowHosts: msteamsCfg?.mediaAllowHosts,
|
|
449
592
|
authAllowHosts: msteamsCfg?.mediaAuthAllowHosts,
|
|
450
593
|
conversationType,
|
|
451
|
-
conversationId,
|
|
594
|
+
conversationId: graphConversationId,
|
|
452
595
|
conversationMessageId: conversationMessageId ?? undefined,
|
|
596
|
+
serviceUrl: activity.serviceUrl,
|
|
453
597
|
activity: {
|
|
454
598
|
id: activity.id,
|
|
455
599
|
replyToId: activity.replyToId,
|
|
@@ -461,6 +605,90 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
|
|
461
605
|
});
|
|
462
606
|
|
|
463
607
|
const mediaPayload = buildMSTeamsMediaPayload(mediaList);
|
|
608
|
+
|
|
609
|
+
// Fetch thread history when the message is a reply inside a Teams channel thread.
|
|
610
|
+
// This is a best-effort enhancement; errors are logged and do not block the reply.
|
|
611
|
+
//
|
|
612
|
+
// We also enqueue a compact `Replying to @sender: …` system event when the parent
|
|
613
|
+
// is resolvable. On brand-new thread sessions (see PR #62713), this gives the agent
|
|
614
|
+
// immediate parent context even before the fuller `[Thread history]` block is assembled.
|
|
615
|
+
// Parent fetches are cached (5 min LRU, 100 entries) and per-session deduped so
|
|
616
|
+
// consecutive replies in the same thread do not re-inject identical context.
|
|
617
|
+
let threadContext: string | undefined;
|
|
618
|
+
if (activity.replyToId && isChannel && teamId) {
|
|
619
|
+
try {
|
|
620
|
+
const graphToken = await tokenProvider.getAccessToken("https://graph.microsoft.com");
|
|
621
|
+
const groupId = await resolveTeamGroupId(graphToken, teamId);
|
|
622
|
+
// Use allSettled so a failure in one fetch does not discard the other.
|
|
623
|
+
// For example, reply-fetch 403 should not throw away a successful parent fetch.
|
|
624
|
+
const [parentResult, repliesResult] = await Promise.allSettled([
|
|
625
|
+
fetchParentMessageCached(graphToken, groupId, conversationId, activity.replyToId),
|
|
626
|
+
fetchThreadReplies(graphToken, groupId, conversationId, activity.replyToId),
|
|
627
|
+
]);
|
|
628
|
+
const parentMsg = parentResult.status === "fulfilled" ? parentResult.value : undefined;
|
|
629
|
+
const replies = repliesResult.status === "fulfilled" ? repliesResult.value : [];
|
|
630
|
+
if (parentResult.status === "rejected") {
|
|
631
|
+
log.debug?.("failed to fetch parent message", {
|
|
632
|
+
error: formatUnknownError(parentResult.reason),
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
if (repliesResult.status === "rejected") {
|
|
636
|
+
log.debug?.("failed to fetch thread replies", {
|
|
637
|
+
error: formatUnknownError(repliesResult.reason),
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
const isThreadSenderAllowed = (msg: GraphThreadMessage) =>
|
|
641
|
+
groupPolicy === "allowlist"
|
|
642
|
+
? resolveMSTeamsAllowlistMatch({
|
|
643
|
+
allowFrom: effectiveGroupAllowFrom,
|
|
644
|
+
senderId: msg.from?.user?.id ?? "",
|
|
645
|
+
senderName: msg.from?.user?.displayName,
|
|
646
|
+
allowNameMatching,
|
|
647
|
+
}).allowed
|
|
648
|
+
: true;
|
|
649
|
+
const parentSummary = summarizeParentMessage(parentMsg);
|
|
650
|
+
const visibleParentMessages = parentMsg
|
|
651
|
+
? filterSupplementalContextItems({
|
|
652
|
+
items: [parentMsg],
|
|
653
|
+
mode: contextVisibilityMode,
|
|
654
|
+
kind: "thread",
|
|
655
|
+
isSenderAllowed: isThreadSenderAllowed,
|
|
656
|
+
}).items
|
|
657
|
+
: [];
|
|
658
|
+
if (
|
|
659
|
+
parentSummary &&
|
|
660
|
+
visibleParentMessages.length > 0 &&
|
|
661
|
+
shouldInjectParentContext(route.sessionKey, activity.replyToId)
|
|
662
|
+
) {
|
|
663
|
+
core.system.enqueueSystemEvent(formatParentContextEvent(parentSummary), {
|
|
664
|
+
sessionKey: route.sessionKey,
|
|
665
|
+
contextKey: `msteams:thread-parent:${conversationId}:${activity.replyToId}`,
|
|
666
|
+
});
|
|
667
|
+
markParentContextInjected(route.sessionKey, activity.replyToId);
|
|
668
|
+
}
|
|
669
|
+
const allMessages = parentMsg ? [parentMsg, ...replies] : replies;
|
|
670
|
+
quoteSenderId = parentMsg?.from?.user?.id ?? parentMsg?.from?.application?.id ?? undefined;
|
|
671
|
+
quoteSenderName =
|
|
672
|
+
parentMsg?.from?.user?.displayName ??
|
|
673
|
+
parentMsg?.from?.application?.displayName ??
|
|
674
|
+
quoteInfo?.sender;
|
|
675
|
+
const { items: threadMessages } = filterSupplementalContextItems({
|
|
676
|
+
items: allMessages,
|
|
677
|
+
mode: contextVisibilityMode,
|
|
678
|
+
kind: "thread",
|
|
679
|
+
isSenderAllowed: isThreadSenderAllowed,
|
|
680
|
+
});
|
|
681
|
+
const formatted = formatThreadContext(threadMessages, activity.id);
|
|
682
|
+
if (formatted) {
|
|
683
|
+
threadContext = formatted;
|
|
684
|
+
}
|
|
685
|
+
} catch (err) {
|
|
686
|
+
log.debug?.("failed to fetch thread history", { error: formatUnknownError(err) });
|
|
687
|
+
// Graceful degradation: thread history is an optional enhancement.
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
quoteSenderName ??= quoteInfo?.sender;
|
|
691
|
+
|
|
464
692
|
const envelopeFrom = isDirectMessage ? senderName : conversationType;
|
|
465
693
|
const { storePath, envelopeOptions, previousTimestamp } = resolveInboundSessionEnvelopeContext({
|
|
466
694
|
cfg,
|
|
@@ -504,10 +732,40 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
|
|
504
732
|
}))
|
|
505
733
|
: undefined;
|
|
506
734
|
const commandBody = text.trim();
|
|
735
|
+
const quoteSenderAllowed =
|
|
736
|
+
quoteInfo && quoteInfo.sender
|
|
737
|
+
? !isChannel || groupPolicy !== "allowlist"
|
|
738
|
+
? true
|
|
739
|
+
: resolveMSTeamsAllowlistMatch({
|
|
740
|
+
allowFrom: effectiveGroupAllowFrom,
|
|
741
|
+
senderId: quoteSenderId ?? "",
|
|
742
|
+
senderName: quoteSenderName,
|
|
743
|
+
allowNameMatching,
|
|
744
|
+
}).allowed
|
|
745
|
+
: true;
|
|
746
|
+
const includeQuoteContext =
|
|
747
|
+
quoteInfo &&
|
|
748
|
+
shouldIncludeSupplementalContext({
|
|
749
|
+
mode: contextVisibilityMode,
|
|
750
|
+
kind: "quote",
|
|
751
|
+
senderAllowed: quoteSenderAllowed,
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
// Prepend thread history to the agent body so the agent has full thread context.
|
|
755
|
+
const bodyForAgent = threadContext
|
|
756
|
+
? `[Thread history]\n${threadContext}\n[/Thread history]\n\n${rawBody}`
|
|
757
|
+
: rawBody;
|
|
758
|
+
|
|
759
|
+
// For Teams *channel* messages (not group chats / DMs), preserve the
|
|
760
|
+
// `teamId/channelId` pair on NativeChannelId so downstream action handlers
|
|
761
|
+
// can route through `/teams/{teamId}/channels/{channelId}` via Graph API.
|
|
762
|
+
// The bare conversation id (`19:...@thread.tacv2`) is insufficient on its
|
|
763
|
+
// own because channel Graph endpoints require the owning team id too.
|
|
764
|
+
const nativeChannelId = isChannel && teamId ? `${teamId}/${conversationId}` : undefined;
|
|
507
765
|
|
|
508
766
|
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
509
767
|
Body: combinedBody,
|
|
510
|
-
BodyForAgent:
|
|
768
|
+
BodyForAgent: bodyForAgent,
|
|
511
769
|
InboundHistory: inboundHistory,
|
|
512
770
|
RawBody: rawBody,
|
|
513
771
|
CommandBody: commandBody,
|
|
@@ -519,34 +777,32 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
|
|
519
777
|
ChatType: isDirectMessage ? "direct" : isChannel ? "channel" : "group",
|
|
520
778
|
ConversationLabel: envelopeFrom,
|
|
521
779
|
GroupSubject: !isDirectMessage ? conversationType : undefined,
|
|
780
|
+
GroupSpace: teamId,
|
|
522
781
|
SenderName: senderName,
|
|
523
782
|
SenderId: senderId,
|
|
524
783
|
Provider: "msteams" as const,
|
|
525
784
|
Surface: "msteams" as const,
|
|
526
785
|
MessageSid: activity.id,
|
|
527
786
|
Timestamp: timestamp?.getTime() ?? Date.now(),
|
|
528
|
-
WasMentioned: isDirectMessage ||
|
|
787
|
+
WasMentioned: isDirectMessage || mentionDecision.effectiveWasMentioned,
|
|
529
788
|
CommandAuthorized: commandAuthorized,
|
|
530
789
|
OriginatingChannel: "msteams" as const,
|
|
531
790
|
OriginatingTo: teamsTo,
|
|
791
|
+
NativeChannelId: nativeChannelId,
|
|
792
|
+
ReplyToId: activity.replyToId ?? undefined,
|
|
793
|
+
ReplyToBody: includeQuoteContext ? quoteInfo?.body : undefined,
|
|
794
|
+
ReplyToSender: includeQuoteContext ? quoteInfo?.sender : undefined,
|
|
795
|
+
ReplyToIsQuote: quoteInfo ? true : undefined,
|
|
532
796
|
...mediaPayload,
|
|
533
797
|
});
|
|
534
798
|
|
|
535
|
-
await core.channel.session.recordInboundSession({
|
|
536
|
-
storePath,
|
|
537
|
-
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
|
|
538
|
-
ctx: ctxPayload,
|
|
539
|
-
onRecordError: (err) => {
|
|
540
|
-
logVerboseMessage(`msteams: failed updating session meta: ${String(err)}`);
|
|
541
|
-
},
|
|
542
|
-
});
|
|
543
|
-
|
|
544
799
|
logVerboseMessage(`msteams inbound: from=${ctxPayload.From} preview="${preview}"`);
|
|
545
800
|
|
|
546
801
|
const sharePointSiteId = msteamsCfg?.sharePointSiteId;
|
|
547
802
|
const { dispatcher, replyOptions, markDispatchIdle } = createMSTeamsReplyDispatcher({
|
|
548
803
|
cfg,
|
|
549
804
|
agentId: route.agentId,
|
|
805
|
+
sessionKey: route.sessionKey,
|
|
550
806
|
accountId: route.accountId,
|
|
551
807
|
runtime,
|
|
552
808
|
log,
|
|
@@ -565,48 +821,93 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
|
|
565
821
|
sharePointSiteId,
|
|
566
822
|
});
|
|
567
823
|
|
|
824
|
+
// Use Teams clientInfo timezone if no explicit userTimezone is configured.
|
|
825
|
+
// This ensures the agent knows the sender's timezone for time-aware responses
|
|
826
|
+
// and proactive sends within the same session.
|
|
827
|
+
const activityClientInfo = activity.entities?.find((e) => e.type === "clientInfo") as
|
|
828
|
+
| { timezone?: string }
|
|
829
|
+
| undefined;
|
|
830
|
+
const senderTimezone = activityClientInfo?.timezone || conversationRef.timezone;
|
|
831
|
+
const configOverride =
|
|
832
|
+
senderTimezone && !cfg.agents?.defaults?.userTimezone
|
|
833
|
+
? {
|
|
834
|
+
agents: {
|
|
835
|
+
defaults: { ...cfg.agents?.defaults, userTimezone: senderTimezone },
|
|
836
|
+
},
|
|
837
|
+
}
|
|
838
|
+
: undefined;
|
|
839
|
+
|
|
568
840
|
log.info("dispatching to agent", { sessionKey: route.sessionKey });
|
|
569
841
|
try {
|
|
570
|
-
const
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
842
|
+
const turnResult = await core.channel.turn.run({
|
|
843
|
+
channel: "msteams",
|
|
844
|
+
accountId: route.accountId,
|
|
845
|
+
raw: context,
|
|
846
|
+
adapter: {
|
|
847
|
+
ingest: () => ({
|
|
848
|
+
id: activity.id ?? `${teamsFrom}:${Date.now()}`,
|
|
849
|
+
timestamp: timestamp?.getTime(),
|
|
850
|
+
rawText: rawBody,
|
|
851
|
+
textForAgent: bodyForAgent,
|
|
852
|
+
textForCommands: commandBody,
|
|
853
|
+
raw: activity,
|
|
854
|
+
}),
|
|
855
|
+
resolveTurn: () => ({
|
|
856
|
+
channel: "msteams",
|
|
857
|
+
accountId: route.accountId,
|
|
858
|
+
routeSessionKey: route.sessionKey,
|
|
859
|
+
storePath,
|
|
860
|
+
ctxPayload,
|
|
861
|
+
recordInboundSession: core.channel.session.recordInboundSession,
|
|
862
|
+
record: {
|
|
863
|
+
onRecordError: (err) => {
|
|
864
|
+
logVerboseMessage(
|
|
865
|
+
`msteams: failed updating session meta: ${formatUnknownError(err)}`,
|
|
866
|
+
);
|
|
867
|
+
},
|
|
868
|
+
},
|
|
869
|
+
history: {
|
|
870
|
+
isGroup: isRoomish,
|
|
871
|
+
historyKey,
|
|
872
|
+
historyMap: conversationHistories,
|
|
873
|
+
limit: historyLimit,
|
|
874
|
+
},
|
|
875
|
+
onPreDispatchFailure: () =>
|
|
876
|
+
core.channel.reply.settleReplyDispatcher({
|
|
877
|
+
dispatcher,
|
|
878
|
+
onSettled: () => markDispatchIdle(),
|
|
879
|
+
}),
|
|
880
|
+
runDispatch: () =>
|
|
881
|
+
dispatchReplyFromConfigWithSettledDispatcher({
|
|
882
|
+
cfg,
|
|
883
|
+
ctxPayload,
|
|
884
|
+
dispatcher,
|
|
885
|
+
onSettled: () => markDispatchIdle(),
|
|
886
|
+
replyOptions,
|
|
887
|
+
configOverride,
|
|
888
|
+
}),
|
|
889
|
+
}),
|
|
576
890
|
},
|
|
577
|
-
replyOptions,
|
|
578
891
|
});
|
|
892
|
+
const dispatchResult = turnResult.dispatched ? turnResult.dispatchResult : undefined;
|
|
893
|
+
const queuedFinal = dispatchResult?.queuedFinal ?? false;
|
|
894
|
+
const counts = resolveInboundReplyDispatchCounts(dispatchResult);
|
|
895
|
+
const hasFinalResponse = hasFinalInboundReplyDispatch(dispatchResult);
|
|
579
896
|
|
|
580
897
|
log.info("dispatch complete", { queuedFinal, counts });
|
|
581
898
|
|
|
582
|
-
if (!
|
|
583
|
-
if (isRoomish && historyKey) {
|
|
584
|
-
clearHistoryEntriesIfEnabled({
|
|
585
|
-
historyMap: conversationHistories,
|
|
586
|
-
historyKey,
|
|
587
|
-
limit: historyLimit,
|
|
588
|
-
});
|
|
589
|
-
}
|
|
899
|
+
if (!hasFinalResponse) {
|
|
590
900
|
return;
|
|
591
901
|
}
|
|
592
902
|
const finalCount = counts.final;
|
|
593
903
|
logVerboseMessage(
|
|
594
904
|
`msteams: delivered ${finalCount} reply${finalCount === 1 ? "" : "ies"} to ${teamsTo}`,
|
|
595
905
|
);
|
|
596
|
-
if (isRoomish && historyKey) {
|
|
597
|
-
clearHistoryEntriesIfEnabled({
|
|
598
|
-
historyMap: conversationHistories,
|
|
599
|
-
historyKey,
|
|
600
|
-
limit: historyLimit,
|
|
601
|
-
});
|
|
602
|
-
}
|
|
603
906
|
} catch (err) {
|
|
604
|
-
log.error("dispatch failed", { error:
|
|
605
|
-
runtime.error?.(`msteams dispatch failed: ${
|
|
907
|
+
log.error("dispatch failed", { error: formatUnknownError(err) });
|
|
908
|
+
runtime.error?.(`msteams dispatch failed: ${formatUnknownError(err)}`);
|
|
606
909
|
try {
|
|
607
|
-
await context.sendActivity(
|
|
608
|
-
`⚠️ Agent failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
609
|
-
);
|
|
910
|
+
await context.sendActivity("⚠️ Something went wrong. Please try again.");
|
|
610
911
|
} catch {
|
|
611
912
|
// Best effort.
|
|
612
913
|
}
|
|
@@ -656,34 +957,36 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
|
|
656
957
|
.filter(Boolean)
|
|
657
958
|
.join("\n");
|
|
658
959
|
const wasMentioned = entries.some((entry) => entry.wasMentioned);
|
|
659
|
-
const
|
|
960
|
+
const implicitMentionKinds = entries.flatMap((entry) => entry.implicitMentionKinds);
|
|
660
961
|
await handleTeamsMessageNow({
|
|
661
962
|
context: last.context,
|
|
662
963
|
rawText: combinedRawText,
|
|
663
964
|
text: combinedText,
|
|
664
965
|
attachments: [],
|
|
665
966
|
wasMentioned,
|
|
666
|
-
|
|
967
|
+
implicitMentionKinds,
|
|
667
968
|
});
|
|
668
969
|
},
|
|
669
970
|
onError: (err) => {
|
|
670
|
-
runtime.error?.(`msteams debounce flush failed: ${
|
|
971
|
+
runtime.error?.(`msteams debounce flush failed: ${formatUnknownError(err)}`);
|
|
671
972
|
},
|
|
672
973
|
});
|
|
673
974
|
|
|
674
975
|
return async function handleTeamsMessage(context: MSTeamsTurnContext) {
|
|
675
976
|
const activity = context.activity;
|
|
676
|
-
const rawText = activity.text?.trim() ?? "";
|
|
677
|
-
const text = stripMSTeamsMentionTags(rawText);
|
|
678
977
|
const attachments = Array.isArray(activity.attachments)
|
|
679
978
|
? (activity.attachments as unknown as MSTeamsAttachmentLike[])
|
|
680
979
|
: [];
|
|
980
|
+
const rawText = activity.text?.trim() ?? "";
|
|
981
|
+
const htmlText = extractTextFromHtmlAttachments(attachments);
|
|
982
|
+
const text = stripMSTeamsMentionTags(rawText || htmlText);
|
|
681
983
|
const wasMentioned = wasMSTeamsBotMentioned(activity);
|
|
682
984
|
const conversationId = normalizeMSTeamsConversationId(activity.conversation?.id ?? "");
|
|
683
985
|
const replyToId = activity.replyToId ?? undefined;
|
|
684
|
-
const
|
|
685
|
-
conversationId && replyToId && wasMSTeamsMessageSent(conversationId, replyToId)
|
|
686
|
-
|
|
986
|
+
const implicitMentionKinds: Array<"reply_to_bot"> =
|
|
987
|
+
conversationId && replyToId && wasMSTeamsMessageSent(conversationId, replyToId)
|
|
988
|
+
? ["reply_to_bot"]
|
|
989
|
+
: [];
|
|
687
990
|
|
|
688
991
|
await inboundDebouncer.enqueue({
|
|
689
992
|
context,
|
|
@@ -691,7 +994,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
|
|
691
994
|
text,
|
|
692
995
|
attachments,
|
|
693
996
|
wasMentioned,
|
|
694
|
-
|
|
997
|
+
implicitMentionKinds,
|
|
695
998
|
});
|
|
696
999
|
};
|
|
697
1000
|
}
|