feat(msteams): add replyStyle config for thread vs top-level replies
- Add replyStyle config at global, team, and channel levels - "thread" replies to the original message (for Posts layout channels) - "top-level" posts as a new message (for Threads layout channels) - Default based on requireMention: false → top-level, true → thread - DMs always use thread style (direct reply)
This commit is contained in:
@@ -126,32 +126,52 @@ export async function monitorMSTeamsProvider(
|
|||||||
});
|
});
|
||||||
const adapter = new CloudAdapter(authConfig);
|
const adapter = new CloudAdapter(authConfig);
|
||||||
|
|
||||||
// Helper to deliver replies as top-level messages (not threaded)
|
// Helper to deliver replies with configurable reply style
|
||||||
// We use proactive messaging to avoid threading to the original message
|
// - "thread": reply to the original message (for Posts layout channels)
|
||||||
|
// - "top-level": post as a new message (for Threads layout channels)
|
||||||
async function deliverReplies(params: {
|
async function deliverReplies(params: {
|
||||||
replies: ReplyPayload[];
|
replies: ReplyPayload[];
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
context: any; // TurnContext from SDK - has activity.getConversationReference()
|
context: any; // TurnContext from SDK - has activity.getConversationReference()
|
||||||
adapter: InstanceType<typeof CloudAdapter>;
|
adapter: InstanceType<typeof CloudAdapter>;
|
||||||
appId: string;
|
appId: string;
|
||||||
|
replyStyle: "thread" | "top-level";
|
||||||
}) {
|
}) {
|
||||||
const chunkLimit = Math.min(textLimit, 4000);
|
const chunkLimit = Math.min(textLimit, 4000);
|
||||||
|
|
||||||
// Get conversation reference from SDK's activity (includes proper bot info)
|
// For "thread" style, use context.sendActivity directly (replies to original message)
|
||||||
// Then remove activityId to avoid threading
|
// For "top-level" style, use proactive messaging without activityId
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
const sendMessage =
|
||||||
const fullRef = params.context.activity.getConversationReference() as any;
|
params.replyStyle === "thread"
|
||||||
const conversationRef = {
|
? async (message: string) => {
|
||||||
...fullRef,
|
await params.context.sendActivity({ type: "message", text: message });
|
||||||
activityId: undefined, // Remove to post as top-level message, not thread
|
}
|
||||||
};
|
: async (message: string) => {
|
||||||
// Also strip the messageid suffix from conversation.id if present
|
// Get conversation reference from SDK's activity (includes proper bot info)
|
||||||
if (conversationRef.conversation?.id) {
|
// Then remove activityId to avoid threading
|
||||||
conversationRef.conversation = {
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
...conversationRef.conversation,
|
const fullRef = params.context.activity.getConversationReference() as any;
|
||||||
id: conversationRef.conversation.id.split(";")[0],
|
const conversationRef = {
|
||||||
};
|
...fullRef,
|
||||||
}
|
activityId: undefined, // Remove to post as top-level message
|
||||||
|
};
|
||||||
|
// Also strip the messageid suffix from conversation.id if present
|
||||||
|
if (conversationRef.conversation?.id) {
|
||||||
|
conversationRef.conversation = {
|
||||||
|
...conversationRef.conversation,
|
||||||
|
id: conversationRef.conversation.id.split(";")[0],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
await (params.adapter as any).continueConversation(
|
||||||
|
params.appId,
|
||||||
|
conversationRef,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
async (ctx: any) => {
|
||||||
|
await ctx.sendActivity({ type: "message", text: message });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
for (const payload of params.replies) {
|
for (const payload of params.replies) {
|
||||||
const mediaList =
|
const mediaList =
|
||||||
@@ -159,18 +179,6 @@ export async function monitorMSTeamsProvider(
|
|||||||
const text = payload.text ?? "";
|
const text = payload.text ?? "";
|
||||||
if (!text && mediaList.length === 0) continue;
|
if (!text && mediaList.length === 0) continue;
|
||||||
|
|
||||||
const sendMessage = async (message: string) => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
await (params.adapter as any).continueConversation(
|
|
||||||
params.appId,
|
|
||||||
conversationRef,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
async (ctx: any) => {
|
|
||||||
await ctx.sendActivity({ type: "message", text: message });
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (mediaList.length === 0) {
|
if (mediaList.length === 0) {
|
||||||
for (const chunk of chunkMarkdownText(text, chunkLimit)) {
|
for (const chunk of chunkMarkdownText(text, chunkLimit)) {
|
||||||
const trimmed = chunk.trim();
|
const trimmed = chunk.trim();
|
||||||
@@ -335,15 +343,15 @@ export async function monitorMSTeamsProvider(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolve team/channel config for channels and group chats
|
||||||
|
const teamId = activity.channelData?.team?.id;
|
||||||
|
const channelId = conversationId;
|
||||||
|
const teamConfig = teamId ? msteamsCfg?.teams?.[teamId] : undefined;
|
||||||
|
const channelConfig = teamConfig?.channels?.[channelId];
|
||||||
|
|
||||||
// Check requireMention for channels and group chats
|
// Check requireMention for channels and group chats
|
||||||
if (!isDirectMessage) {
|
if (!isDirectMessage) {
|
||||||
const teamId = activity.channelData?.team?.id;
|
|
||||||
const channelId = conversationId;
|
|
||||||
|
|
||||||
// Resolution order: channel config > team config > global config > default (true)
|
// Resolution order: channel config > team config > global config > default (true)
|
||||||
const teamConfig = teamId ? msteamsCfg?.teams?.[teamId] : undefined;
|
|
||||||
const channelConfig = teamConfig?.channels?.[channelId];
|
|
||||||
|
|
||||||
const requireMention =
|
const requireMention =
|
||||||
channelConfig?.requireMention ??
|
channelConfig?.requireMention ??
|
||||||
teamConfig?.requireMention ??
|
teamConfig?.requireMention ??
|
||||||
@@ -363,6 +371,24 @@ export async function monitorMSTeamsProvider(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolve reply style for channels/groups
|
||||||
|
// Resolution order: channel config > team config > global config > default based on requireMention
|
||||||
|
// If requireMention is false (Threads layout), default to "top-level"
|
||||||
|
// If requireMention is true (Posts layout), default to "thread"
|
||||||
|
const explicitReplyStyle =
|
||||||
|
channelConfig?.replyStyle ??
|
||||||
|
teamConfig?.replyStyle ??
|
||||||
|
msteamsCfg?.replyStyle;
|
||||||
|
const effectiveRequireMention =
|
||||||
|
channelConfig?.requireMention ??
|
||||||
|
teamConfig?.requireMention ??
|
||||||
|
msteamsCfg?.requireMention ??
|
||||||
|
true;
|
||||||
|
// For DMs, always use "thread" style (direct reply)
|
||||||
|
const replyStyle: "thread" | "top-level" = isDirectMessage
|
||||||
|
? "thread"
|
||||||
|
: explicitReplyStyle ?? (effectiveRequireMention ? "thread" : "top-level");
|
||||||
|
|
||||||
// Format the message body with envelope
|
// Format the message body with envelope
|
||||||
const timestamp = parseTimestamp(activity.timestamp);
|
const timestamp = parseTimestamp(activity.timestamp);
|
||||||
const body = formatAgentEnvelope({
|
const body = formatAgentEnvelope({
|
||||||
@@ -420,6 +446,7 @@ export async function monitorMSTeamsProvider(
|
|||||||
context,
|
context,
|
||||||
adapter,
|
adapter,
|
||||||
appId,
|
appId,
|
||||||
|
replyStyle,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onError: (err, info) => {
|
onError: (err, info) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user