mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-03 11:14:58 -05:00
more integrations
This commit is contained in:
@@ -7,6 +7,7 @@ import binaryExtensionsList from 'binary-extensions'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { secureFetchWithPinnedIP, validateUrlWithDNS } from '@/lib/core/security/input-validation'
|
||||
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
|
||||
import { isSupportedFileType, parseFile } from '@/lib/file-parsers'
|
||||
import { isUsingCloudStorage, type StorageContext, StorageService } from '@/lib/uploads'
|
||||
import { uploadExecutionFile } from '@/lib/uploads/contexts/execution'
|
||||
@@ -367,7 +368,7 @@ async function handleExternalUrl(
|
||||
throw new Error(`File too large: ${buffer.length} bytes (max: ${MAX_DOWNLOAD_SIZE_BYTES})`)
|
||||
}
|
||||
|
||||
logger.info(`Downloaded file from URL: ${url}, size: ${buffer.length} bytes`)
|
||||
logger.info(`Downloaded file from URL: ${sanitizeUrlForLog(url)}, size: ${buffer.length} bytes`)
|
||||
|
||||
let userFile: UserFile | undefined
|
||||
const mimeType = response.headers.get('content-type') || getMimeTypeFromExtension(extension)
|
||||
@@ -420,7 +421,7 @@ async function handleExternalUrl(
|
||||
|
||||
return parseResult
|
||||
} catch (error) {
|
||||
logger.error(`Error handling external URL ${url}:`, error)
|
||||
logger.error(`Error handling external URL ${sanitizeUrlForLog(url)}:`, error)
|
||||
return {
|
||||
success: false,
|
||||
error: `Error fetching URL: ${(error as Error).message}`,
|
||||
|
||||
@@ -102,6 +102,12 @@ export async function POST(request: NextRequest) {
|
||||
logger.info(`[${requestId}] Processing ${validatedData.files.length} file(s)`)
|
||||
|
||||
const userFiles = processFilesToUserFiles(validatedData.files, requestId, logger)
|
||||
const filesOutput: Array<{
|
||||
name: string
|
||||
mimeType: string
|
||||
data: string
|
||||
size: number
|
||||
}> = []
|
||||
|
||||
if (userFiles.length === 0) {
|
||||
logger.warn(`[${requestId}] No valid files to upload, falling back to text-only`)
|
||||
@@ -138,6 +144,12 @@ export async function POST(request: NextRequest) {
|
||||
logger.info(`[${requestId}] Downloading file ${i}: ${userFile.name}`)
|
||||
|
||||
const buffer = await downloadFileFromStorage(userFile, requestId, logger)
|
||||
filesOutput.push({
|
||||
name: userFile.name,
|
||||
mimeType: userFile.type || 'application/octet-stream',
|
||||
data: buffer.toString('base64'),
|
||||
size: buffer.length,
|
||||
})
|
||||
|
||||
const blob = new Blob([new Uint8Array(buffer)], { type: userFile.type })
|
||||
formData.append(`files[${i}]`, blob, userFile.name)
|
||||
@@ -174,6 +186,7 @@ export async function POST(request: NextRequest) {
|
||||
message: data.content,
|
||||
data: data,
|
||||
fileCount: userFiles.length,
|
||||
files: filesOutput,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateImageUrl } from '@/lib/core/security/input-validation'
|
||||
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
|
||||
const logger = createLogger('ImageProxyAPI')
|
||||
@@ -29,13 +30,13 @@ export async function GET(request: NextRequest) {
|
||||
const urlValidation = validateImageUrl(imageUrl)
|
||||
if (!urlValidation.isValid) {
|
||||
logger.warn(`[${requestId}] Blocked image proxy request`, {
|
||||
url: imageUrl.substring(0, 100),
|
||||
url: sanitizeUrlForLog(imageUrl),
|
||||
error: urlValidation.error,
|
||||
})
|
||||
return new NextResponse(urlValidation.error || 'Invalid image URL', { status: 403 })
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Proxying image request for: ${imageUrl}`)
|
||||
logger.info(`[${requestId}] Proxying image request for: ${sanitizeUrlForLog(imageUrl)}`)
|
||||
|
||||
try {
|
||||
const imageResponse = await fetch(imageUrl, {
|
||||
|
||||
121
apps/sim/app/api/tools/jira/add-attachment/route.ts
Normal file
121
apps/sim/app/api/tools/jira/add-attachment/route.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas'
|
||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
import { getJiraCloudId } from '@/tools/jira/utils'
|
||||
|
||||
const logger = createLogger('JiraAddAttachmentAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const JiraAddAttachmentSchema = z.object({
|
||||
accessToken: z.string().min(1, 'Access token is required'),
|
||||
domain: z.string().min(1, 'Domain is required'),
|
||||
issueKey: z.string().min(1, 'Issue key is required'),
|
||||
files: RawFileInputArraySchema,
|
||||
cloudId: z.string().optional().nullable(),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = `jira-attach-${Date.now()}`
|
||||
|
||||
try {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
if (!authResult.success) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: authResult.error || 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const validatedData = JiraAddAttachmentSchema.parse(body)
|
||||
|
||||
const userFiles = processFilesToUserFiles(validatedData.files, requestId, logger)
|
||||
if (userFiles.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'No valid files provided for upload' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const cloudId =
|
||||
validatedData.cloudId ||
|
||||
(await getJiraCloudId(validatedData.domain, validatedData.accessToken))
|
||||
|
||||
const formData = new FormData()
|
||||
const filesOutput: Array<{ name: string; mimeType: string; data: string; size: number }> = []
|
||||
|
||||
for (const file of userFiles) {
|
||||
const buffer = await downloadFileFromStorage(file, requestId, logger)
|
||||
filesOutput.push({
|
||||
name: file.name,
|
||||
mimeType: file.type || 'application/octet-stream',
|
||||
data: buffer.toString('base64'),
|
||||
size: buffer.length,
|
||||
})
|
||||
const blob = new Blob([new Uint8Array(buffer)], {
|
||||
type: file.type || 'application/octet-stream',
|
||||
})
|
||||
formData.append('file', blob, file.name)
|
||||
}
|
||||
|
||||
const url = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${validatedData.issueKey}/attachments`
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${validatedData.accessToken}`,
|
||||
'X-Atlassian-Token': 'no-check',
|
||||
},
|
||||
body: formData,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error(`[${requestId}] Jira attachment upload failed`, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: `Failed to upload attachments: ${response.statusText}`,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const attachments = await response.json()
|
||||
const attachmentIds = Array.isArray(attachments)
|
||||
? attachments.map((attachment) => attachment.id).filter(Boolean)
|
||||
: []
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
issueKey: validatedData.issueKey,
|
||||
attachmentIds,
|
||||
files: filesOutput,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Invalid request data', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
logger.error(`[${requestId}] Jira attachment upload error`, error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: error instanceof Error ? error.message : 'Internal server error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
|
||||
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -62,7 +63,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
const url = `${baseUrl}/servicedesk/${serviceDeskId}/queue${params.toString() ? `?${params.toString()}` : ''}`
|
||||
|
||||
logger.info('Fetching queues from:', url)
|
||||
logger.info('Fetching queues from:', sanitizeUrlForLog(url))
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
validateJiraCloudId,
|
||||
validateJiraIssueKey,
|
||||
} from '@/lib/core/security/input-validation'
|
||||
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
|
||||
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -66,7 +67,7 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
const url = `${baseUrl}/request`
|
||||
|
||||
logger.info('Creating request at:', url)
|
||||
logger.info('Creating request at:', sanitizeUrlForLog(url))
|
||||
|
||||
const requestBody: Record<string, unknown> = {
|
||||
serviceDeskId,
|
||||
@@ -128,7 +129,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
const url = `${baseUrl}/request/${issueIdOrKey}`
|
||||
|
||||
logger.info('Fetching request from:', url)
|
||||
logger.info('Fetching request from:', sanitizeUrlForLog(url))
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
|
||||
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -68,7 +69,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
const url = `${baseUrl}/request${params.toString() ? `?${params.toString()}` : ''}`
|
||||
|
||||
logger.info('Fetching requests from:', url)
|
||||
logger.info('Fetching requests from:', sanitizeUrlForLog(url))
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
|
||||
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -53,7 +54,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
const url = `${baseUrl}/servicedesk/${serviceDeskId}/requesttype${params.toString() ? `?${params.toString()}` : ''}`
|
||||
|
||||
logger.info('Fetching request types from:', url)
|
||||
logger.info('Fetching request types from:', sanitizeUrlForLog(url))
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateJiraCloudId } from '@/lib/core/security/input-validation'
|
||||
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
|
||||
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -43,7 +44,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
const url = `${baseUrl}/servicedesk${params.toString() ? `?${params.toString()}` : ''}`
|
||||
|
||||
logger.info('Fetching service desks from:', url)
|
||||
logger.info('Fetching service desks from:', sanitizeUrlForLog(url))
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
|
||||
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
|
||||
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -53,7 +54,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
const url = `${baseUrl}/request/${issueIdOrKey}/sla${params.toString() ? `?${params.toString()}` : ''}`
|
||||
|
||||
logger.info('Fetching SLA info from:', url)
|
||||
logger.info('Fetching SLA info from:', sanitizeUrlForLog(url))
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
validateJiraCloudId,
|
||||
validateJiraIssueKey,
|
||||
} from '@/lib/core/security/input-validation'
|
||||
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
|
||||
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -69,7 +70,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
const url = `${baseUrl}/request/${issueIdOrKey}/transition`
|
||||
|
||||
logger.info('Transitioning request at:', url)
|
||||
logger.info('Transitioning request at:', sanitizeUrlForLog(url))
|
||||
|
||||
const body: Record<string, unknown> = {
|
||||
id: transitionId,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
|
||||
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
|
||||
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -49,7 +50,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
const url = `${baseUrl}/request/${issueIdOrKey}/transition`
|
||||
|
||||
logger.info('Fetching transitions from:', url)
|
||||
logger.info('Fetching transitions from:', sanitizeUrlForLog(url))
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas'
|
||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||
@@ -55,6 +56,12 @@ export async function POST(request: NextRequest) {
|
||||
})
|
||||
|
||||
const attachments: any[] = []
|
||||
const filesOutput: Array<{
|
||||
name: string
|
||||
mimeType: string
|
||||
data: string
|
||||
size: number
|
||||
}> = []
|
||||
if (validatedData.files && validatedData.files.length > 0) {
|
||||
const rawFiles = validatedData.files
|
||||
logger.info(`[${requestId}] Processing ${rawFiles.length} file(s) for upload to OneDrive`)
|
||||
@@ -66,13 +73,19 @@ export async function POST(request: NextRequest) {
|
||||
logger.info(`[${requestId}] Uploading file to Teams: ${file.name} (${file.size} bytes)`)
|
||||
|
||||
const buffer = await downloadFileFromStorage(file, requestId, logger)
|
||||
filesOutput.push({
|
||||
name: file.name,
|
||||
mimeType: file.type || 'application/octet-stream',
|
||||
data: buffer.toString('base64'),
|
||||
size: buffer.length,
|
||||
})
|
||||
|
||||
const uploadUrl =
|
||||
'https://graph.microsoft.com/v1.0/me/drive/root:/TeamsAttachments/' +
|
||||
encodeURIComponent(file.name) +
|
||||
':/content'
|
||||
|
||||
logger.info(`[${requestId}] Uploading to Teams: ${uploadUrl}`)
|
||||
logger.info(`[${requestId}] Uploading to Teams: ${sanitizeUrlForLog(uploadUrl)}`)
|
||||
|
||||
const uploadResponse = await fetch(uploadUrl, {
|
||||
method: 'PUT',
|
||||
@@ -238,6 +251,7 @@ export async function POST(request: NextRequest) {
|
||||
url: responseData.webUrl || '',
|
||||
attachmentCount: attachments.length,
|
||||
},
|
||||
files: filesOutput,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas'
|
||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||
@@ -53,6 +54,12 @@ export async function POST(request: NextRequest) {
|
||||
})
|
||||
|
||||
const attachments: any[] = []
|
||||
const filesOutput: Array<{
|
||||
name: string
|
||||
mimeType: string
|
||||
data: string
|
||||
size: number
|
||||
}> = []
|
||||
if (validatedData.files && validatedData.files.length > 0) {
|
||||
const rawFiles = validatedData.files
|
||||
logger.info(`[${requestId}] Processing ${rawFiles.length} file(s) for upload to Teams`)
|
||||
@@ -64,13 +71,19 @@ export async function POST(request: NextRequest) {
|
||||
logger.info(`[${requestId}] Uploading file to Teams: ${file.name} (${file.size} bytes)`)
|
||||
|
||||
const buffer = await downloadFileFromStorage(file, requestId, logger)
|
||||
filesOutput.push({
|
||||
name: file.name,
|
||||
mimeType: file.type || 'application/octet-stream',
|
||||
data: buffer.toString('base64'),
|
||||
size: buffer.length,
|
||||
})
|
||||
|
||||
const uploadUrl =
|
||||
'https://graph.microsoft.com/v1.0/me/drive/root:/TeamsAttachments/' +
|
||||
encodeURIComponent(file.name) +
|
||||
':/content'
|
||||
|
||||
logger.info(`[${requestId}] Uploading to Teams: ${uploadUrl}`)
|
||||
logger.info(`[${requestId}] Uploading to Teams: ${sanitizeUrlForLog(uploadUrl)}`)
|
||||
|
||||
const uploadResponse = await fetch(uploadUrl, {
|
||||
method: 'PUT',
|
||||
@@ -234,6 +247,7 @@ export async function POST(request: NextRequest) {
|
||||
url: responseData.webUrl || '',
|
||||
attachmentCount: attachments.length,
|
||||
},
|
||||
files: filesOutput,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { StorageService } from '@/lib/uploads'
|
||||
import { FileInputSchema } from '@/lib/uploads/utils/file-schemas'
|
||||
import {
|
||||
extractStorageKey,
|
||||
inferContextFromKey,
|
||||
@@ -19,7 +20,7 @@ const logger = createLogger('MistralParseAPI')
|
||||
const MistralParseSchema = z.object({
|
||||
apiKey: z.string().min(1, 'API key is required'),
|
||||
filePath: z.string().min(1, 'File path is required').optional(),
|
||||
fileData: z.unknown().optional(),
|
||||
fileData: FileInputSchema.optional(),
|
||||
resultType: z.string().optional(),
|
||||
pages: z.array(z.number()).optional(),
|
||||
includeImageBase64: z.boolean().optional(),
|
||||
|
||||
@@ -4,6 +4,7 @@ import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { getFileExtension, getMimeTypeFromExtension } from '@/lib/uploads/utils/file-utils'
|
||||
import { createSftpConnection, getSftp, isPathSafe, sanitizePath } from '@/app/api/tools/sftp/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -111,6 +112,8 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
const buffer = Buffer.concat(chunks)
|
||||
const fileName = path.basename(remotePath)
|
||||
const extension = getFileExtension(fileName)
|
||||
const mimeType = getMimeTypeFromExtension(extension)
|
||||
|
||||
let content: string
|
||||
if (params.encoding === 'base64') {
|
||||
@@ -124,6 +127,12 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
fileName,
|
||||
file: {
|
||||
name: fileName,
|
||||
mimeType,
|
||||
data: buffer.toString('base64'),
|
||||
size: buffer.length,
|
||||
},
|
||||
content,
|
||||
size: buffer.length,
|
||||
encoding: params.encoding,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas'
|
||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||
@@ -144,7 +145,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
const uploadUrl = `https://graph.microsoft.com/v1.0/sites/${validatedData.siteId}/drives/${effectiveDriveId}/root:${encodedPath}:/content`
|
||||
|
||||
logger.info(`[${requestId}] Uploading to: ${uploadUrl}`)
|
||||
logger.info(`[${requestId}] Uploading to: ${sanitizeUrlForLog(uploadUrl)}`)
|
||||
|
||||
const uploadResponse = await fetch(uploadUrl, {
|
||||
method: 'PUT',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Logger } from '@sim/logger'
|
||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
import type { ToolFileData } from '@/tools/types'
|
||||
|
||||
/**
|
||||
* Sends a message to a Slack channel using chat.postMessage
|
||||
@@ -70,14 +71,21 @@ export async function uploadFilesToSlack(
|
||||
accessToken: string,
|
||||
requestId: string,
|
||||
logger: Logger
|
||||
): Promise<string[]> {
|
||||
): Promise<{ fileIds: string[]; files: ToolFileData[] }> {
|
||||
const userFiles = processFilesToUserFiles(files, requestId, logger)
|
||||
const uploadedFileIds: string[] = []
|
||||
const uploadedFiles: ToolFileData[] = []
|
||||
|
||||
for (const userFile of userFiles) {
|
||||
logger.info(`[${requestId}] Uploading file: ${userFile.name}`)
|
||||
|
||||
const buffer = await downloadFileFromStorage(userFile, requestId, logger)
|
||||
uploadedFiles.push({
|
||||
name: userFile.name,
|
||||
mimeType: userFile.type || 'application/octet-stream',
|
||||
data: buffer.toString('base64'),
|
||||
size: buffer.length,
|
||||
})
|
||||
|
||||
const getUrlResponse = await fetch('https://slack.com/api/files.getUploadURLExternal', {
|
||||
method: 'POST',
|
||||
@@ -114,7 +122,7 @@ export async function uploadFilesToSlack(
|
||||
uploadedFileIds.push(urlData.file_id)
|
||||
}
|
||||
|
||||
return uploadedFileIds
|
||||
return { fileIds: uploadedFileIds, files: uploadedFiles }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -217,7 +225,13 @@ export async function sendSlackMessage(
|
||||
logger: Logger
|
||||
): Promise<{
|
||||
success: boolean
|
||||
output?: { message: any; ts: string; channel: string; fileCount?: number }
|
||||
output?: {
|
||||
message: any
|
||||
ts: string
|
||||
channel: string
|
||||
fileCount?: number
|
||||
files?: ToolFileData[]
|
||||
}
|
||||
error?: string
|
||||
}> {
|
||||
const { accessToken, text, threadTs, files } = params
|
||||
@@ -249,10 +263,15 @@ export async function sendSlackMessage(
|
||||
|
||||
// Process files
|
||||
logger.info(`[${requestId}] Processing ${files.length} file(s)`)
|
||||
const uploadedFileIds = await uploadFilesToSlack(files, accessToken, requestId, logger)
|
||||
const { fileIds, files: uploadedFiles } = await uploadFilesToSlack(
|
||||
files,
|
||||
accessToken,
|
||||
requestId,
|
||||
logger
|
||||
)
|
||||
|
||||
// No valid files uploaded - send text-only
|
||||
if (uploadedFileIds.length === 0) {
|
||||
if (fileIds.length === 0) {
|
||||
logger.warn(`[${requestId}] No valid files to upload, sending text-only message`)
|
||||
|
||||
const data = await postSlackMessage(accessToken, channel, text, threadTs)
|
||||
@@ -265,7 +284,7 @@ export async function sendSlackMessage(
|
||||
}
|
||||
|
||||
// Complete file upload
|
||||
const completeData = await completeSlackFileUpload(uploadedFileIds, channel, text, accessToken)
|
||||
const completeData = await completeSlackFileUpload(fileIds, channel, text, accessToken)
|
||||
|
||||
if (!completeData.ok) {
|
||||
logger.error(`[${requestId}] Failed to complete upload:`, completeData.error)
|
||||
@@ -282,7 +301,8 @@ export async function sendSlackMessage(
|
||||
message: fileMessage,
|
||||
ts: fileMessage.ts,
|
||||
channel,
|
||||
fileCount: uploadedFileIds.length,
|
||||
fileCount: fileIds.length,
|
||||
files: uploadedFiles,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { type NextRequest, NextResponse } from 'next/server'
|
||||
import type { Client, SFTPWrapper } from 'ssh2'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { getFileExtension, getMimeTypeFromExtension } from '@/lib/uploads/utils/file-utils'
|
||||
import { createSSHConnection, sanitizePath } from '@/app/api/tools/ssh/utils'
|
||||
|
||||
const logger = createLogger('SSHDownloadFileAPI')
|
||||
@@ -96,6 +97,8 @@ export async function POST(request: NextRequest) {
|
||||
})
|
||||
|
||||
const fileName = path.basename(remotePath)
|
||||
const extension = getFileExtension(fileName)
|
||||
const mimeType = getMimeTypeFromExtension(extension)
|
||||
|
||||
// Encode content as base64 for binary safety
|
||||
const base64Content = content.toString('base64')
|
||||
@@ -104,6 +107,12 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
return NextResponse.json({
|
||||
downloaded: true,
|
||||
file: {
|
||||
name: fileName,
|
||||
mimeType,
|
||||
data: base64Content,
|
||||
size: stats.size,
|
||||
},
|
||||
content: base64Content,
|
||||
fileName: fileName,
|
||||
remotePath: remotePath,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { env } from '@/lib/core/config/env'
|
||||
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
|
||||
import { ensureZodObject, normalizeUrl } from '@/app/api/tools/stagehand/utils'
|
||||
|
||||
const logger = createLogger('StagehandExtractAPI')
|
||||
@@ -120,7 +121,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
const page = stagehand.context.pages()[0]
|
||||
|
||||
logger.info(`Navigating to ${url}`)
|
||||
logger.info(`Navigating to ${sanitizeUrlForLog(url)}`)
|
||||
await page.goto(url, { waitUntil: 'networkidle' })
|
||||
logger.info('Navigation complete')
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { extractAudioFromVideo, isVideoFile } from '@/lib/audio/extractor'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
import type { UserFile } from '@/executor/types'
|
||||
import type { TranscriptSegment } from '@/tools/stt/types'
|
||||
@@ -88,7 +89,7 @@ export async function POST(request: NextRequest) {
|
||||
audioFileName = file.name
|
||||
audioMimeType = file.type
|
||||
} else if (body.audioUrl) {
|
||||
logger.info(`[${requestId}] Downloading from URL: ${body.audioUrl}`)
|
||||
logger.info(`[${requestId}] Downloading from URL: ${sanitizeUrlForLog(body.audioUrl)}`)
|
||||
|
||||
const response = await fetch(body.audioUrl)
|
||||
if (!response.ok) {
|
||||
|
||||
@@ -94,6 +94,14 @@ export async function POST(request: NextRequest) {
|
||||
logger.info(`[${requestId}] Uploading document: ${userFile.name}`)
|
||||
|
||||
const buffer = await downloadFileFromStorage(userFile, requestId, logger)
|
||||
const filesOutput = [
|
||||
{
|
||||
name: userFile.name,
|
||||
mimeType: userFile.type || 'application/octet-stream',
|
||||
data: buffer.toString('base64'),
|
||||
size: buffer.length,
|
||||
},
|
||||
]
|
||||
|
||||
logger.info(`[${requestId}] Downloaded file: ${buffer.length} bytes`)
|
||||
|
||||
@@ -136,6 +144,7 @@ export async function POST(request: NextRequest) {
|
||||
output: {
|
||||
message: 'Document sent successfully',
|
||||
data: data.result,
|
||||
files: filesOutput,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { createLogger } from '@sim/logger'
|
||||
import { ArrowDown, Loader2 } from 'lucide-react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Button } from '@/components/emcn'
|
||||
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
|
||||
import { extractWorkspaceIdFromExecutionKey, getViewerUrl } from '@/lib/uploads/utils/file-utils'
|
||||
|
||||
const logger = createLogger('FileCards')
|
||||
@@ -57,7 +58,7 @@ function FileCard({ file, isExecutionFile = false, workspaceId }: FileCardProps)
|
||||
if (file.key.startsWith('url/')) {
|
||||
if (file.url) {
|
||||
window.open(file.url, '_blank')
|
||||
logger.info(`Opened URL-type file directly: ${file.url}`)
|
||||
logger.info(`Opened URL-type file directly: ${sanitizeUrlForLog(file.url)}`)
|
||||
return
|
||||
}
|
||||
throw new Error('URL is required for URL-type files')
|
||||
@@ -77,13 +78,13 @@ function FileCard({ file, isExecutionFile = false, workspaceId }: FileCardProps)
|
||||
const serveUrl =
|
||||
file.url || `/api/files/serve/${encodeURIComponent(file.key)}?context=execution`
|
||||
window.open(serveUrl, '_blank')
|
||||
logger.info(`Opened execution file serve URL: ${serveUrl}`)
|
||||
logger.info(`Opened execution file serve URL: ${sanitizeUrlForLog(serveUrl)}`)
|
||||
} else {
|
||||
const viewerUrl = resolvedWorkspaceId ? getViewerUrl(file.key, resolvedWorkspaceId) : null
|
||||
|
||||
if (viewerUrl) {
|
||||
router.push(viewerUrl)
|
||||
logger.info(`Navigated to viewer URL: ${viewerUrl}`)
|
||||
logger.info(`Navigated to viewer URL: ${sanitizeUrlForLog(viewerUrl)}`)
|
||||
} else {
|
||||
logger.warn(
|
||||
`Could not construct viewer URL for file: ${file.name}, falling back to serve URL`
|
||||
|
||||
@@ -807,7 +807,7 @@ export function Chat() {
|
||||
|
||||
const newReservedFields: StartInputFormatField[] = missingStartReservedFields.map(
|
||||
(fieldName) => {
|
||||
const defaultType = fieldName === 'files' ? 'files' : 'string'
|
||||
const defaultType = fieldName === 'files' ? 'file[]' : 'string'
|
||||
|
||||
return {
|
||||
id: crypto.randomUUID(),
|
||||
|
||||
@@ -225,7 +225,7 @@ const getOutputTypeForPath = (
|
||||
const chatModeTypes: Record<string, string> = {
|
||||
input: 'string',
|
||||
conversationId: 'string',
|
||||
files: 'files',
|
||||
files: 'file[]',
|
||||
}
|
||||
return chatModeTypes[outputPath] || 'any'
|
||||
}
|
||||
@@ -1563,16 +1563,11 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
blockTagGroups.sort((a, b) => a.distance - b.distance)
|
||||
finalBlockTagGroups.push(...blockTagGroups)
|
||||
|
||||
const contextualTags: string[] = []
|
||||
if (loopBlockGroup) {
|
||||
contextualTags.push(...loopBlockGroup.tags)
|
||||
}
|
||||
if (parallelBlockGroup) {
|
||||
contextualTags.push(...parallelBlockGroup.tags)
|
||||
}
|
||||
const groupTags = finalBlockTagGroups.flatMap((group) => group.tags)
|
||||
const tags = [...groupTags, ...variableTags]
|
||||
|
||||
return {
|
||||
tags: [...allBlockTags, ...variableTags, ...contextualTags],
|
||||
tags,
|
||||
variableInfoMap,
|
||||
blockTagGroups: finalBlockTagGroups,
|
||||
}
|
||||
|
||||
@@ -578,13 +578,20 @@ export const DiscordBlock: BlockConfig<DiscordResponse> = {
|
||||
if (!params.serverId) throw new Error('Server ID is required')
|
||||
|
||||
switch (params.operation) {
|
||||
case 'discord_send_message':
|
||||
case 'discord_send_message': {
|
||||
const fileParam = params.attachmentFiles || params.files
|
||||
const normalizedFiles = fileParam
|
||||
? Array.isArray(fileParam)
|
||||
? fileParam
|
||||
: [fileParam]
|
||||
: undefined
|
||||
return {
|
||||
...commonParams,
|
||||
channelId: params.channelId,
|
||||
content: params.content,
|
||||
files: params.attachmentFiles || params.files,
|
||||
files: normalizedFiles,
|
||||
}
|
||||
}
|
||||
case 'discord_get_messages':
|
||||
return {
|
||||
...commonParams,
|
||||
@@ -789,6 +796,7 @@ export const DiscordBlock: BlockConfig<DiscordResponse> = {
|
||||
},
|
||||
outputs: {
|
||||
message: { type: 'string', description: 'Status message' },
|
||||
files: { type: 'file[]', description: 'Files attached to the message' },
|
||||
data: { type: 'json', description: 'Response data' },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -73,5 +73,6 @@ export const ElevenLabsBlock: BlockConfig<ElevenLabsBlockResponse> = {
|
||||
|
||||
outputs: {
|
||||
audioUrl: { type: 'string', description: 'Generated audio URL' },
|
||||
audioFile: { type: 'file', description: 'Generated audio file' },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
|
||||
{ label: 'Update Comment', id: 'update_comment' },
|
||||
{ label: 'Delete Comment', id: 'delete_comment' },
|
||||
{ label: 'Get Attachments', id: 'get_attachments' },
|
||||
{ label: 'Add Attachment', id: 'add_attachment' },
|
||||
{ label: 'Delete Attachment', id: 'delete_attachment' },
|
||||
{ label: 'Add Worklog', id: 'add_worklog' },
|
||||
{ label: 'Get Worklogs', id: 'get_worklogs' },
|
||||
@@ -137,6 +138,7 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
|
||||
'update_comment',
|
||||
'delete_comment',
|
||||
'get_attachments',
|
||||
'add_attachment',
|
||||
'add_worklog',
|
||||
'get_worklogs',
|
||||
'update_worklog',
|
||||
@@ -168,6 +170,7 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
|
||||
'update_comment',
|
||||
'delete_comment',
|
||||
'get_attachments',
|
||||
'add_attachment',
|
||||
'add_worklog',
|
||||
'get_worklogs',
|
||||
'update_worklog',
|
||||
@@ -407,6 +410,27 @@ Return ONLY the comment text - no explanations.`,
|
||||
condition: { field: 'operation', value: ['update_comment', 'delete_comment'] },
|
||||
},
|
||||
// Attachment fields
|
||||
{
|
||||
id: 'attachmentFiles',
|
||||
title: 'Attachments',
|
||||
type: 'file-upload',
|
||||
canonicalParamId: 'files',
|
||||
placeholder: 'Upload files',
|
||||
condition: { field: 'operation', value: 'add_attachment' },
|
||||
mode: 'basic',
|
||||
multiple: true,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'files',
|
||||
title: 'File References',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'files',
|
||||
placeholder: 'File reference from previous block',
|
||||
condition: { field: 'operation', value: 'add_attachment' },
|
||||
mode: 'advanced',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'attachmentId',
|
||||
title: 'Attachment ID',
|
||||
@@ -576,6 +600,7 @@ Return ONLY the comment text - no explanations.`,
|
||||
'jira_update_comment',
|
||||
'jira_delete_comment',
|
||||
'jira_get_attachments',
|
||||
'jira_add_attachment',
|
||||
'jira_delete_attachment',
|
||||
'jira_add_worklog',
|
||||
'jira_get_worklogs',
|
||||
@@ -623,6 +648,8 @@ Return ONLY the comment text - no explanations.`,
|
||||
return 'jira_delete_comment'
|
||||
case 'get_attachments':
|
||||
return 'jira_get_attachments'
|
||||
case 'add_attachment':
|
||||
return 'jira_add_attachment'
|
||||
case 'delete_attachment':
|
||||
return 'jira_delete_attachment'
|
||||
case 'add_worklog':
|
||||
@@ -838,6 +865,21 @@ Return ONLY the comment text - no explanations.`,
|
||||
issueKey: effectiveIssueKey,
|
||||
}
|
||||
}
|
||||
case 'add_attachment': {
|
||||
if (!effectiveIssueKey) {
|
||||
throw new Error('Issue Key is required to add attachments.')
|
||||
}
|
||||
const fileParam = params.attachmentFiles || params.files
|
||||
if (!fileParam || (Array.isArray(fileParam) && fileParam.length === 0)) {
|
||||
throw new Error('At least one attachment file is required.')
|
||||
}
|
||||
const normalizedFiles = Array.isArray(fileParam) ? fileParam : [fileParam]
|
||||
return {
|
||||
...baseParams,
|
||||
issueKey: effectiveIssueKey,
|
||||
files: normalizedFiles,
|
||||
}
|
||||
}
|
||||
case 'delete_attachment': {
|
||||
return {
|
||||
...baseParams,
|
||||
@@ -982,6 +1024,8 @@ Return ONLY the comment text - no explanations.`,
|
||||
commentBody: { type: 'string', description: 'Text content for comment operations' },
|
||||
commentId: { type: 'string', description: 'Comment ID for update/delete operations' },
|
||||
// Attachment operation inputs
|
||||
attachmentFiles: { type: 'json', description: 'Files to attach (UI upload)' },
|
||||
files: { type: 'array', description: 'Files to attach (UserFile array)' },
|
||||
attachmentId: { type: 'string', description: 'Attachment ID for delete operation' },
|
||||
// Worklog operation inputs
|
||||
timeSpentSeconds: {
|
||||
@@ -1049,9 +1093,11 @@ Return ONLY the comment text - no explanations.`,
|
||||
|
||||
// jira_get_attachments outputs
|
||||
attachments: {
|
||||
type: 'file[]',
|
||||
type: 'json',
|
||||
description: 'Array of attachments with id, filename, size, mimeType, created, author',
|
||||
},
|
||||
files: { type: 'file[]', description: 'Uploaded attachment files' },
|
||||
attachmentIds: { type: 'json', description: 'Uploaded attachment IDs' },
|
||||
|
||||
// jira_delete_attachment, jira_delete_comment, jira_delete_issue, jira_delete_worklog, jira_delete_issue_link outputs
|
||||
attachmentId: { type: 'string', description: 'Deleted attachment ID' },
|
||||
|
||||
@@ -668,17 +668,44 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
|
||||
generationType: 'timestamp',
|
||||
},
|
||||
},
|
||||
// Attachment file
|
||||
{
|
||||
id: 'attachmentFileUpload',
|
||||
title: 'Attachment',
|
||||
type: 'file-upload',
|
||||
canonicalParamId: 'file',
|
||||
placeholder: 'Upload attachment',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['linear_create_attachment'],
|
||||
},
|
||||
mode: 'basic',
|
||||
multiple: false,
|
||||
},
|
||||
{
|
||||
id: 'file',
|
||||
title: 'File Reference',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'file',
|
||||
placeholder: 'File reference from previous block',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['linear_create_attachment'],
|
||||
},
|
||||
mode: 'advanced',
|
||||
},
|
||||
// Attachment URL
|
||||
{
|
||||
id: 'url',
|
||||
title: 'URL',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter URL',
|
||||
required: true,
|
||||
required: false,
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['linear_create_attachment'],
|
||||
},
|
||||
mode: 'advanced',
|
||||
},
|
||||
// Attachment title
|
||||
{
|
||||
@@ -1742,16 +1769,31 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
|
||||
teamId: effectiveTeamId,
|
||||
}
|
||||
|
||||
case 'linear_create_attachment':
|
||||
if (!params.issueId?.trim() || !params.url?.trim()) {
|
||||
throw new Error('Issue ID and URL are required.')
|
||||
case 'linear_create_attachment': {
|
||||
if (!params.issueId?.trim()) {
|
||||
throw new Error('Issue ID is required.')
|
||||
}
|
||||
if (Array.isArray(params.file)) {
|
||||
throw new Error('Attachment file must be a single file.')
|
||||
}
|
||||
if (Array.isArray(params.attachmentFileUpload)) {
|
||||
throw new Error('Attachment file must be a single file.')
|
||||
}
|
||||
const attachmentFile = params.attachmentFileUpload || params.file
|
||||
const attachmentUrl =
|
||||
params.url?.trim() ||
|
||||
(attachmentFile && !Array.isArray(attachmentFile) ? attachmentFile.url : undefined)
|
||||
if (!attachmentUrl) {
|
||||
throw new Error('URL or file is required.')
|
||||
}
|
||||
return {
|
||||
...baseParams,
|
||||
issueId: params.issueId.trim(),
|
||||
url: params.url.trim(),
|
||||
url: attachmentUrl,
|
||||
file: attachmentFile,
|
||||
title: params.attachmentTitle,
|
||||
}
|
||||
}
|
||||
|
||||
case 'linear_list_attachments':
|
||||
if (!params.issueId?.trim()) {
|
||||
@@ -2248,6 +2290,8 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
|
||||
endDate: { type: 'string', description: 'End date' },
|
||||
targetDate: { type: 'string', description: 'Target date' },
|
||||
url: { type: 'string', description: 'URL' },
|
||||
attachmentFileUpload: { type: 'json', description: 'File to attach (UI upload)' },
|
||||
file: { type: 'json', description: 'File to attach (UserFile)' },
|
||||
attachmentTitle: { type: 'string', description: 'Attachment title' },
|
||||
attachmentId: { type: 'string', description: 'Attachment identifier' },
|
||||
relationType: { type: 'string', description: 'Relation type' },
|
||||
@@ -2341,7 +2385,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
|
||||
cycles: { type: 'json', description: 'Cycles list' },
|
||||
// Attachment outputs
|
||||
attachment: { type: 'json', description: 'Attachment data' },
|
||||
attachments: { type: 'file[]', description: 'Attachments list' },
|
||||
attachments: { type: 'json', description: 'Attachments list' },
|
||||
// Relation outputs
|
||||
relation: { type: 'json', description: 'Issue relation data' },
|
||||
relations: { type: 'json', description: 'Issue relations list' },
|
||||
|
||||
@@ -346,7 +346,10 @@ export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
|
||||
// Add files if provided
|
||||
const fileParam = attachmentFiles || files
|
||||
if (fileParam && (operation === 'write_chat' || operation === 'write_channel')) {
|
||||
baseParams.files = fileParam
|
||||
const normalizedFiles = Array.isArray(fileParam) ? fileParam : [fileParam]
|
||||
if (normalizedFiles.length > 0) {
|
||||
baseParams.files = normalizedFiles
|
||||
}
|
||||
}
|
||||
|
||||
// Add messageId if provided
|
||||
@@ -463,6 +466,7 @@ export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
|
||||
totalAttachments: { type: 'number', description: 'Total number of attachments' },
|
||||
attachmentTypes: { type: 'json', description: 'Array of attachment content types' },
|
||||
attachments: { type: 'file[]', description: 'Downloaded message attachments' },
|
||||
files: { type: 'file[]', description: 'Files attached to the message' },
|
||||
updatedContent: {
|
||||
type: 'boolean',
|
||||
description: 'Whether content was successfully updated/sent',
|
||||
|
||||
@@ -803,7 +803,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
|
||||
outputs: {
|
||||
deals: { type: 'json', description: 'Array of deal objects' },
|
||||
deal: { type: 'json', description: 'Single deal object' },
|
||||
files: { type: 'file[]', description: 'Array of file objects' },
|
||||
files: { type: 'json', description: 'Array of file objects' },
|
||||
messages: { type: 'json', description: 'Array of mail message objects' },
|
||||
pipelines: { type: 'json', description: 'Array of pipeline objects' },
|
||||
projects: { type: 'json', description: 'Array of project objects' },
|
||||
|
||||
@@ -293,6 +293,7 @@ export const SftpBlock: BlockConfig<SftpUploadResult> = {
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether the operation was successful' },
|
||||
uploadedFiles: { type: 'json', description: 'Array of uploaded file details' },
|
||||
file: { type: 'file', description: 'Downloaded file stored in execution files' },
|
||||
fileName: { type: 'string', description: 'Downloaded file name' },
|
||||
content: { type: 'string', description: 'Downloaded file content' },
|
||||
size: { type: 'number', description: 'File size in bytes' },
|
||||
|
||||
@@ -522,7 +522,7 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
|
||||
description: 'Array of SharePoint list items with fields',
|
||||
},
|
||||
uploadedFiles: {
|
||||
type: 'file[]',
|
||||
type: 'json',
|
||||
description: 'Array of uploaded file objects with id, name, webUrl, size',
|
||||
},
|
||||
fileCount: {
|
||||
|
||||
@@ -622,7 +622,10 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
}
|
||||
const fileParam = attachmentFiles || files
|
||||
if (fileParam) {
|
||||
baseParams.files = fileParam
|
||||
const normalizedFiles = Array.isArray(fileParam) ? fileParam : [fileParam]
|
||||
if (normalizedFiles.length > 0) {
|
||||
baseParams.files = normalizedFiles
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -796,6 +799,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
type: 'number',
|
||||
description: 'Number of files uploaded (when files are attached)',
|
||||
},
|
||||
files: { type: 'file[]', description: 'Files attached to the message' },
|
||||
|
||||
// slack_canvas outputs
|
||||
canvas_id: { type: 'string', description: 'Canvas identifier for created canvases' },
|
||||
|
||||
@@ -507,6 +507,7 @@ export const SSHBlock: BlockConfig<SSHResponse> = {
|
||||
stderr: { type: 'string', description: 'Command standard error' },
|
||||
exitCode: { type: 'number', description: 'Command exit code' },
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
file: { type: 'file', description: 'Downloaded file stored in execution files' },
|
||||
fileContent: { type: 'string', description: 'Downloaded/read file content' },
|
||||
entries: { type: 'json', description: 'Directory entries' },
|
||||
exists: { type: 'boolean', description: 'File/directory existence' },
|
||||
|
||||
@@ -314,9 +314,14 @@ export const TelegramBlock: BlockConfig<TelegramResponse> = {
|
||||
case 'telegram_send_document': {
|
||||
// Handle file upload
|
||||
const fileParam = params.attachmentFiles || params.files
|
||||
const normalizedFiles = fileParam
|
||||
? Array.isArray(fileParam)
|
||||
? fileParam
|
||||
: [fileParam]
|
||||
: undefined
|
||||
return {
|
||||
...commonParams,
|
||||
files: fileParam,
|
||||
files: normalizedFiles,
|
||||
caption: params.caption,
|
||||
}
|
||||
}
|
||||
@@ -359,6 +364,7 @@ export const TelegramBlock: BlockConfig<TelegramResponse> = {
|
||||
},
|
||||
message: { type: 'string', description: 'Success or error message' },
|
||||
data: { type: 'json', description: 'Response data' },
|
||||
files: { type: 'file[]', description: 'Files attached to the message' },
|
||||
// Specific result fields
|
||||
messageId: { type: 'number', description: 'Sent message ID' },
|
||||
chatId: { type: 'number', description: 'Chat ID where message was sent' },
|
||||
|
||||
19
apps/sim/lib/core/utils/logging.ts
Normal file
19
apps/sim/lib/core/utils/logging.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Sanitize URLs for logging by stripping query/hash and truncating.
|
||||
*/
|
||||
export function sanitizeUrlForLog(url: string, maxLength = 120): string {
|
||||
if (!url) return ''
|
||||
|
||||
const trimmed = url.trim()
|
||||
try {
|
||||
const hasProtocol = /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(trimmed)
|
||||
const parsed = new URL(trimmed, hasProtocol ? undefined : 'http://localhost')
|
||||
const origin = parsed.origin === 'null' ? '' : parsed.origin
|
||||
const sanitized = `${origin}${parsed.pathname}`
|
||||
const result = sanitized || parsed.pathname || trimmed
|
||||
return result.length > maxLength ? `${result.slice(0, maxLength)}...` : result
|
||||
} catch {
|
||||
const withoutQuery = trimmed.split('?')[0].split('#')[0]
|
||||
return withoutQuery.length > maxLength ? `${withoutQuery.slice(0, maxLength)}...` : withoutQuery
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { PDFDocument } from 'pdf-lib'
|
||||
import { getBYOKKey } from '@/lib/api-key/byok'
|
||||
import { type Chunk, JsonYamlChunker, StructuredDataChunker, TextChunker } from '@/lib/chunkers'
|
||||
import { env } from '@/lib/core/config/env'
|
||||
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
|
||||
import { parseBuffer, parseFile } from '@/lib/file-parsers'
|
||||
import type { FileParseMetadata } from '@/lib/file-parsers/types'
|
||||
import { retryWithExponentialBackoff } from '@/lib/knowledge/documents/utils'
|
||||
@@ -489,7 +490,7 @@ async function parseWithMistralOCR(
|
||||
workspaceId
|
||||
)
|
||||
|
||||
logger.info(`Mistral OCR: Using presigned URL for ${filename}: ${httpsUrl.substring(0, 120)}...`)
|
||||
logger.info(`Mistral OCR: Using presigned URL for ${filename}: ${sanitizeUrlForLog(httpsUrl)}`)
|
||||
|
||||
let pageCount = 0
|
||||
if (mimeType === 'application/pdf' && buffer) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
secureFetchWithPinnedIP,
|
||||
validateUrlWithDNS,
|
||||
} from '@/lib/core/security/input-validation'
|
||||
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
|
||||
import type { DbOrTx } from '@/lib/db/types'
|
||||
import { getProviderIdFromServiceId } from '@/lib/oauth'
|
||||
import {
|
||||
@@ -114,7 +115,7 @@ async function fetchWithDNSPinning(
|
||||
const urlValidation = await validateUrlWithDNS(url, 'contentUrl')
|
||||
if (!urlValidation.isValid) {
|
||||
logger.warn(`[${requestId}] Invalid content URL: ${urlValidation.error}`, {
|
||||
url: url.substring(0, 100),
|
||||
url: sanitizeUrlForLog(url),
|
||||
})
|
||||
return null
|
||||
}
|
||||
@@ -133,7 +134,7 @@ async function fetchWithDNSPinning(
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error fetching URL with DNS pinning`, {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
url: url.substring(0, 100),
|
||||
url: sanitizeUrlForLog(url),
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
|
||||
import type { BrowserUseRunTaskParams, BrowserUseRunTaskResponse } from '@/tools/browser_use/types'
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
@@ -183,7 +184,7 @@ async function pollForCompletion(
|
||||
}
|
||||
|
||||
if (!liveUrlLogged && taskData.live_url) {
|
||||
logger.info(`BrowserUse task ${taskId} live URL: ${taskData.live_url}`)
|
||||
logger.info(`BrowserUse task ${taskId} live URL: ${sanitizeUrlForLog(taskData.live_url)}`)
|
||||
liveUrlLogged = true
|
||||
}
|
||||
|
||||
|
||||
@@ -72,6 +72,7 @@ export const discordSendMessageTool: ToolConfig<
|
||||
|
||||
outputs: {
|
||||
message: { type: 'string', description: 'Success or error message' },
|
||||
files: { type: 'file[]', description: 'Files attached to the message' },
|
||||
data: {
|
||||
type: 'object',
|
||||
description: 'Discord message data',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { UserFile } from '@/executor/types'
|
||||
import type { ToolFileData } from '@/tools/types'
|
||||
|
||||
export interface DiscordMessage {
|
||||
id: string
|
||||
@@ -85,6 +86,7 @@ interface BaseDiscordResponse {
|
||||
export interface DiscordSendMessageResponse extends BaseDiscordResponse {
|
||||
output: {
|
||||
message: string
|
||||
files?: ToolFileData[]
|
||||
data?: DiscordMessage
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { UserFile } from '@/executor/types'
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
export interface ElevenLabsTtsParams {
|
||||
@@ -12,11 +13,13 @@ export interface ElevenLabsTtsParams {
|
||||
export interface ElevenLabsTtsResponse extends ToolResponse {
|
||||
output: {
|
||||
audioUrl: string
|
||||
audioFile?: UserFile
|
||||
}
|
||||
}
|
||||
|
||||
export interface ElevenLabsBlockResponse extends ToolResponse {
|
||||
output: {
|
||||
audioUrl: string
|
||||
audioFile?: UserFile
|
||||
}
|
||||
}
|
||||
|
||||
83
apps/sim/tools/jira/add_attachment.ts
Normal file
83
apps/sim/tools/jira/add_attachment.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import type { JiraAddAttachmentParams, JiraAddAttachmentResponse } from '@/tools/jira/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const jiraAddAttachmentTool: ToolConfig<JiraAddAttachmentParams, JiraAddAttachmentResponse> =
|
||||
{
|
||||
id: 'jira_add_attachment',
|
||||
name: 'Jira Add Attachment',
|
||||
description: 'Add attachments to a Jira issue',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'jira',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token for Jira',
|
||||
},
|
||||
domain: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Your Jira domain (e.g., yourcompany.atlassian.net)',
|
||||
},
|
||||
issueKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Jira issue key to add attachments to (e.g., PROJ-123)',
|
||||
},
|
||||
files: {
|
||||
type: 'file[]',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Files to attach to the Jira issue',
|
||||
},
|
||||
cloudId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'hidden',
|
||||
description:
|
||||
'Jira Cloud ID for the instance. If not provided, it will be fetched using the domain.',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/jira/add-attachment',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params: JiraAddAttachmentParams) => ({
|
||||
accessToken: params.accessToken,
|
||||
domain: params.domain,
|
||||
issueKey: params.issueKey,
|
||||
files: params.files,
|
||||
cloudId: params.cloudId,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok || !data.success) {
|
||||
throw new Error(data.error || 'Failed to add Jira attachment')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: data.output,
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
ts: { type: 'string', description: 'Timestamp of the operation' },
|
||||
issueKey: { type: 'string', description: 'Issue key' },
|
||||
attachmentIds: { type: 'json', description: 'IDs of uploaded attachments' },
|
||||
files: { type: 'file[]', description: 'Uploaded attachment files' },
|
||||
},
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { jiraAddAttachmentTool } from '@/tools/jira/add_attachment'
|
||||
import { jiraAddCommentTool } from '@/tools/jira/add_comment'
|
||||
import { jiraAddWatcherTool } from '@/tools/jira/add_watcher'
|
||||
import { jiraAddWorklogTool } from '@/tools/jira/add_worklog'
|
||||
@@ -32,6 +33,7 @@ export {
|
||||
jiraTransitionIssueTool,
|
||||
jiraSearchIssuesTool,
|
||||
jiraAddCommentTool,
|
||||
jiraAddAttachmentTool,
|
||||
jiraGetCommentsTool,
|
||||
jiraUpdateCommentTool,
|
||||
jiraDeleteCommentTool,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
import type { UserFile } from '@/executor/types'
|
||||
import type { ToolFileData, ToolResponse } from '@/tools/types'
|
||||
|
||||
export interface JiraRetrieveParams {
|
||||
accessToken: string
|
||||
@@ -312,6 +313,23 @@ export interface JiraDeleteAttachmentResponse extends ToolResponse {
|
||||
}
|
||||
}
|
||||
|
||||
export interface JiraAddAttachmentParams {
|
||||
accessToken: string
|
||||
domain: string
|
||||
issueKey: string
|
||||
files: UserFile[]
|
||||
cloudId?: string
|
||||
}
|
||||
|
||||
export interface JiraAddAttachmentResponse extends ToolResponse {
|
||||
output: {
|
||||
ts: string
|
||||
issueKey: string
|
||||
attachmentIds: string[]
|
||||
files: ToolFileData[]
|
||||
}
|
||||
}
|
||||
|
||||
// Worklogs
|
||||
export interface JiraAddWorklogParams {
|
||||
accessToken: string
|
||||
@@ -482,6 +500,7 @@ export type JiraResponse =
|
||||
| JiraUpdateCommentResponse
|
||||
| JiraDeleteCommentResponse
|
||||
| JiraGetAttachmentsResponse
|
||||
| JiraAddAttachmentResponse
|
||||
| JiraDeleteAttachmentResponse
|
||||
| JiraAddWorklogResponse
|
||||
| JiraGetWorklogsResponse
|
||||
|
||||
@@ -28,10 +28,16 @@ export const linearCreateAttachmentTool: ToolConfig<
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'URL of the attachment',
|
||||
},
|
||||
file: {
|
||||
type: 'file',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'File to attach',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
@@ -59,9 +65,14 @@ export const linearCreateAttachmentTool: ToolConfig<
|
||||
}
|
||||
},
|
||||
body: (params) => {
|
||||
const attachmentUrl = params.url || params.file?.url
|
||||
if (!attachmentUrl) {
|
||||
throw new Error('URL or file is required')
|
||||
}
|
||||
|
||||
const input: Record<string, any> = {
|
||||
issueId: params.issueId,
|
||||
url: params.url,
|
||||
url: attachmentUrl,
|
||||
title: params.title,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { UserFile } from '@/executor/types'
|
||||
import type { OutputProperty, ToolResponse } from '@/tools/types'
|
||||
|
||||
/**
|
||||
@@ -875,7 +876,8 @@ export interface LinearGetActiveCycleParams {
|
||||
|
||||
export interface LinearCreateAttachmentParams {
|
||||
issueId: string
|
||||
url: string
|
||||
url?: string
|
||||
file?: UserFile
|
||||
title?: string
|
||||
subtitle?: string
|
||||
accessToken?: string
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
|
||||
import type {
|
||||
MicrosoftPlannerReadResponse,
|
||||
MicrosoftPlannerToolParams,
|
||||
@@ -76,7 +77,7 @@ export const readTaskTool: ToolConfig<MicrosoftPlannerToolParams, MicrosoftPlann
|
||||
finalUrl = 'https://graph.microsoft.com/v1.0/me/planner/tasks'
|
||||
}
|
||||
|
||||
logger.info('Microsoft Planner URL:', finalUrl)
|
||||
logger.info('Microsoft Planner URL:', sanitizeUrlForLog(finalUrl))
|
||||
return finalUrl
|
||||
},
|
||||
method: 'GET',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { UserFile } from '@/executor/types'
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
import type { ToolFileData, ToolResponse } from '@/tools/types'
|
||||
|
||||
export interface MicrosoftTeamsAttachment {
|
||||
id: string
|
||||
@@ -61,6 +61,7 @@ export interface MicrosoftTeamsWriteResponse extends ToolResponse {
|
||||
output: {
|
||||
updatedContent: boolean
|
||||
metadata: MicrosoftTeamsMetadata
|
||||
files?: ToolFileData[]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ export const writeChannelTool: ToolConfig<MicrosoftTeamsToolParams, MicrosoftTea
|
||||
createdTime: { type: 'string', description: 'Timestamp when message was created' },
|
||||
url: { type: 'string', description: 'Web URL to the message' },
|
||||
updatedContent: { type: 'boolean', description: 'Whether content was successfully updated' },
|
||||
files: { type: 'file[]', description: 'Files attached to the message' },
|
||||
},
|
||||
|
||||
request: {
|
||||
|
||||
@@ -50,6 +50,7 @@ export const writeChatTool: ToolConfig<MicrosoftTeamsToolParams, MicrosoftTeamsW
|
||||
createdTime: { type: 'string', description: 'Timestamp when message was created' },
|
||||
url: { type: 'string', description: 'Web URL to the message' },
|
||||
updatedContent: { type: 'boolean', description: 'Whether content was successfully updated' },
|
||||
files: { type: 'file[]', description: 'Files attached to the message' },
|
||||
},
|
||||
|
||||
request: {
|
||||
|
||||
@@ -98,10 +98,10 @@ export const mistralParserTool: ToolConfig<MistralParserInput, MistralParserOutp
|
||||
if (
|
||||
typeof params.fileUpload === 'object' &&
|
||||
params.fileUpload !== null &&
|
||||
(params.fileUpload.url || params.fileUpload.path)
|
||||
params.fileUpload.url
|
||||
) {
|
||||
// Get the full URL to the file - prefer url over path for UserFile compatibility
|
||||
let uploadedFilePath = params.fileUpload.url || params.fileUpload.path
|
||||
// Get the full URL to the file
|
||||
let uploadedFilePath = params.fileUpload.url
|
||||
|
||||
// Make sure the file path is an absolute URL
|
||||
if (uploadedFilePath.startsWith('/')) {
|
||||
@@ -184,9 +184,9 @@ export const mistralParserTool: ToolConfig<MistralParserInput, MistralParserOutp
|
||||
}
|
||||
|
||||
// Check if this is an internal workspace file path
|
||||
if (params.fileUpload?.path?.startsWith('/api/files/serve/')) {
|
||||
if (params.fileUpload?.url?.startsWith('/api/files/serve/')) {
|
||||
// Update filePath to the internal path for workspace files
|
||||
requestBody.filePath = params.fileUpload.path
|
||||
requestBody.filePath = params.fileUpload.url
|
||||
}
|
||||
|
||||
// Add optional parameters with proper validation
|
||||
|
||||
@@ -763,6 +763,7 @@ import {
|
||||
} from '@/tools/intercom'
|
||||
import { jinaReadUrlTool, jinaSearchTool } from '@/tools/jina'
|
||||
import {
|
||||
jiraAddAttachmentTool,
|
||||
jiraAddCommentTool,
|
||||
jiraAddWatcherTool,
|
||||
jiraAddWorklogTool,
|
||||
@@ -1879,6 +1880,7 @@ export const tools: Record<string, ToolConfig> = {
|
||||
jira_update_comment: jiraUpdateCommentTool,
|
||||
jira_delete_comment: jiraDeleteCommentTool,
|
||||
jira_get_attachments: jiraGetAttachmentsTool,
|
||||
jira_add_attachment: jiraAddAttachmentTool,
|
||||
jira_delete_attachment: jiraDeleteAttachmentTool,
|
||||
jira_add_worklog: jiraAddWorklogTool,
|
||||
jira_get_worklogs: jiraGetWorklogsTool,
|
||||
|
||||
@@ -94,6 +94,7 @@ export const sftpDownloadTool: ToolConfig<SftpDownloadParams, SftpDownloadResult
|
||||
output: {
|
||||
success: true,
|
||||
fileName: data.fileName,
|
||||
file: data.file,
|
||||
content: data.content,
|
||||
size: data.size,
|
||||
encoding: data.encoding,
|
||||
@@ -104,6 +105,7 @@ export const sftpDownloadTool: ToolConfig<SftpDownloadParams, SftpDownloadResult
|
||||
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Whether the download was successful' },
|
||||
file: { type: 'file', description: 'Downloaded file stored in execution files' },
|
||||
fileName: { type: 'string', description: 'Name of the downloaded file' },
|
||||
content: { type: 'string', description: 'File content (text or base64 encoded)' },
|
||||
size: { type: 'number', description: 'File size in bytes' },
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { UserFile } from '@/executor/types'
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
import type { ToolFileData, ToolResponse } from '@/tools/types'
|
||||
|
||||
export interface SftpConnectionConfig {
|
||||
host: string
|
||||
@@ -42,6 +42,7 @@ export interface SftpDownloadResult extends ToolResponse {
|
||||
output: {
|
||||
success: boolean
|
||||
fileName?: string
|
||||
file?: ToolFileData
|
||||
content?: string
|
||||
size?: number
|
||||
encoding?: string
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
|
||||
import type {
|
||||
SharepointGetListResponse,
|
||||
SharepointList,
|
||||
@@ -56,7 +57,10 @@ export const getListTool: ToolConfig<SharepointToolParams, SharepointGetListResp
|
||||
const baseUrl = `https://graph.microsoft.com/v1.0/sites/${siteId}/lists`
|
||||
const url = new URL(baseUrl)
|
||||
const finalUrl = url.toString()
|
||||
logger.info('SharePoint List All Lists URL', { finalUrl, siteId })
|
||||
logger.info('SharePoint List All Lists URL', {
|
||||
finalUrl: sanitizeUrlForLog(finalUrl),
|
||||
siteId,
|
||||
})
|
||||
return finalUrl
|
||||
}
|
||||
|
||||
@@ -72,7 +76,7 @@ export const getListTool: ToolConfig<SharepointToolParams, SharepointGetListResp
|
||||
itemsUrl.searchParams.set('$expand', 'fields')
|
||||
const finalItemsUrl = itemsUrl.toString()
|
||||
logger.info('SharePoint Get List Items URL', {
|
||||
finalUrl: finalItemsUrl,
|
||||
finalUrl: sanitizeUrlForLog(finalItemsUrl),
|
||||
siteId,
|
||||
listId: params.listId,
|
||||
})
|
||||
@@ -89,7 +93,7 @@ export const getListTool: ToolConfig<SharepointToolParams, SharepointGetListResp
|
||||
|
||||
const finalUrl = url.toString()
|
||||
logger.info('SharePoint Get List URL', {
|
||||
finalUrl,
|
||||
finalUrl: sanitizeUrlForLog(finalUrl),
|
||||
siteId,
|
||||
listId: params.listId,
|
||||
includeColumns: !!params.includeColumns,
|
||||
|
||||
@@ -114,5 +114,6 @@ export const slackMessageTool: ToolConfig<SlackMessageParams, SlackMessageRespon
|
||||
type: 'number',
|
||||
description: 'Number of files uploaded (when files are attached)',
|
||||
},
|
||||
files: { type: 'file[]', description: 'Files attached to the message' },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { UserFile } from '@/executor/types'
|
||||
import type { OutputProperty, ToolResponse } from '@/tools/types'
|
||||
import type { OutputProperty, ToolFileData, ToolResponse } from '@/tools/types'
|
||||
|
||||
/**
|
||||
* Shared output property definitions for Slack API responses.
|
||||
@@ -596,6 +596,7 @@ export interface SlackMessageResponse extends ToolResponse {
|
||||
ts: string
|
||||
channel: string
|
||||
fileCount?: number
|
||||
files?: ToolFileData[]
|
||||
// New comprehensive message object
|
||||
message: SlackMessage
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@ export const downloadFileTool: ToolConfig<SSHDownloadFileParams, SSHResponse> =
|
||||
success: true,
|
||||
output: {
|
||||
downloaded: true,
|
||||
file: data.file,
|
||||
fileContent: data.content,
|
||||
fileName: data.fileName,
|
||||
remotePath: data.remotePath,
|
||||
@@ -91,6 +92,7 @@ export const downloadFileTool: ToolConfig<SSHDownloadFileParams, SSHResponse> =
|
||||
|
||||
outputs: {
|
||||
downloaded: { type: 'boolean', description: 'Whether the file was downloaded successfully' },
|
||||
file: { type: 'file', description: 'Downloaded file stored in execution files' },
|
||||
fileContent: { type: 'string', description: 'File content (base64 encoded for binary files)' },
|
||||
fileName: { type: 'string', description: 'Name of the downloaded file' },
|
||||
remotePath: { type: 'string', description: 'Source path on the remote server' },
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
import type { ToolFileData, ToolResponse } from '@/tools/types'
|
||||
|
||||
// Base SSH connection configuration
|
||||
export interface SSHConnectionConfig {
|
||||
@@ -149,6 +149,7 @@ export interface SSHResponse extends ToolResponse {
|
||||
|
||||
uploaded?: boolean
|
||||
downloaded?: boolean
|
||||
file?: ToolFileData
|
||||
fileContent?: string
|
||||
fileName?: string
|
||||
remotePath?: string
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
|
||||
import type { StagehandAgentParams, StagehandAgentResponse } from '@/tools/stagehand/types'
|
||||
import { STAGEHAND_AGENT_RESULT_OUTPUT_PROPERTIES } from '@/tools/stagehand/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
@@ -61,7 +62,9 @@ export const agentTool: ToolConfig<StagehandAgentParams, StagehandAgentResponse>
|
||||
let startUrl = params.startUrl
|
||||
if (startUrl && !startUrl.match(/^https?:\/\//i)) {
|
||||
startUrl = `https://${startUrl.trim()}`
|
||||
logger.info(`Normalized URL from ${params.startUrl} to ${startUrl}`)
|
||||
logger.info(
|
||||
`Normalized URL from ${sanitizeUrlForLog(params.startUrl)} to ${sanitizeUrlForLog(startUrl)}`
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { UserFile } from '@/executor/types'
|
||||
import type { OutputProperty, ToolResponse } from '@/tools/types'
|
||||
|
||||
/**
|
||||
@@ -441,7 +442,7 @@ export interface SupabaseStorageUploadParams {
|
||||
bucket: string
|
||||
fileName: string
|
||||
path?: string
|
||||
fileData: any // UserFile object (basic mode) or string (advanced mode: base64/plain text)
|
||||
fileData: UserFile | string
|
||||
contentType?: string
|
||||
upsert?: boolean
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ export const telegramSendDocumentTool: ToolConfig<
|
||||
|
||||
outputs: {
|
||||
message: { type: 'string', description: 'Success or error message' },
|
||||
files: { type: 'file[]', description: 'Files attached to the message' },
|
||||
data: {
|
||||
type: 'object',
|
||||
description: 'Telegram message data including document',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { UserFile } from '@/executor/types'
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
import type { ToolFileData, ToolResponse } from '@/tools/types'
|
||||
|
||||
export interface TelegramMessage {
|
||||
message_id: number
|
||||
@@ -167,6 +167,7 @@ export interface TelegramSendDocumentResponse extends ToolResponse {
|
||||
output: {
|
||||
message: string
|
||||
data?: TelegramMedia
|
||||
files?: ToolFileData[]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Common types for WordPress REST API tools
|
||||
import type { UserFile } from '@/executor/types'
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
// Common parameters for all WordPress tools (WordPress.com OAuth)
|
||||
@@ -254,7 +255,7 @@ export interface WordPressListPagesResponse extends ToolResponse {
|
||||
|
||||
// Upload Media
|
||||
export interface WordPressUploadMediaParams extends WordPressBaseParams {
|
||||
file: any // UserFile object from file upload
|
||||
file: UserFile
|
||||
filename?: string // Optional filename override
|
||||
title?: string
|
||||
caption?: string
|
||||
|
||||
@@ -98,7 +98,7 @@ export const microsoftTeamsWebhookTrigger: TriggerConfig = {
|
||||
},
|
||||
message: {
|
||||
raw: {
|
||||
attachments: { type: 'file[]', description: 'Array of attachments' },
|
||||
attachments: { type: 'json', description: 'Array of attachments' },
|
||||
channelData: {
|
||||
team: { id: { type: 'string', description: 'Team ID' } },
|
||||
tenant: { id: { type: 'string', description: 'Tenant ID' } },
|
||||
|
||||
Reference in New Issue
Block a user