mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
improvement(mothership): tool display titles, html sanitization, and ui fixes (#3631)
* improvement(mothership): tool display titles, html sanitization, and ui fixes - Use TOOL_UI_METADATA as fallback for tool display titles (fast_edit shows "Editing workflow" instead of "Fast Edit") - Harden HTML-to-text extraction with replaceUntilStable to prevent nested tag injection - Decode HTML entities in a single pass to avoid double-unescaping - Fix Google Drive/Docs query escaping for backslashes in folder IDs - Replace regex with indexOf for email sender/display name parsing - Update embedded workflow run tooltip to "Run workflow" * fix(security): decode entities before tag stripping and cap loop iterations Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -267,11 +267,24 @@ async function createRejectedTask(
|
||||
* Format: "username@domain.com" or "Display Name <username@domain.com>"
|
||||
*/
|
||||
function extractSenderEmail(from: string): string {
|
||||
const match = from.match(/<([^>]+)>/)
|
||||
return (match?.[1] || from).toLowerCase().trim()
|
||||
const openBracket = from.indexOf('<')
|
||||
const closeBracket = from.indexOf('>', openBracket + 1)
|
||||
if (openBracket !== -1 && closeBracket !== -1) {
|
||||
return from
|
||||
.substring(openBracket + 1, closeBracket)
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
}
|
||||
return from.toLowerCase().trim()
|
||||
}
|
||||
|
||||
function extractDisplayName(from: string): string | null {
|
||||
const match = from.match(/^(.+?)\s*</)
|
||||
return match?.[1]?.trim().replace(/^"|"$/g, '') || null
|
||||
const openBracket = from.indexOf('<')
|
||||
if (openBracket <= 0) return null
|
||||
const name = from.substring(0, openBracket).trim()
|
||||
if (!name) return null
|
||||
if (name.startsWith('"') && name.endsWith('"')) {
|
||||
return name.slice(1, -1) || null
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import type { ContentBlock, OptionItem, SubagentName, ToolCallData } from '../../types'
|
||||
import { SUBAGENT_LABELS } from '../../types'
|
||||
import { SUBAGENT_LABELS, TOOL_UI_METADATA } from '../../types'
|
||||
import type { AgentGroupItem } from './components'
|
||||
import { AgentGroup, ChatContent, CircleStop, Options, PendingTagIndicator } from './components'
|
||||
|
||||
@@ -47,7 +47,10 @@ function toToolData(tc: NonNullable<ContentBlock['toolCall']>): ToolCallData {
|
||||
return {
|
||||
id: tc.id,
|
||||
toolName: tc.name,
|
||||
displayTitle: tc.displayTitle || formatToolName(tc.name),
|
||||
displayTitle:
|
||||
tc.displayTitle ||
|
||||
TOOL_UI_METADATA[tc.name as keyof typeof TOOL_UI_METADATA]?.title ||
|
||||
formatToolName(tc.name),
|
||||
status: tc.status,
|
||||
result: tc.result,
|
||||
}
|
||||
|
||||
@@ -197,7 +197,7 @@ export function EmbeddedWorkflowActions({ workspaceId, workflowId }: EmbeddedWor
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side='bottom'>
|
||||
<p>{isExecuting ? 'Stop' : 'Run'}</p>
|
||||
<p>{isExecuting ? 'Stop' : 'Run workflow'}</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</>
|
||||
|
||||
@@ -162,7 +162,7 @@ function buildQuery(sourceConfig: Record<string, unknown>): string {
|
||||
|
||||
const folderId = sourceConfig.folderId as string | undefined
|
||||
if (folderId?.trim()) {
|
||||
parts.push(`'${folderId.trim().replace(/'/g, "\\'")}' in parents`)
|
||||
parts.push(`'${folderId.trim().replace(/\\/g, '\\\\').replace(/'/g, "\\'")}' in parents`)
|
||||
}
|
||||
|
||||
return parts.join(' and ')
|
||||
|
||||
@@ -112,7 +112,7 @@ function buildQuery(sourceConfig: Record<string, unknown>): string {
|
||||
|
||||
const folderId = sourceConfig.folderId as string | undefined
|
||||
if (folderId?.trim()) {
|
||||
parts.push(`'${folderId.trim().replace(/'/g, "\\'")}' in parents`)
|
||||
parts.push(`'${folderId.trim().replace(/\\/g, '\\\\').replace(/'/g, "\\'")}' in parents`)
|
||||
}
|
||||
|
||||
const fileType = (sourceConfig.fileType as string) || 'all'
|
||||
|
||||
@@ -94,26 +94,67 @@ function isForwardedEmail(subject: string | null, body: string | null): boolean
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Repeatedly applies a regex replacement until the string stabilises.
|
||||
* Prevents incomplete sanitization from nested/overlapping patterns
|
||||
* like `<scr<script>ipt>`.
|
||||
*/
|
||||
export function replaceUntilStable(
|
||||
input: string,
|
||||
pattern: RegExp,
|
||||
replacement: string,
|
||||
maxIterations = 100
|
||||
): string {
|
||||
let prev = input
|
||||
let next = prev.replace(pattern, replacement)
|
||||
let iterations = 0
|
||||
while (next !== prev && iterations++ < maxIterations) {
|
||||
prev = next
|
||||
next = prev.replace(pattern, replacement)
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
const HTML_ENTITY_MAP: Record<string, string> = {
|
||||
' ': ' ',
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
''': "'",
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes known HTML entities in a single pass to avoid double-unescaping.
|
||||
* A two-step decode (e.g. `&` -> `&` then `<` -> `<`) would turn
|
||||
* `&lt;` into `<`, which is incorrect.
|
||||
*/
|
||||
function decodeHtmlEntities(text: string): string {
|
||||
return text.replace(/&(?:nbsp|amp|lt|gt|quot|#39);/g, (match) => HTML_ENTITY_MAP[match] ?? match)
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic HTML to text extraction.
|
||||
*/
|
||||
function extractTextFromHtml(html: string | null): string | null {
|
||||
if (!html) return null
|
||||
|
||||
return html
|
||||
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
||||
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
||||
let text = html
|
||||
|
||||
text = decodeHtmlEntities(text)
|
||||
|
||||
text = replaceUntilStable(text, /<style[^>]*>[\s\S]*?<\/style\s*>/gi, '')
|
||||
text = replaceUntilStable(text, /<script[^>]*>[\s\S]*?<\/script\s*>/gi, '')
|
||||
|
||||
text = text
|
||||
.replace(/<br\s*\/?>/gi, '\n')
|
||||
.replace(/<\/p>/gi, '\n\n')
|
||||
.replace(/<\/div>/gi, '\n')
|
||||
.replace(/<\/li>/gi, '\n')
|
||||
.replace(/<[^>]+>/g, '')
|
||||
.replace(/ /g, ' ')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/\n{3,}/g, '\n\n')
|
||||
.trim()
|
||||
|
||||
text = replaceUntilStable(text, /<[^>]+>/g, '')
|
||||
|
||||
text = text.replace(/\n{3,}/g, '\n\n').trim()
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
|
||||
import { marked } from 'marked'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import * as agentmail from '@/lib/mothership/inbox/agentmail-client'
|
||||
import { replaceUntilStable } from '@/lib/mothership/inbox/format'
|
||||
import type { InboxTask } from '@/lib/mothership/inbox/types'
|
||||
|
||||
const logger = createLogger('InboxResponse')
|
||||
@@ -82,7 +83,9 @@ const EMAIL_STYLES = `
|
||||
function stripRawHtml(text: string): string {
|
||||
return text
|
||||
.split(/(```[\s\S]*?```)/g)
|
||||
.map((segment, i) => (i % 2 === 0 ? segment.replace(/<\/?[a-z][^>]*>/gi, '') : segment))
|
||||
.map((segment, i) =>
|
||||
i % 2 === 0 ? replaceUntilStable(segment, /<\/?[a-z][^>]*>/gi, '') : segment
|
||||
)
|
||||
.join('')
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user