mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(msteams): preserve thread targets for structured sends (#117516)
This commit is contained in:
committed by
GitHub
parent
29577fb03b
commit
17625e5cd2
@@ -297,14 +297,15 @@ describe("msteamsOutbound cfg threading", () => {
|
||||
|
||||
const result = await requireSendPayload()({
|
||||
cfg,
|
||||
to: "conversation:abc",
|
||||
to: "conversation:19:channel@thread.tacv2",
|
||||
threadId: "presentation-thread-root",
|
||||
text: "Deploy finished",
|
||||
payload: rendered!,
|
||||
});
|
||||
|
||||
expect(mocks.sendAdaptiveCardMSTeams).toHaveBeenCalledWith({
|
||||
cfg,
|
||||
to: "conversation:abc",
|
||||
to: "conversation:19:channel@thread.tacv2;messageid=presentation-thread-root",
|
||||
card: (rendered!.channelData!.msteams as { presentationCard: unknown }).presentationCard,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
@@ -574,6 +575,26 @@ describe("msteamsOutbound cfg threading", () => {
|
||||
expect(Number.isNaN(Date.parse(pollRecord?.createdAt))).toBe(false);
|
||||
});
|
||||
|
||||
it("forwards resolved channel thread ids to poll sends", async () => {
|
||||
await requireSendPoll()({
|
||||
cfg,
|
||||
to: "conversation:19:channel@thread.tacv2",
|
||||
threadId: "poll-thread-root",
|
||||
poll: {
|
||||
question: "Ship it?",
|
||||
options: ["Yes", "No"],
|
||||
},
|
||||
});
|
||||
|
||||
expect(mocks.sendPollMSTeams).toHaveBeenCalledWith({
|
||||
cfg,
|
||||
to: "conversation:19:channel@thread.tacv2;messageid=poll-thread-root",
|
||||
question: "Ship it?",
|
||||
options: ["Yes", "No"],
|
||||
maxSelections: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it("chunks outbound text without requiring MSTeams runtime initialization", () => {
|
||||
const chunker = msteamsOutbound.chunker;
|
||||
if (!chunker) {
|
||||
|
||||
@@ -218,11 +218,11 @@ export const msteamsOutbound: ChannelOutboundAdapter = {
|
||||
mediaReadFile,
|
||||
});
|
||||
},
|
||||
sendPoll: async ({ cfg, to, poll }) => {
|
||||
sendPoll: async ({ cfg, to, poll, threadId }) => {
|
||||
const maxSelections = poll.maxSelections ?? 1;
|
||||
const result = await sendPollMSTeams({
|
||||
cfg,
|
||||
to,
|
||||
to: resolveMSTeamsThreadTarget(to, threadId),
|
||||
question: poll.question,
|
||||
options: poll.options,
|
||||
maxSelections,
|
||||
|
||||
@@ -50,7 +50,7 @@ function channelRef(params?: Partial<StoredConversationReference>): StoredConver
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveMSTeamsProactiveReplyStyle(params: {
|
||||
async function resolveMSTeamsProactiveReplyTarget(params: {
|
||||
cfg?: MSTeamsConfig;
|
||||
conversationId: string;
|
||||
ref: StoredConversationReference;
|
||||
@@ -76,12 +76,14 @@ async function resolveMSTeamsProactiveReplyStyle(params: {
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
return (
|
||||
await resolveMSTeamsSendContext({
|
||||
cfg,
|
||||
to: `conversation:${params.conversationId}`,
|
||||
})
|
||||
).replyStyle;
|
||||
const context = await resolveMSTeamsSendContext({
|
||||
cfg,
|
||||
to: `conversation:${params.conversationId}`,
|
||||
});
|
||||
return {
|
||||
replyStyle: context.replyStyle,
|
||||
threadActivityId: context.threadActivityId,
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -155,6 +157,7 @@ describe("resolveMSTeamsSendContext", () => {
|
||||
conversationId: "19:channel@thread.tacv2",
|
||||
ref: { threadId: "explicit-root" },
|
||||
replyStyle: "thread",
|
||||
threadActivityId: "explicit-root",
|
||||
});
|
||||
expect(sendContextMockState.store.get).toHaveBeenCalledWith("19:channel@thread.tacv2");
|
||||
});
|
||||
@@ -186,6 +189,7 @@ describe("resolveMSTeamsSendContext", () => {
|
||||
conversationId: "19:channel@thread.tacv2",
|
||||
ref: { threadId: "graph-root" },
|
||||
replyStyle: "thread",
|
||||
threadActivityId: "graph-root",
|
||||
});
|
||||
expect(sendContextMockState.store.get).toHaveBeenCalledWith("19:channel@thread.tacv2");
|
||||
});
|
||||
@@ -248,27 +252,27 @@ describe("resolveMSTeamsSendContext", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveMSTeamsProactiveReplyStyle", () => {
|
||||
describe("resolveMSTeamsProactiveReplyTarget", () => {
|
||||
it("uses thread for channel conversations with a stored thread root", async () => {
|
||||
await expect(
|
||||
resolveMSTeamsProactiveReplyStyle({
|
||||
resolveMSTeamsProactiveReplyTarget({
|
||||
cfg: {},
|
||||
conversationId: "19:channel@thread.tacv2",
|
||||
ref: channelRef({ threadId: "thread-root-1" }),
|
||||
conversationType: "channel",
|
||||
}),
|
||||
).resolves.toBe("thread");
|
||||
).resolves.toEqual({ replyStyle: "thread", threadActivityId: "thread-root-1" });
|
||||
});
|
||||
|
||||
it("falls back to activityId for legacy channel references", async () => {
|
||||
await expect(
|
||||
resolveMSTeamsProactiveReplyStyle({
|
||||
resolveMSTeamsProactiveReplyTarget({
|
||||
cfg: {},
|
||||
conversationId: "19:channel@thread.tacv2",
|
||||
ref: channelRef({ activityId: "legacy-root-1" }),
|
||||
conversationType: "channel",
|
||||
}),
|
||||
).resolves.toBe("thread");
|
||||
).resolves.toEqual({ replyStyle: "thread", threadActivityId: "legacy-root-1" });
|
||||
});
|
||||
|
||||
it("keeps configured top-level channel routing", async () => {
|
||||
@@ -284,44 +288,44 @@ describe("resolveMSTeamsProactiveReplyStyle", () => {
|
||||
};
|
||||
|
||||
await expect(
|
||||
resolveMSTeamsProactiveReplyStyle({
|
||||
resolveMSTeamsProactiveReplyTarget({
|
||||
cfg,
|
||||
conversationId: "19:channel@thread.tacv2",
|
||||
ref: channelRef({ threadId: "thread-root-1" }),
|
||||
conversationType: "channel",
|
||||
}),
|
||||
).resolves.toBe("top-level");
|
||||
).resolves.toEqual({ replyStyle: "top-level", threadActivityId: undefined });
|
||||
});
|
||||
|
||||
it("uses top-level when a channel has no stored thread root", async () => {
|
||||
await expect(
|
||||
resolveMSTeamsProactiveReplyStyle({
|
||||
resolveMSTeamsProactiveReplyTarget({
|
||||
cfg: { replyStyle: "thread" },
|
||||
conversationId: "19:channel@thread.tacv2",
|
||||
ref: channelRef(),
|
||||
conversationType: "channel",
|
||||
}),
|
||||
).resolves.toBe("top-level");
|
||||
).resolves.toEqual({ replyStyle: "top-level", threadActivityId: undefined });
|
||||
});
|
||||
|
||||
it("uses top-level for non-channel conversations", async () => {
|
||||
const ref = channelRef({ activityId: "activity-1" });
|
||||
|
||||
await expect(
|
||||
resolveMSTeamsProactiveReplyStyle({
|
||||
resolveMSTeamsProactiveReplyTarget({
|
||||
cfg: { replyStyle: "thread" },
|
||||
conversationId: "19:group@thread.v2",
|
||||
ref,
|
||||
conversationType: "groupChat",
|
||||
}),
|
||||
).resolves.toBe("top-level");
|
||||
).resolves.toEqual({ replyStyle: "top-level", threadActivityId: undefined });
|
||||
await expect(
|
||||
resolveMSTeamsProactiveReplyStyle({
|
||||
resolveMSTeamsProactiveReplyTarget({
|
||||
cfg: { replyStyle: "thread" },
|
||||
conversationId: "a:personal",
|
||||
ref,
|
||||
conversationType: "personal",
|
||||
}),
|
||||
).resolves.toBe("top-level");
|
||||
).resolves.toEqual({ replyStyle: "top-level", threadActivityId: undefined });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,6 @@ import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coer
|
||||
import {
|
||||
resolveChannelMediaMaxBytes,
|
||||
type MSTeamsConfig,
|
||||
type MSTeamsReplyStyle,
|
||||
type OpenClawConfig,
|
||||
type PluginRuntime,
|
||||
} from "../runtime-api.js";
|
||||
@@ -33,6 +32,12 @@ import { resolveMSTeamsCredentials } from "./token.js";
|
||||
|
||||
type MSTeamsConversationType = "personal" | "groupChat" | "channel";
|
||||
|
||||
// Keep reply policy and the Connector thread suffix together so every proactive
|
||||
// activity kind uses the same resolved destination instead of re-deriving it.
|
||||
type MSTeamsProactiveReplyTarget =
|
||||
| { replyStyle: "thread"; threadActivityId: string }
|
||||
| { replyStyle: "top-level"; threadActivityId?: never };
|
||||
|
||||
export type MSTeamsProactiveContext = {
|
||||
appId: string;
|
||||
conversationId: string;
|
||||
@@ -41,8 +46,6 @@ export type MSTeamsProactiveContext = {
|
||||
log: ReturnType<PluginRuntime["logging"]["getChildLogger"]>;
|
||||
/** The type of conversation: personal (1:1), groupChat, or channel */
|
||||
conversationType: MSTeamsConversationType;
|
||||
/** Reply style resolved for proactive text/media sends. */
|
||||
replyStyle: MSTeamsReplyStyle;
|
||||
/** Teams SDK cloud/service endpoint used to validate proactive sends. */
|
||||
sdkCloudOptions: MSTeamsSdkCloudOptions;
|
||||
/** Token provider for Graph API / SharePoint operations */
|
||||
@@ -51,17 +54,17 @@ export type MSTeamsProactiveContext = {
|
||||
sharePointSiteId?: string;
|
||||
/** Resolved media max bytes from config (default: 100MB) */
|
||||
mediaMaxBytes?: number;
|
||||
};
|
||||
} & MSTeamsProactiveReplyTarget;
|
||||
|
||||
function resolveMSTeamsProactiveReplyStyle(params: {
|
||||
function resolveMSTeamsProactiveReplyTarget(params: {
|
||||
cfg?: MSTeamsConfig;
|
||||
conversationId: string;
|
||||
ref: StoredConversationReference;
|
||||
conversationType: MSTeamsConversationType;
|
||||
}): MSTeamsReplyStyle {
|
||||
}): MSTeamsProactiveReplyTarget {
|
||||
const threadRootId = params.ref.threadId ?? params.ref.activityId;
|
||||
if (params.conversationType !== "channel" || !threadRootId) {
|
||||
return "top-level";
|
||||
return { replyStyle: "top-level" };
|
||||
}
|
||||
|
||||
const routeConfig = resolveMSTeamsRouteConfig({
|
||||
@@ -76,7 +79,7 @@ function resolveMSTeamsProactiveReplyStyle(params: {
|
||||
teamConfig: routeConfig.teamConfig,
|
||||
channelConfig: routeConfig.channelConfig,
|
||||
});
|
||||
return replyStyle;
|
||||
return replyStyle === "thread" ? { replyStyle, threadActivityId: threadRootId } : { replyStyle };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -250,10 +253,10 @@ export async function resolveMSTeamsSendContext(params: {
|
||||
// An explicit messageid is a caller-owned destination. Ambient and stored
|
||||
// roots still obey route policy, but explicit channel roots must not be
|
||||
// flattened by a top-level default.
|
||||
const replyStyle =
|
||||
const replyTarget: MSTeamsProactiveReplyTarget =
|
||||
recipient.threadId && conversationType === "channel"
|
||||
? "thread"
|
||||
: resolveMSTeamsProactiveReplyStyle({
|
||||
? { replyStyle: "thread", threadActivityId: recipient.threadId }
|
||||
: resolveMSTeamsProactiveReplyTarget({
|
||||
cfg: msteamsCfg,
|
||||
conversationId,
|
||||
ref: safeRef,
|
||||
@@ -276,7 +279,7 @@ export async function resolveMSTeamsSendContext(params: {
|
||||
app,
|
||||
log,
|
||||
conversationType,
|
||||
replyStyle,
|
||||
...replyTarget,
|
||||
sdkCloudOptions,
|
||||
tokenProvider,
|
||||
sharePointSiteId,
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { AddressInfo } from "node:net";
|
||||
import { Client as TeamsApiClient } from "@microsoft/teams.api";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../runtime-api.js";
|
||||
import { sendMessageMSTeams } from "./send.js";
|
||||
import { sendAdaptiveCardMSTeams, sendMessageMSTeams, sendPollMSTeams } from "./send.js";
|
||||
|
||||
const serviceUrl = "https://smba.trafficmanager.net/amer";
|
||||
const conversationId = "19:channel@thread.tacv2";
|
||||
@@ -173,6 +173,15 @@ type AttachmentRoutingCase = {
|
||||
expectedConversationId: string;
|
||||
};
|
||||
|
||||
type StructuredRoutingCase = {
|
||||
label: string;
|
||||
conversationType: "channel" | "groupChat" | "personal";
|
||||
replyStyle: "thread" | "top-level";
|
||||
threadActivityId?: string;
|
||||
storedThreadId?: string;
|
||||
expectedConversationId: string;
|
||||
};
|
||||
|
||||
const attachmentRoutingCases: AttachmentRoutingCase[] = [
|
||||
{
|
||||
label: "channel attachment in its stored thread root",
|
||||
@@ -205,6 +214,69 @@ const attachmentRoutingCases: AttachmentRoutingCase[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const structuredRoutingCases: StructuredRoutingCase[] = [
|
||||
{
|
||||
label: "threaded channel",
|
||||
conversationType: "channel",
|
||||
replyStyle: "thread",
|
||||
threadActivityId: "thread-root-1",
|
||||
storedThreadId: "thread-root-1",
|
||||
expectedConversationId: `${conversationId};messageid=thread-root-1`,
|
||||
},
|
||||
{
|
||||
label: "top-level channel",
|
||||
conversationType: "channel",
|
||||
replyStyle: "top-level",
|
||||
storedThreadId: "thread-root-1",
|
||||
expectedConversationId: conversationId,
|
||||
},
|
||||
{
|
||||
label: "group chat",
|
||||
conversationType: "groupChat",
|
||||
replyStyle: "top-level",
|
||||
storedThreadId: "group-activity-1",
|
||||
expectedConversationId: conversationId,
|
||||
},
|
||||
{
|
||||
label: "personal chat",
|
||||
conversationType: "personal",
|
||||
replyStyle: "top-level",
|
||||
storedThreadId: "personal-activity-1",
|
||||
expectedConversationId: conversationId,
|
||||
},
|
||||
];
|
||||
|
||||
type StructuredSender = {
|
||||
label: string;
|
||||
send: (cfg: OpenClawConfig) => Promise<unknown>;
|
||||
};
|
||||
|
||||
const structuredSenders: StructuredSender[] = [
|
||||
{
|
||||
label: "presentation card",
|
||||
send: async (cfg) =>
|
||||
await sendAdaptiveCardMSTeams({
|
||||
cfg,
|
||||
to: conversationId,
|
||||
card: {
|
||||
type: "AdaptiveCard",
|
||||
version: "1.4",
|
||||
body: [{ type: "TextBlock", text: "Deploy finished" }],
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: "poll",
|
||||
send: async (cfg) =>
|
||||
await sendPollMSTeams({
|
||||
cfg,
|
||||
to: conversationId,
|
||||
question: "Ship it?",
|
||||
options: ["Yes", "No"],
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
describe("Microsoft Teams SharePoint attachment thread routing", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@@ -231,6 +303,9 @@ describe("Microsoft Teams SharePoint attachment thread routing", () => {
|
||||
},
|
||||
conversationType,
|
||||
replyStyle,
|
||||
...(replyStyle === "thread" && conversationType === "channel"
|
||||
? { threadActivityId: threadId ?? activityId }
|
||||
: {}),
|
||||
sdkCloudOptions: { cloud: "Public" },
|
||||
tokenProvider: { getAccessToken: vi.fn(async () => "token") },
|
||||
sharePointSiteId: "sharepoint-site-1",
|
||||
@@ -307,3 +382,54 @@ describe("Microsoft Teams SharePoint attachment thread routing", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe.each(structuredSenders)("Microsoft Teams $label thread routing", ({ send }) => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it.each(structuredRoutingCases)(
|
||||
"sends to the resolved $label identity through the real Teams SDK",
|
||||
async ({
|
||||
conversationType,
|
||||
replyStyle,
|
||||
threadActivityId,
|
||||
storedThreadId,
|
||||
expectedConversationId,
|
||||
}) => {
|
||||
await withRealTeamsSdkHttp(async ({ api, requests }) => {
|
||||
mockState.resolveMSTeamsSendContext.mockResolvedValue({
|
||||
app: { api },
|
||||
appId: "app-id",
|
||||
conversationId,
|
||||
ref: {
|
||||
serviceUrl,
|
||||
agent: { id: "28:bot", name: "OpenClaw", role: "bot" },
|
||||
user: { id: "29:user" },
|
||||
conversation: { id: conversationId, conversationType },
|
||||
activityId: "incoming-activity-1",
|
||||
...(storedThreadId ? { threadId: storedThreadId } : {}),
|
||||
},
|
||||
conversationType,
|
||||
replyStyle,
|
||||
...(threadActivityId ? { threadActivityId } : {}),
|
||||
sdkCloudOptions: { cloud: "Public" },
|
||||
tokenProvider: { getAccessToken: vi.fn(async () => "token") },
|
||||
log: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
||||
});
|
||||
|
||||
await send({} as OpenClawConfig);
|
||||
|
||||
expect(requests).toHaveLength(1);
|
||||
expect(requests[0]).toMatchObject({
|
||||
path: `${new URL(serviceUrl).pathname}/v3/conversations/${expectedConversationId}/activities`,
|
||||
body: {
|
||||
type: "message",
|
||||
conversation: { id: expectedConversationId, conversationType },
|
||||
attachments: [{ contentType: "application/vnd.microsoft.card.adaptive" }],
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -182,17 +182,7 @@ export async function sendMessageMSTeams(
|
||||
});
|
||||
const messageText = formatMSTeamsMarkdown(text ?? "", tableMode);
|
||||
const ctx = await resolveMSTeamsSendContext({ cfg, to });
|
||||
const {
|
||||
app,
|
||||
conversationId,
|
||||
ref,
|
||||
log,
|
||||
conversationType,
|
||||
replyStyle,
|
||||
tokenProvider,
|
||||
sharePointSiteId,
|
||||
sdkCloudOptions,
|
||||
} = ctx;
|
||||
const { conversationId, log, conversationType, tokenProvider, sharePointSiteId } = ctx;
|
||||
|
||||
log.debug?.("sending proactive message", {
|
||||
conversationId,
|
||||
@@ -245,11 +235,9 @@ export async function sendMessageMSTeams(
|
||||
log.debug?.("sending file consent card", { uploadId, fileName, size: media.buffer.length });
|
||||
|
||||
const messageId = await sendProactiveActivity({
|
||||
app,
|
||||
ref,
|
||||
ctx,
|
||||
activity,
|
||||
errorPrefix: "msteams consent card send",
|
||||
serviceUrlBoundary: sdkCloudOptions,
|
||||
});
|
||||
|
||||
// Store the activity ID so the accept handler can replace the consent
|
||||
@@ -326,15 +314,8 @@ export async function sendMessageMSTeams(
|
||||
attachments: [fileCardAttachment],
|
||||
};
|
||||
const messageId = await sendProactiveActivityRaw({
|
||||
app,
|
||||
ref,
|
||||
ctx,
|
||||
activity,
|
||||
// Only channel replies carry a thread root; top-level and group sends must stay unchanged.
|
||||
threadActivityId:
|
||||
replyStyle === "thread" && conversationType === "channel"
|
||||
? (ref.threadId ?? ref.activityId)
|
||||
: undefined,
|
||||
serviceUrlBoundary: sdkCloudOptions,
|
||||
});
|
||||
|
||||
log.info("sent native file card", {
|
||||
@@ -428,41 +409,32 @@ async function sendTextWithMedia(
|
||||
}
|
||||
|
||||
type ProactiveActivityParams = {
|
||||
app: MSTeamsProactiveContext["app"];
|
||||
ref: MSTeamsProactiveContext["ref"];
|
||||
ctx: MSTeamsProactiveContext;
|
||||
activity: Record<string, unknown>;
|
||||
errorPrefix: string;
|
||||
serviceUrlBoundary: MSTeamsProactiveContext["sdkCloudOptions"];
|
||||
};
|
||||
|
||||
type ProactiveActivityRawParams = Omit<ProactiveActivityParams, "errorPrefix"> & {
|
||||
threadActivityId?: string;
|
||||
};
|
||||
type ProactiveActivityRawParams = Omit<ProactiveActivityParams, "errorPrefix">;
|
||||
|
||||
async function sendProactiveActivityRaw({
|
||||
app,
|
||||
ref,
|
||||
ctx,
|
||||
activity,
|
||||
threadActivityId,
|
||||
serviceUrlBoundary,
|
||||
}: ProactiveActivityRawParams): Promise<string> {
|
||||
const baseRef = buildConversationReference(ref);
|
||||
const response = await sendMSTeamsActivityWithReference(app, baseRef, activity, {
|
||||
...(threadActivityId ? { threadActivityId } : {}),
|
||||
serviceUrlBoundary,
|
||||
const baseRef = buildConversationReference(ctx.ref);
|
||||
const response = await sendMSTeamsActivityWithReference(ctx.app, baseRef, activity, {
|
||||
...(ctx.threadActivityId ? { threadActivityId: ctx.threadActivityId } : {}),
|
||||
serviceUrlBoundary: ctx.sdkCloudOptions,
|
||||
});
|
||||
return extractMessageId(response) ?? "unknown";
|
||||
}
|
||||
|
||||
async function sendProactiveActivity({
|
||||
app,
|
||||
ref,
|
||||
ctx,
|
||||
activity,
|
||||
errorPrefix,
|
||||
serviceUrlBoundary,
|
||||
}: ProactiveActivityParams): Promise<string> {
|
||||
try {
|
||||
return await sendProactiveActivityRaw({ app, ref, activity, serviceUrlBoundary });
|
||||
return await sendProactiveActivityRaw({ ctx, activity });
|
||||
} catch (err) {
|
||||
const classification = classifyMSTeamsSendError(err);
|
||||
const hint = formatMSTeamsSendErrorHint(classification);
|
||||
@@ -481,10 +453,11 @@ export async function sendPollMSTeams(
|
||||
params: SendMSTeamsPollParams,
|
||||
): Promise<SendMSTeamsPollResult> {
|
||||
const { cfg, to, question, options, maxSelections } = params;
|
||||
const { app, conversationId, ref, log, sdkCloudOptions } = await resolveMSTeamsSendContext({
|
||||
const ctx = await resolveMSTeamsSendContext({
|
||||
cfg,
|
||||
to,
|
||||
});
|
||||
const { conversationId, log } = ctx;
|
||||
|
||||
const pollCard = buildMSTeamsPollCard({
|
||||
question,
|
||||
@@ -510,11 +483,9 @@ export async function sendPollMSTeams(
|
||||
|
||||
// Send poll via proactive conversation (Adaptive Cards require direct activity send)
|
||||
const messageId = await sendProactiveActivity({
|
||||
app,
|
||||
ref,
|
||||
ctx,
|
||||
activity,
|
||||
errorPrefix: "msteams poll send",
|
||||
serviceUrlBoundary: sdkCloudOptions,
|
||||
});
|
||||
|
||||
log.info("sent poll", { conversationId, pollId: pollCard.pollId, messageId });
|
||||
@@ -533,10 +504,11 @@ export async function sendAdaptiveCardMSTeams(
|
||||
params: SendMSTeamsCardParams,
|
||||
): Promise<SendMSTeamsCardResult> {
|
||||
const { cfg, to, card } = params;
|
||||
const { app, conversationId, ref, log, sdkCloudOptions } = await resolveMSTeamsSendContext({
|
||||
const ctx = await resolveMSTeamsSendContext({
|
||||
cfg,
|
||||
to,
|
||||
});
|
||||
const { conversationId, log } = ctx;
|
||||
|
||||
log.debug?.("sending adaptive card", {
|
||||
conversationId,
|
||||
@@ -556,11 +528,9 @@ export async function sendAdaptiveCardMSTeams(
|
||||
|
||||
// Send card via proactive conversation
|
||||
const messageId = await sendProactiveActivity({
|
||||
app,
|
||||
ref,
|
||||
ctx,
|
||||
activity,
|
||||
errorPrefix: "msteams card send",
|
||||
serviceUrlBoundary: sdkCloudOptions,
|
||||
});
|
||||
|
||||
log.info("sent adaptive card", { conversationId, messageId });
|
||||
|
||||
Reference in New Issue
Block a user