@openclaw/msteams 2026.3.13 → 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 +138 -1
- 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 +163 -418
- 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 +145 -4
- 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 +161 -4
- package/src/graph-upload.ts +147 -56
- 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 +504 -23
- 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 +470 -164
- 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 +281 -79
- 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 +11 -5
- 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 -107
- 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
|
-
resolveDualTextControlCommandGate,
|
|
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,14 +395,14 @@ 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
407
|
const { commandAuthorized, shouldBlock } = resolveDualTextControlCommandGate({
|
|
301
408
|
useAccessGroups,
|
|
@@ -315,23 +422,6 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
|
|
315
422
|
return;
|
|
316
423
|
}
|
|
317
424
|
|
|
318
|
-
// Build conversation reference for proactive replies.
|
|
319
|
-
const agent = activity.recipient;
|
|
320
|
-
const conversationRef: StoredConversationReference = {
|
|
321
|
-
activityId: activity.id,
|
|
322
|
-
user: { id: from.id, name: from.name, aadObjectId: from.aadObjectId },
|
|
323
|
-
agent,
|
|
324
|
-
bot: agent ? { id: agent.id, name: agent.name } : undefined,
|
|
325
|
-
conversation: {
|
|
326
|
-
id: conversationId,
|
|
327
|
-
conversationType,
|
|
328
|
-
tenantId: conversation?.tenantId,
|
|
329
|
-
},
|
|
330
|
-
teamId,
|
|
331
|
-
channelId: activity.channelId,
|
|
332
|
-
serviceUrl: activity.serviceUrl,
|
|
333
|
-
locale: activity.locale,
|
|
334
|
-
};
|
|
335
425
|
conversationStore.upsert(conversationId, conversationRef).catch((err) => {
|
|
336
426
|
log.debug?.("failed to save conversation reference", {
|
|
337
427
|
error: formatUnknownError(err),
|
|
@@ -381,12 +471,25 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
|
|
381
471
|
const route = core.channel.routing.resolveAgentRoute({
|
|
382
472
|
cfg,
|
|
383
473
|
channel: "msteams",
|
|
474
|
+
teamId,
|
|
384
475
|
peer: {
|
|
385
476
|
kind: isDirectMessage ? "direct" : isChannel ? "channel" : "group",
|
|
386
477
|
id: isDirectMessage ? senderId : conversationId,
|
|
387
478
|
},
|
|
388
479
|
});
|
|
389
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
|
+
|
|
390
493
|
const preview = rawBody.replace(/\s+/g, " ").slice(0, 160);
|
|
391
494
|
const inboundLabel = isDirectMessage
|
|
392
495
|
? `Teams DM from ${senderName}`
|
|
@@ -406,17 +509,24 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
|
|
406
509
|
channelConfig,
|
|
407
510
|
});
|
|
408
511
|
const timestamp = parseMSTeamsActivityTimestamp(activity.timestamp);
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
const mentionGate = resolveMentionGating({
|
|
412
|
-
requireMention: Boolean(requireMention),
|
|
512
|
+
const mentionDecision = resolveInboundMentionDecision({
|
|
513
|
+
facts: {
|
|
413
514
|
canDetectMention: true,
|
|
414
515
|
wasMentioned: params.wasMentioned,
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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) {
|
|
420
530
|
log.debug?.("skipping message (mention required)", {
|
|
421
531
|
teamId,
|
|
422
532
|
channelId,
|
|
@@ -437,6 +547,42 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
|
|
437
547
|
return;
|
|
438
548
|
}
|
|
439
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
|
+
|
|
440
586
|
const mediaList = await resolveMSTeamsInboundMedia({
|
|
441
587
|
attachments,
|
|
442
588
|
htmlSummary: htmlSummary ?? undefined,
|
|
@@ -445,8 +591,9 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
|
|
445
591
|
allowHosts: msteamsCfg?.mediaAllowHosts,
|
|
446
592
|
authAllowHosts: msteamsCfg?.mediaAuthAllowHosts,
|
|
447
593
|
conversationType,
|
|
448
|
-
conversationId,
|
|
594
|
+
conversationId: graphConversationId,
|
|
449
595
|
conversationMessageId: conversationMessageId ?? undefined,
|
|
596
|
+
serviceUrl: activity.serviceUrl,
|
|
450
597
|
activity: {
|
|
451
598
|
id: activity.id,
|
|
452
599
|
replyToId: activity.replyToId,
|
|
@@ -458,6 +605,90 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
|
|
458
605
|
});
|
|
459
606
|
|
|
460
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
|
+
|
|
461
692
|
const envelopeFrom = isDirectMessage ? senderName : conversationType;
|
|
462
693
|
const { storePath, envelopeOptions, previousTimestamp } = resolveInboundSessionEnvelopeContext({
|
|
463
694
|
cfg,
|
|
@@ -501,10 +732,40 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
|
|
501
732
|
}))
|
|
502
733
|
: undefined;
|
|
503
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;
|
|
504
765
|
|
|
505
766
|
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
506
767
|
Body: combinedBody,
|
|
507
|
-
BodyForAgent:
|
|
768
|
+
BodyForAgent: bodyForAgent,
|
|
508
769
|
InboundHistory: inboundHistory,
|
|
509
770
|
RawBody: rawBody,
|
|
510
771
|
CommandBody: commandBody,
|
|
@@ -516,34 +777,32 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
|
|
516
777
|
ChatType: isDirectMessage ? "direct" : isChannel ? "channel" : "group",
|
|
517
778
|
ConversationLabel: envelopeFrom,
|
|
518
779
|
GroupSubject: !isDirectMessage ? conversationType : undefined,
|
|
780
|
+
GroupSpace: teamId,
|
|
519
781
|
SenderName: senderName,
|
|
520
782
|
SenderId: senderId,
|
|
521
783
|
Provider: "msteams" as const,
|
|
522
784
|
Surface: "msteams" as const,
|
|
523
785
|
MessageSid: activity.id,
|
|
524
786
|
Timestamp: timestamp?.getTime() ?? Date.now(),
|
|
525
|
-
WasMentioned: isDirectMessage ||
|
|
787
|
+
WasMentioned: isDirectMessage || mentionDecision.effectiveWasMentioned,
|
|
526
788
|
CommandAuthorized: commandAuthorized,
|
|
527
789
|
OriginatingChannel: "msteams" as const,
|
|
528
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,
|
|
529
796
|
...mediaPayload,
|
|
530
797
|
});
|
|
531
798
|
|
|
532
|
-
await core.channel.session.recordInboundSession({
|
|
533
|
-
storePath,
|
|
534
|
-
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
|
|
535
|
-
ctx: ctxPayload,
|
|
536
|
-
onRecordError: (err) => {
|
|
537
|
-
logVerboseMessage(`msteams: failed updating session meta: ${String(err)}`);
|
|
538
|
-
},
|
|
539
|
-
});
|
|
540
|
-
|
|
541
799
|
logVerboseMessage(`msteams inbound: from=${ctxPayload.From} preview="${preview}"`);
|
|
542
800
|
|
|
543
801
|
const sharePointSiteId = msteamsCfg?.sharePointSiteId;
|
|
544
802
|
const { dispatcher, replyOptions, markDispatchIdle } = createMSTeamsReplyDispatcher({
|
|
545
803
|
cfg,
|
|
546
804
|
agentId: route.agentId,
|
|
805
|
+
sessionKey: route.sessionKey,
|
|
547
806
|
accountId: route.accountId,
|
|
548
807
|
runtime,
|
|
549
808
|
log,
|
|
@@ -562,48 +821,93 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
|
|
562
821
|
sharePointSiteId,
|
|
563
822
|
});
|
|
564
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
|
+
|
|
565
840
|
log.info("dispatching to agent", { sessionKey: route.sessionKey });
|
|
566
841
|
try {
|
|
567
|
-
const
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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
|
+
}),
|
|
573
890
|
},
|
|
574
|
-
replyOptions,
|
|
575
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);
|
|
576
896
|
|
|
577
897
|
log.info("dispatch complete", { queuedFinal, counts });
|
|
578
898
|
|
|
579
|
-
if (!
|
|
580
|
-
if (isRoomish && historyKey) {
|
|
581
|
-
clearHistoryEntriesIfEnabled({
|
|
582
|
-
historyMap: conversationHistories,
|
|
583
|
-
historyKey,
|
|
584
|
-
limit: historyLimit,
|
|
585
|
-
});
|
|
586
|
-
}
|
|
899
|
+
if (!hasFinalResponse) {
|
|
587
900
|
return;
|
|
588
901
|
}
|
|
589
902
|
const finalCount = counts.final;
|
|
590
903
|
logVerboseMessage(
|
|
591
904
|
`msteams: delivered ${finalCount} reply${finalCount === 1 ? "" : "ies"} to ${teamsTo}`,
|
|
592
905
|
);
|
|
593
|
-
if (isRoomish && historyKey) {
|
|
594
|
-
clearHistoryEntriesIfEnabled({
|
|
595
|
-
historyMap: conversationHistories,
|
|
596
|
-
historyKey,
|
|
597
|
-
limit: historyLimit,
|
|
598
|
-
});
|
|
599
|
-
}
|
|
600
906
|
} catch (err) {
|
|
601
|
-
log.error("dispatch failed", { error:
|
|
602
|
-
runtime.error?.(`msteams dispatch failed: ${
|
|
907
|
+
log.error("dispatch failed", { error: formatUnknownError(err) });
|
|
908
|
+
runtime.error?.(`msteams dispatch failed: ${formatUnknownError(err)}`);
|
|
603
909
|
try {
|
|
604
|
-
await context.sendActivity(
|
|
605
|
-
`⚠️ Agent failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
606
|
-
);
|
|
910
|
+
await context.sendActivity("⚠️ Something went wrong. Please try again.");
|
|
607
911
|
} catch {
|
|
608
912
|
// Best effort.
|
|
609
913
|
}
|
|
@@ -653,34 +957,36 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
|
|
653
957
|
.filter(Boolean)
|
|
654
958
|
.join("\n");
|
|
655
959
|
const wasMentioned = entries.some((entry) => entry.wasMentioned);
|
|
656
|
-
const
|
|
960
|
+
const implicitMentionKinds = entries.flatMap((entry) => entry.implicitMentionKinds);
|
|
657
961
|
await handleTeamsMessageNow({
|
|
658
962
|
context: last.context,
|
|
659
963
|
rawText: combinedRawText,
|
|
660
964
|
text: combinedText,
|
|
661
965
|
attachments: [],
|
|
662
966
|
wasMentioned,
|
|
663
|
-
|
|
967
|
+
implicitMentionKinds,
|
|
664
968
|
});
|
|
665
969
|
},
|
|
666
970
|
onError: (err) => {
|
|
667
|
-
runtime.error?.(`msteams debounce flush failed: ${
|
|
971
|
+
runtime.error?.(`msteams debounce flush failed: ${formatUnknownError(err)}`);
|
|
668
972
|
},
|
|
669
973
|
});
|
|
670
974
|
|
|
671
975
|
return async function handleTeamsMessage(context: MSTeamsTurnContext) {
|
|
672
976
|
const activity = context.activity;
|
|
673
|
-
const rawText = activity.text?.trim() ?? "";
|
|
674
|
-
const text = stripMSTeamsMentionTags(rawText);
|
|
675
977
|
const attachments = Array.isArray(activity.attachments)
|
|
676
978
|
? (activity.attachments as unknown as MSTeamsAttachmentLike[])
|
|
677
979
|
: [];
|
|
980
|
+
const rawText = activity.text?.trim() ?? "";
|
|
981
|
+
const htmlText = extractTextFromHtmlAttachments(attachments);
|
|
982
|
+
const text = stripMSTeamsMentionTags(rawText || htmlText);
|
|
678
983
|
const wasMentioned = wasMSTeamsBotMentioned(activity);
|
|
679
984
|
const conversationId = normalizeMSTeamsConversationId(activity.conversation?.id ?? "");
|
|
680
985
|
const replyToId = activity.replyToId ?? undefined;
|
|
681
|
-
const
|
|
682
|
-
conversationId && replyToId && wasMSTeamsMessageSent(conversationId, replyToId)
|
|
683
|
-
|
|
986
|
+
const implicitMentionKinds: Array<"reply_to_bot"> =
|
|
987
|
+
conversationId && replyToId && wasMSTeamsMessageSent(conversationId, replyToId)
|
|
988
|
+
? ["reply_to_bot"]
|
|
989
|
+
: [];
|
|
684
990
|
|
|
685
991
|
await inboundDebouncer.enqueue({
|
|
686
992
|
context,
|
|
@@ -688,7 +994,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
|
|
688
994
|
text,
|
|
689
995
|
attachments,
|
|
690
996
|
wasMentioned,
|
|
691
|
-
|
|
997
|
+
implicitMentionKinds,
|
|
692
998
|
});
|
|
693
999
|
};
|
|
694
1000
|
}
|