Skip to content

Commit a89b7ae

Browse files
committed
fix(msteams): thread topLevel through established send-context path with replyStyleOverride
- Replace topLevel boolean with replyStyleOverride (MSTeamsReplyStyle) in sendMessageMSTeams and sendAdaptiveCardMSTeams, passing through to resolveMSTeamsSendContext instead of mapping internally. - Add presentation guard in channel.ts: topLevel + card/presentation sends are explicitly rejected with a clear error message. - Use the full send params (mediaLocalRoots, mediaReadFile) in the topLevel intercept path so the established outbound pipeline stays intact. - Add send-context.test.ts coverage: replyStyleOverride honored before config-derived replyStyle, and fallback to config when omitted. Fixes #93288
1 parent 523e1e4 commit a89b7ae

3 files changed

Lines changed: 103 additions & 14 deletions

File tree

extensions/msteams/src/channel.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -803,26 +803,38 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount, ProbeMSTeamsRe
803803
},
804804
});
805805
}
806-
// Only intercept send when the caller explicitly requests topLevel.
807-
// Normal sends fall through to the outbound adapter so media, payloads,
808-
// chunking, hooks, and receipts stay on the established delivery path.
806+
// Per-call `topLevel: true` opens a new root post / thread in a channel,
807+
// bypassing the static channel `replyStyle` config. Only meaningful for
808+
// plain-text sends — card/presentation sends are explicitly rejected because
809+
// the established card path does not currently accept replyStyleOverride.
810+
// Normal sends without topLevel fall through to the outbound adapter so
811+
// media, payloads, chunking, hooks, and receipts stay on the established
812+
// delivery path.
809813
if (ctx.action === "send" && ctx.params.topLevel === true) {
814+
if (presentation) {
815+
return actionError(
816+
"topLevel is not supported with presentation/card sends. " +
817+
"Use topLevel only with plain-text sends.",
818+
);
819+
}
810820
return await runWithRequiredActionTarget({
811-
actionLabel: "Send",
821+
actionLabel: "Send (top-level / new thread)",
812822
toolParams: ctx.params,
813823
run: async (to) => {
814824
const { sendMessageMSTeams } = await loadMSTeamsChannelRuntime();
815825
const result = await sendMessageMSTeams({
816826
cfg: ctx.cfg,
817827
to,
818828
text: resolveActionContent(ctx.params),
819-
topLevel: ctx.params.topLevel === true ? true : undefined,
829+
mediaLocalRoots: ctx.mediaLocalRoots,
830+
mediaReadFile: ctx.mediaReadFile,
831+
replyStyleOverride: "top-level",
820832
});
821833
return jsonActionResultWithDetails(
822834
{
823835
ok: true,
824836
channel: "msteams",
825-
action: "send",
837+
topLevel: true,
826838
messageId: result.messageId,
827839
conversationId: result.conversationId,
828840
},

extensions/msteams/src/send-context.test.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,73 @@ describe("resolveMSTeamsSendContext", () => {
9393
});
9494
});
9595

96+
it("honors replyStyleOverride before the config-derived replyStyle for channels", async () => {
97+
sendContextMockState.store.get.mockResolvedValue(
98+
channelRef({
99+
serviceUrl: "https://smba.trafficmanager.net/amer/",
100+
threadId: "thread-root-1",
101+
conversation: { id: "19:[email protected]", conversationType: "channel" },
102+
}),
103+
);
104+
105+
const cfg = {
106+
channels: {
107+
msteams: {
108+
enabled: true,
109+
appId: "app-id",
110+
appPassword: "app-password",
111+
tenantId: "tenant-id",
112+
// Channel default reply style is "thread" — without the override,
113+
// the resolved context would carry replyStyle: "thread".
114+
replyStyle: "thread" as const,
115+
},
116+
},
117+
} as OpenClawConfig;
118+
119+
await expect(
120+
resolveMSTeamsSendContext({
121+
cfg,
122+
to: "conversation:19:[email protected]",
123+
replyStyleOverride: "top-level",
124+
}),
125+
).resolves.toMatchObject({
126+
conversationId: "19:[email protected]",
127+
replyStyle: "top-level",
128+
});
129+
});
130+
131+
it("falls back to the config-derived replyStyle when no override is passed", async () => {
132+
sendContextMockState.store.get.mockResolvedValue(
133+
channelRef({
134+
serviceUrl: "https://smba.trafficmanager.net/amer/",
135+
threadId: "thread-root-1",
136+
conversation: { id: "19:[email protected]", conversationType: "channel" },
137+
}),
138+
);
139+
140+
const cfg = {
141+
channels: {
142+
msteams: {
143+
enabled: true,
144+
appId: "app-id",
145+
appPassword: "app-password",
146+
tenantId: "tenant-id",
147+
replyStyle: "thread" as const,
148+
},
149+
},
150+
} as OpenClawConfig;
151+
152+
await expect(
153+
resolveMSTeamsSendContext({
154+
cfg,
155+
to: "conversation:19:[email protected]",
156+
}),
157+
).resolves.toMatchObject({
158+
conversationId: "19:[email protected]",
159+
replyStyle: "thread",
160+
});
161+
});
162+
96163
it("removes stored conversation references with blocked serviceUrl hosts", async () => {
97164
sendContextMockState.store.get.mockResolvedValue(
98165
channelRef({

extensions/msteams/src/send.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ import {
66
} from "openclaw/plugin-sdk/channel-outbound";
77
import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/markdown-table-runtime";
88
import { convertMarkdownTables } from "openclaw/plugin-sdk/text-chunking";
9-
import { loadOutboundMediaFromUrl, type OpenClawConfig } from "../runtime-api.js";
9+
import {
10+
loadOutboundMediaFromUrl,
11+
type MSTeamsReplyStyle,
12+
type OpenClawConfig,
13+
} from "../runtime-api.js";
1014
import {
1115
classifyMSTeamsSendError,
1216
formatMSTeamsSendErrorHint,
@@ -44,8 +48,13 @@ type SendMSTeamsMessageParams = {
4448
filename?: string;
4549
mediaLocalRoots?: readonly string[];
4650
mediaReadFile?: (filePath: string) => Promise<Buffer>;
47-
/** Force a new root post in the channel (channel only; ignored for DMs/group chats) */
48-
topLevel?: boolean;
51+
/**
52+
* Optional per-call override for the resolved reply style. When `"top-level"`
53+
* is passed, the send opens a new root post / new thread in a channel
54+
* regardless of the static channel `replyStyle` config. Ignored for DM and
55+
* group-chat conversations. See `resolveMSTeamsSendContext`.
56+
*/
57+
replyStyleOverride?: MSTeamsReplyStyle;
4958
};
5059

5160
type SendMSTeamsMessageResult = {
@@ -131,7 +140,7 @@ type SendMSTeamsCardParams = {
131140
/** Adaptive Card JSON object */
132141
card: Record<string, unknown>;
133142
/** Force a new root post in the channel (channel only; ignored for DMs/group chats) */
134-
topLevel?: boolean;
143+
replyStyleOverride?: MSTeamsReplyStyle;
135144
};
136145

137146
type SendMSTeamsCardResult = {
@@ -153,7 +162,8 @@ type SendMSTeamsCardResult = {
153162
export async function sendMessageMSTeams(
154163
params: SendMSTeamsMessageParams,
155164
): Promise<SendMSTeamsMessageResult> {
156-
const { cfg, to, text, mediaUrl, filename, mediaLocalRoots, mediaReadFile, topLevel } = params;
165+
const { cfg, to, text, mediaUrl, filename, mediaLocalRoots, mediaReadFile, replyStyleOverride } =
166+
params;
157167
const tableMode = resolveMarkdownTableMode({
158168
cfg,
159169
channel: "msteams",
@@ -162,7 +172,7 @@ export async function sendMessageMSTeams(
162172
const ctx = await resolveMSTeamsSendContext({
163173
cfg,
164174
to,
165-
replyStyleOverride: topLevel ? "top-level" : undefined,
175+
replyStyleOverride,
166176
});
167177
const {
168178
app,
@@ -550,11 +560,11 @@ export async function sendPollMSTeams(
550560
export async function sendAdaptiveCardMSTeams(
551561
params: SendMSTeamsCardParams,
552562
): Promise<SendMSTeamsCardResult> {
553-
const { cfg, to, card, topLevel } = params;
563+
const { cfg, to, card, replyStyleOverride } = params;
554564
const { app, conversationId, ref, log, sdkCloudOptions } = await resolveMSTeamsSendContext({
555565
cfg,
556566
to,
557-
replyStyleOverride: topLevel ? "top-level" : undefined,
567+
replyStyleOverride,
558568
});
559569

560570
log.debug?.("sending adaptive card", {

0 commit comments

Comments
 (0)