// Match the thread-aware session key used by the message handler so feedback // events land in the correct per-thread transcript. For channel threads, the // thread root ID comes from the ;messageid= suffix on the conversation ID or // from activity.replyToId. const feedbackThreadId = isChannel
? (extractMSTeamsConversationMessageId(rawConversationId) ?? activity.replyToId ?? undefined)
: undefined; if (feedbackThreadId) { const threadKeys = resolveThreadSessionKeys({
baseSessionKey: route.sessionKey,
threadId: feedbackThreadId,
parentSessionKey: route.sessionKey,
});
route.sessionKey = threadKeys.sessionKey;
}
// For negative feedback, trigger background reflection (fire-and-forget). // No ack message — the reflection follow-up serves as the acknowledgement. // Sending anything during the invoke handler causes "unable to reach app" errors. if (isNegative && msteamsCfg?.feedbackReflection !== false) { // Note: thumbedDownResponse is not populated here because we don't cache // sent message text. The agent still has full session context for reflection // since the reflection runs in the same session. The user comment (if any) // provides additional signal.
runFeedbackReflection({
cfg: deps.cfg,
adapter: deps.adapter,
appId: deps.appId,
conversationRef,
sessionKey: route.sessionKey,
agentId: route.agentId,
conversationId,
feedbackMessageId: messageId,
userComment,
log: deps.log,
}).catch((err) => {
deps.log.error("feedback reflection failed", { error: formatUnknownError(err) });
});
}
returntrue;
}
export function registerMSTeamsHandlers<T extends MSTeamsActivityHandler>(
handler: T,
deps: MSTeamsMessageHandlerDeps,
): T { const handleTeamsMessage = createMSTeamsMessageHandler(deps); const handleReaction = createMSTeamsReactionHandler(deps);
// Wrap the original run method to intercept invokes const originalRun = handler.run; if (originalRun) {
handler.run = async (context: unknown) => { const ctx = context as MSTeamsTurnContext; // Handle file consent invokes before passing to normal flow if (ctx.activity?.type === "invoke" && ctx.activity?.name === "fileConsent/invoke") {
await respondToMSTeamsFileConsentInvoke(ctx, deps.log); return;
}
// Handle feedback invokes (thumbs up/down on AI-generated messages). // Just return after handling — the process() handler sends HTTP 200 automatically. // Do NOT call sendActivity with invokeResponse; our custom adapter would POST // a new activity to Bot Framework instead of responding to the HTTP request. if (ctx.activity?.type === "invoke" && ctx.activity?.name === "message/submitAction") { const handled = await handleFeedbackInvoke(ctx, deps); if (handled) { return;
}
}
if (ctx.activity?.type === "invoke" && ctx.activity?.name === "adaptiveCard/action") { const text = serializeAdaptiveCardActionValue(ctx.activity?.value); if (text) {
await handleTeamsMessage({
...ctx,
activity: {
...ctx.activity,
type: "message",
text,
},
}); return;
}
deps.log.debug?.("skipping adaptive card action invoke without value payload");
}
// Bot Framework OAuth SSO: Teams sends signin/tokenExchange (with a // Teams-provided exchangeable token) or signin/verifyState (magic // code fallback) after an oauthCard is presented. We must ack with // HTTP 200 and, if configured, exchange the token with the Bot // Framework User Token service and persist it for downstream tools. if (
ctx.activity?.type === "invoke" &&
(ctx.activity?.name === "signin/tokenExchange" ||
ctx.activity?.name === "signin/verifyState")
) { // Always ack immediately — silently dropping the invoke causes // the Teams card UI to report "Something went wrong".
await ctx.sendActivity({ type: "invokeResponse", value: { status: 200, body: {} } });
if (!(await isSigninInvokeAuthorized(ctx, deps))) { return;
}
if (!deps.sso) {
deps.log.debug?.("signin invoke received but msteams.sso is not configured", {
name: ctx.activity.name,
}); return;
}
for (const member of membersAdded) { if (member.id === botId) { // Bot was added to a conversation — send welcome card if configured. const conversationType =
normalizeOptionalLowercaseString(ctx.activity?.conversation?.conversationType) ?? "personal"; const isPersonal = conversationType === "personal";
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.