mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
improvement(ms-teams): resolve mentions accurately
This commit is contained in:
@@ -4,6 +4,7 @@ import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { downloadFileFromStorage, processFilesToUserFiles } from '@/lib/uploads/file-processing'
|
||||
import { generateRequestId } from '@/lib/utils'
|
||||
import { resolveMentionsForChannel } from '@/tools/microsoft_teams/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
@@ -140,23 +141,62 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
let messageContent = validatedData.content
|
||||
let contentType: 'text' | 'html' = 'text'
|
||||
const mentionEntities: Array<{
|
||||
type: 'mention'
|
||||
mentioned: { id: string; name: string }
|
||||
text: string
|
||||
}> = []
|
||||
|
||||
try {
|
||||
const mentionResult = await resolveMentionsForChannel(
|
||||
validatedData.content,
|
||||
validatedData.teamId,
|
||||
validatedData.channelId,
|
||||
validatedData.accessToken
|
||||
)
|
||||
|
||||
if (mentionResult.hasMentions) {
|
||||
contentType = 'html'
|
||||
mentionEntities.push(...mentionResult.entities)
|
||||
logger.info(`[${requestId}] Resolved ${mentionResult.entities.length} mention(s)`)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`[${requestId}] Failed to resolve mentions, continuing without them:`, error)
|
||||
}
|
||||
|
||||
if (attachments.length > 0) {
|
||||
contentType = 'html'
|
||||
const attachmentTags = attachments
|
||||
.map((att) => `<attachment id="${att.id}"></attachment>`)
|
||||
.join(' ')
|
||||
messageContent = `${validatedData.content}<br/>${attachmentTags}`
|
||||
}
|
||||
|
||||
const messageBody = {
|
||||
const messageBody: {
|
||||
body: {
|
||||
contentType: attachments.length > 0 ? 'html' : 'text',
|
||||
contentType: 'text' | 'html'
|
||||
content: string
|
||||
}
|
||||
attachments?: any[]
|
||||
mentions?: Array<{
|
||||
type: 'mention'
|
||||
mentioned: { id: string; name: string }
|
||||
text: string
|
||||
}>
|
||||
} = {
|
||||
body: {
|
||||
contentType,
|
||||
content: messageContent,
|
||||
},
|
||||
}
|
||||
|
||||
if (attachments.length > 0) {
|
||||
;(messageBody as any).attachments = attachments
|
||||
messageBody.attachments = attachments
|
||||
}
|
||||
|
||||
if (mentionEntities.length > 0) {
|
||||
messageBody.mentions = mentionEntities
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Sending message to Teams channel: ${validatedData.channelId}`)
|
||||
|
||||
@@ -4,6 +4,7 @@ import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { downloadFileFromStorage, processFilesToUserFiles } from '@/lib/uploads/file-processing'
|
||||
import { generateRequestId } from '@/lib/utils'
|
||||
import { resolveMentionsForChat } from '@/tools/microsoft_teams/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
@@ -138,23 +139,61 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
let messageContent = validatedData.content
|
||||
let contentType: 'text' | 'html' = 'text'
|
||||
const mentionEntities: Array<{
|
||||
type: 'mention'
|
||||
mentioned: { id: string; name: string }
|
||||
text: string
|
||||
}> = []
|
||||
|
||||
try {
|
||||
const mentionResult = await resolveMentionsForChat(
|
||||
validatedData.content,
|
||||
validatedData.chatId,
|
||||
validatedData.accessToken
|
||||
)
|
||||
|
||||
if (mentionResult.hasMentions) {
|
||||
contentType = 'html'
|
||||
mentionEntities.push(...mentionResult.entities)
|
||||
logger.info(`[${requestId}] Resolved ${mentionResult.entities.length} mention(s)`)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`[${requestId}] Failed to resolve mentions, continuing without them:`, error)
|
||||
}
|
||||
|
||||
if (attachments.length > 0) {
|
||||
contentType = 'html'
|
||||
const attachmentTags = attachments
|
||||
.map((att) => `<attachment id="${att.id}"></attachment>`)
|
||||
.join(' ')
|
||||
messageContent = `${validatedData.content}<br/>${attachmentTags}`
|
||||
}
|
||||
|
||||
const messageBody = {
|
||||
const messageBody: {
|
||||
body: {
|
||||
contentType: attachments.length > 0 ? 'html' : 'text',
|
||||
contentType: 'text' | 'html'
|
||||
content: string
|
||||
}
|
||||
attachments?: any[]
|
||||
mentions?: Array<{
|
||||
type: 'mention'
|
||||
mentioned: { id: string; name: string }
|
||||
text: string
|
||||
}>
|
||||
} = {
|
||||
body: {
|
||||
contentType,
|
||||
content: messageContent,
|
||||
},
|
||||
}
|
||||
|
||||
if (attachments.length > 0) {
|
||||
;(messageBody as any).attachments = attachments
|
||||
messageBody.attachments = attachments
|
||||
}
|
||||
|
||||
if (mentionEntities.length > 0) {
|
||||
messageBody.mentions = mentionEntities
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Sending message to Teams chat: ${validatedData.chatId}`)
|
||||
|
||||
@@ -4,6 +4,28 @@ import type { ToolFileData } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('MicrosoftTeamsUtils')
|
||||
|
||||
interface ParsedMention {
|
||||
name: string
|
||||
fullTag: string
|
||||
startIndex: number
|
||||
endIndex: number
|
||||
}
|
||||
|
||||
interface TeamMember {
|
||||
id: string
|
||||
displayName: string
|
||||
userIdentityType?: string
|
||||
}
|
||||
|
||||
export interface MentionEntity {
|
||||
type: 'mention'
|
||||
mentioned: {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
text: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform raw attachment data from Microsoft Graph API
|
||||
*/
|
||||
@@ -99,3 +121,161 @@ export async function fetchHostedContentsForChannelMessage(params: {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function parseMentions(content: string): ParsedMention[] {
|
||||
const mentions: ParsedMention[] = []
|
||||
const mentionRegex = /<at>([^<]+)<\/at>/gi
|
||||
let match: RegExpExecArray | null
|
||||
|
||||
while ((match = mentionRegex.exec(content)) !== null) {
|
||||
const name = match[1].trim()
|
||||
if (name) {
|
||||
mentions.push({
|
||||
name,
|
||||
fullTag: match[0],
|
||||
startIndex: match.index,
|
||||
endIndex: match.index + match[0].length,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return mentions
|
||||
}
|
||||
|
||||
async function fetchChatMembers(chatId: string, accessToken: string): Promise<TeamMember[]> {
|
||||
const response = await fetch(
|
||||
`https://graph.microsoft.com/v1.0/chats/${encodeURIComponent(chatId)}/members`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
return []
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return (data.value || []).map((member: TeamMember) => ({
|
||||
id: member.id,
|
||||
displayName: member.displayName || '',
|
||||
userIdentityType: member.userIdentityType,
|
||||
}))
|
||||
}
|
||||
|
||||
async function fetchChannelMembers(
|
||||
teamId: string,
|
||||
channelId: string,
|
||||
accessToken: string
|
||||
): Promise<TeamMember[]> {
|
||||
const response = await fetch(
|
||||
`https://graph.microsoft.com/v1.0/teams/${encodeURIComponent(teamId)}/channels/${encodeURIComponent(channelId)}/members`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
return []
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return (data.value || []).map((member: TeamMember) => ({
|
||||
id: member.id,
|
||||
displayName: member.displayName || '',
|
||||
userIdentityType: member.userIdentityType,
|
||||
}))
|
||||
}
|
||||
|
||||
function findMemberByName(members: TeamMember[], name: string): TeamMember | undefined {
|
||||
const normalizedName = name.trim().toLowerCase()
|
||||
return members.find((member) => member.displayName.toLowerCase() === normalizedName)
|
||||
}
|
||||
|
||||
export async function resolveMentionsForChat(
|
||||
content: string,
|
||||
chatId: string,
|
||||
accessToken: string
|
||||
): Promise<{ entities: MentionEntity[]; hasMentions: boolean }> {
|
||||
const parsedMentions = parseMentions(content)
|
||||
|
||||
if (parsedMentions.length === 0) {
|
||||
return { entities: [], hasMentions: false }
|
||||
}
|
||||
|
||||
const members = await fetchChatMembers(chatId, accessToken)
|
||||
const entities: MentionEntity[] = []
|
||||
const resolvedNames = new Set<string>()
|
||||
|
||||
for (const mention of parsedMentions) {
|
||||
if (resolvedNames.has(mention.fullTag)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const member = findMemberByName(members, mention.name)
|
||||
|
||||
if (member) {
|
||||
entities.push({
|
||||
type: 'mention',
|
||||
mentioned: {
|
||||
id: member.id,
|
||||
name: member.displayName,
|
||||
},
|
||||
text: mention.fullTag,
|
||||
})
|
||||
resolvedNames.add(mention.fullTag)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
entities,
|
||||
hasMentions: entities.length > 0,
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveMentionsForChannel(
|
||||
content: string,
|
||||
teamId: string,
|
||||
channelId: string,
|
||||
accessToken: string
|
||||
): Promise<{ entities: MentionEntity[]; hasMentions: boolean }> {
|
||||
const parsedMentions = parseMentions(content)
|
||||
|
||||
if (parsedMentions.length === 0) {
|
||||
return { entities: [], hasMentions: false }
|
||||
}
|
||||
|
||||
const members = await fetchChannelMembers(teamId, channelId, accessToken)
|
||||
const entities: MentionEntity[] = []
|
||||
const resolvedNames = new Set<string>()
|
||||
|
||||
for (const mention of parsedMentions) {
|
||||
if (resolvedNames.has(mention.fullTag)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const member = findMemberByName(members, mention.name)
|
||||
|
||||
if (member) {
|
||||
entities.push({
|
||||
type: 'mention',
|
||||
mentioned: {
|
||||
id: member.id,
|
||||
name: member.displayName,
|
||||
},
|
||||
text: mention.fullTag,
|
||||
})
|
||||
resolvedNames.add(mention.fullTag)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
entities,
|
||||
hasMentions: entities.length > 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,12 @@ export const writeChannelTool: ToolConfig<MicrosoftTeamsToolParams, MicrosoftTea
|
||||
return '/api/tools/microsoft_teams/write_channel'
|
||||
}
|
||||
|
||||
// If content contains mentions, use custom API route for mention resolution
|
||||
const hasMentions = /<at>[^<]+<\/at>/i.test(params.content || '')
|
||||
if (hasMentions) {
|
||||
return '/api/tools/microsoft_teams/write_channel'
|
||||
}
|
||||
|
||||
const encodedTeamId = encodeURIComponent(teamId)
|
||||
const encodedChannelId = encodeURIComponent(channelId)
|
||||
|
||||
@@ -98,7 +104,8 @@ export const writeChannelTool: ToolConfig<MicrosoftTeamsToolParams, MicrosoftTea
|
||||
throw new Error('Content is required')
|
||||
}
|
||||
|
||||
// If using custom API route (with files), pass all params
|
||||
// If using custom API route (with files or mentions), pass all params
|
||||
const hasMentions = /<at>[^<]+<\/at>/i.test(params.content || '')
|
||||
if (params.files && params.files.length > 0) {
|
||||
return {
|
||||
accessToken: params.accessToken,
|
||||
@@ -109,6 +116,15 @@ export const writeChannelTool: ToolConfig<MicrosoftTeamsToolParams, MicrosoftTea
|
||||
}
|
||||
}
|
||||
|
||||
if (hasMentions) {
|
||||
return {
|
||||
accessToken: params.accessToken,
|
||||
teamId: params.teamId,
|
||||
channelId: params.channelId,
|
||||
content: params.content,
|
||||
}
|
||||
}
|
||||
|
||||
// Microsoft Teams API expects this specific format for channel messages
|
||||
const requestBody = {
|
||||
body: {
|
||||
|
||||
@@ -62,6 +62,12 @@ export const writeChatTool: ToolConfig<MicrosoftTeamsToolParams, MicrosoftTeamsW
|
||||
return '/api/tools/microsoft_teams/write_chat'
|
||||
}
|
||||
|
||||
// If content contains mentions, use custom API route for mention resolution
|
||||
const hasMentions = /<at>[^<]+<\/at>/i.test(params.content || '')
|
||||
if (hasMentions) {
|
||||
return '/api/tools/microsoft_teams/write_chat'
|
||||
}
|
||||
|
||||
return `https://graph.microsoft.com/v1.0/chats/${encodeURIComponent(chatId)}/messages`
|
||||
},
|
||||
method: 'POST',
|
||||
@@ -82,7 +88,8 @@ export const writeChatTool: ToolConfig<MicrosoftTeamsToolParams, MicrosoftTeamsW
|
||||
throw new Error('Content is required')
|
||||
}
|
||||
|
||||
// If using custom API route (with files), pass all params
|
||||
// If using custom API route (with files or mentions), pass all params
|
||||
const hasMentions = /<at>[^<]+<\/at>/i.test(params.content || '')
|
||||
if (params.files && params.files.length > 0) {
|
||||
return {
|
||||
accessToken: params.accessToken,
|
||||
@@ -92,6 +99,14 @@ export const writeChatTool: ToolConfig<MicrosoftTeamsToolParams, MicrosoftTeamsW
|
||||
}
|
||||
}
|
||||
|
||||
if (hasMentions) {
|
||||
return {
|
||||
accessToken: params.accessToken,
|
||||
chatId: params.chatId,
|
||||
content: params.content,
|
||||
}
|
||||
}
|
||||
|
||||
// Microsoft Teams API expects this specific format
|
||||
const requestBody = {
|
||||
body: {
|
||||
|
||||
Reference in New Issue
Block a user