mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
improvement(tools): added file selector for confluence, wip. removed unneccessary restricted google scopes
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const { domain, accessToken, title, limit = 50 } = await request.json()
|
||||
|
||||
if (!domain) {
|
||||
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Build the URL with query parameters
|
||||
const baseUrl = `https://${domain}/wiki/api/v2/pages`
|
||||
const queryParams = new URLSearchParams()
|
||||
|
||||
if (limit) {
|
||||
queryParams.append('limit', limit.toString())
|
||||
}
|
||||
|
||||
if (title) {
|
||||
queryParams.append('title', title)
|
||||
}
|
||||
|
||||
const queryString = queryParams.toString()
|
||||
const url = queryString ? `${baseUrl}?${queryString}` : baseUrl
|
||||
|
||||
console.log(`Fetching Confluence pages from: ${url}`)
|
||||
|
||||
// Make the request to Confluence API
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(`Confluence API error: ${response.status} ${response.statusText}`)
|
||||
let errorMessage
|
||||
|
||||
try {
|
||||
const errorData = await response.json()
|
||||
errorMessage = errorData.message || `Failed to fetch Confluence pages (${response.status})`
|
||||
console.error('Error details:', errorData)
|
||||
} catch (e) {
|
||||
errorMessage = `Failed to fetch Confluence pages: ${response.status} ${response.statusText}`
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: errorMessage }, { status: response.status })
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
console.log('Confluence API response:', JSON.stringify(data, null, 2).substring(0, 300) + '...')
|
||||
console.log(`Found ${data.results?.length || 0} pages`)
|
||||
|
||||
if (data.results && data.results.length > 0) {
|
||||
console.log('First few pages:')
|
||||
data.results.slice(0, 3).forEach((page: any) => {
|
||||
console.log(`- ${page.id}: ${page.title}`)
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
files: data.results.map((page: any) => ({
|
||||
id: page.id,
|
||||
name: page.title,
|
||||
mimeType: 'confluence/page',
|
||||
url: page._links?.webui || '',
|
||||
modifiedTime: page.version?.createdAt || '',
|
||||
spaceId: page.spaceId,
|
||||
webViewLink: page._links?.webui || '',
|
||||
})),
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching Confluence pages:', error)
|
||||
return NextResponse.json(
|
||||
{ error: (error as Error).message || 'Internal server error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
+6
-2
@@ -36,14 +36,18 @@ export interface OAuthRequiredModalProps {
|
||||
const SCOPE_DESCRIPTIONS: Record<string, string> = {
|
||||
'https://www.googleapis.com/auth/gmail.send': 'Send emails on your behalf',
|
||||
// 'https://www.googleapis.com/auth/gmail.readonly': 'View and read your email messages',
|
||||
'https://www.googleapis.com/auth/drive': 'View and manage your Google Drive files',
|
||||
// 'https://www.googleapis.com/auth/drive': 'View and manage your Google Drive files',
|
||||
'https://www.googleapis.com/auth/drive.file': 'View and manage your Google Drive files',
|
||||
'https://www.googleapis.com/auth/documents': 'View and manage your Google Docs',
|
||||
// 'https://www.googleapis.com/auth/documents': 'View and manage your Google Docs',
|
||||
'https://www.googleapis.com/auth/calendar': 'View and manage your calendar',
|
||||
'https://www.googleapis.com/auth/userinfo.email': 'View your email address',
|
||||
'https://www.googleapis.com/auth/userinfo.profile': 'View your basic profile info',
|
||||
'https://www.googleapis.com/auth/spreadsheets': 'View and manage your Google Sheets',
|
||||
'read:confluence-content.all': 'Read Confluence content',
|
||||
'read:page:confluence': 'Read Confluence pages',
|
||||
'write:confluence-content': 'Write Confluence content',
|
||||
'read:me': 'Read your profile information',
|
||||
offline_access: 'Access your account when you are not using the application',
|
||||
repo: 'Access your repositories',
|
||||
workflow: 'Manage repository workflows',
|
||||
'user:email': 'Access your email address',
|
||||
|
||||
+582
@@ -0,0 +1,582 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Check, ChevronDown, ExternalLink, RefreshCw, Search, X } from 'lucide-react'
|
||||
import { ConfluenceIcon } from '@/components/icons'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from '@/components/ui/command'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import {
|
||||
Credential,
|
||||
getProviderIdFromServiceId,
|
||||
getServiceIdFromScopes,
|
||||
OAuthProvider,
|
||||
} from '@/lib/oauth'
|
||||
import { saveToStorage } from '@/stores/workflows/persistence'
|
||||
import { OAuthRequiredModal } from '../../credential-selector/components/oauth-required-modal'
|
||||
|
||||
export interface ConfluenceFileInfo {
|
||||
id: string
|
||||
name: string
|
||||
mimeType: string
|
||||
webViewLink?: string
|
||||
modifiedTime?: string
|
||||
spaceId?: string
|
||||
url?: string
|
||||
}
|
||||
|
||||
interface ConfluenceFileSelectorProps {
|
||||
value: string
|
||||
onChange: (value: string, fileInfo?: ConfluenceFileInfo) => void
|
||||
provider: OAuthProvider
|
||||
requiredScopes?: string[]
|
||||
label?: string
|
||||
disabled?: boolean
|
||||
serviceId?: string
|
||||
domain: string
|
||||
showPreview?: boolean
|
||||
onFileInfoChange?: (fileInfo: ConfluenceFileInfo | null) => void
|
||||
}
|
||||
|
||||
export function ConfluenceFileSelector({
|
||||
value,
|
||||
onChange,
|
||||
provider,
|
||||
requiredScopes = [],
|
||||
label = 'Select Confluence page',
|
||||
disabled = false,
|
||||
serviceId,
|
||||
domain,
|
||||
showPreview = true,
|
||||
onFileInfoChange,
|
||||
}: ConfluenceFileSelectorProps) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [credentials, setCredentials] = useState<Credential[]>([])
|
||||
const [files, setFiles] = useState<ConfluenceFileInfo[]>([])
|
||||
const [selectedCredentialId, setSelectedCredentialId] = useState<string>('')
|
||||
const [selectedFileId, setSelectedFileId] = useState(value)
|
||||
const [selectedFile, setSelectedFile] = useState<ConfluenceFileInfo | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [showOAuthModal, setShowOAuthModal] = useState(false)
|
||||
const initialFetchRef = useRef(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
// Handle search with debounce
|
||||
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||
|
||||
const handleSearch = (value: string) => {
|
||||
// Clear any existing timeout
|
||||
if (searchTimeoutRef.current) {
|
||||
clearTimeout(searchTimeoutRef.current)
|
||||
}
|
||||
|
||||
// Set a new timeout
|
||||
searchTimeoutRef.current = setTimeout(() => {
|
||||
if (value.length > 2) {
|
||||
fetchFiles(value)
|
||||
} else if (value.length === 0) {
|
||||
fetchFiles()
|
||||
}
|
||||
}, 500) // 500ms debounce
|
||||
}
|
||||
|
||||
// Clean up the timeout on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (searchTimeoutRef.current) {
|
||||
clearTimeout(searchTimeoutRef.current)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Determine the appropriate service ID based on provider and scopes
|
||||
const getServiceId = (): string => {
|
||||
if (serviceId) return serviceId
|
||||
return getServiceIdFromScopes(provider, requiredScopes)
|
||||
}
|
||||
|
||||
// Determine the appropriate provider ID based on service and scopes
|
||||
const getProviderId = (): string => {
|
||||
const effectiveServiceId = getServiceId()
|
||||
return getProviderIdFromServiceId(effectiveServiceId)
|
||||
}
|
||||
|
||||
// Fetch available credentials for this provider
|
||||
const fetchCredentials = useCallback(async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const providerId = getProviderId()
|
||||
const response = await fetch(`/api/auth/oauth/credentials?provider=${providerId}`)
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setCredentials(data.credentials)
|
||||
|
||||
// Auto-select logic for credentials
|
||||
if (data.credentials.length > 0) {
|
||||
// If we already have a selected credential ID, check if it's valid
|
||||
if (
|
||||
selectedCredentialId &&
|
||||
data.credentials.some((cred: Credential) => cred.id === selectedCredentialId)
|
||||
) {
|
||||
// Keep the current selection
|
||||
} else {
|
||||
// Otherwise, select the default or first credential
|
||||
const defaultCred = data.credentials.find((cred: Credential) => cred.isDefault)
|
||||
if (defaultCred) {
|
||||
setSelectedCredentialId(defaultCred.id)
|
||||
} else if (data.credentials.length === 1) {
|
||||
setSelectedCredentialId(data.credentials[0].id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching credentials:', error)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [provider, getProviderId, selectedCredentialId])
|
||||
|
||||
// Fetch page info when we have a selected file ID
|
||||
const fetchPageInfo = useCallback(
|
||||
async (pageId: string) => {
|
||||
if (!selectedCredentialId || !domain) return
|
||||
|
||||
// Validate domain format
|
||||
const trimmedDomain = domain.trim().toLowerCase()
|
||||
if (!trimmedDomain.includes('.')) {
|
||||
setError(
|
||||
'Invalid domain format. Please provide the full domain (e.g., your-site.atlassian.net)'
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
// Get the access token from the selected credential
|
||||
const tokenResponse = await fetch('/api/auth/oauth/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
credentialId: selectedCredentialId,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!tokenResponse.ok) {
|
||||
const errorData = await tokenResponse.json()
|
||||
throw new Error(errorData.error || 'Failed to get access token')
|
||||
}
|
||||
|
||||
const tokenData = await tokenResponse.json()
|
||||
const accessToken = tokenData.accessToken
|
||||
|
||||
// Use the access token to fetch the page info
|
||||
const response = await fetch(`/api/auth/oauth/confluence/page`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
domain,
|
||||
accessToken,
|
||||
pageId,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(errorData.error || 'Failed to fetch page info')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
if (data.file) {
|
||||
setSelectedFile(data.file)
|
||||
onFileInfoChange?.(data.file)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching page info:', error)
|
||||
setError((error as Error).message)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
},
|
||||
[selectedCredentialId, domain, onFileInfoChange]
|
||||
)
|
||||
|
||||
// Fetch pages from Confluence
|
||||
const fetchFiles = useCallback(
|
||||
async (searchQuery?: string) => {
|
||||
if (!selectedCredentialId || !domain) return
|
||||
|
||||
// Validate domain format
|
||||
const trimmedDomain = domain.trim().toLowerCase()
|
||||
if (!trimmedDomain.includes('.')) {
|
||||
setError(
|
||||
'Invalid domain format. Please provide the full domain (e.g., your-site.atlassian.net)'
|
||||
)
|
||||
setFiles([])
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
// Get the access token from the selected credential
|
||||
const tokenResponse = await fetch('/api/auth/oauth/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
credentialId: selectedCredentialId,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!tokenResponse.ok) {
|
||||
const errorData = await tokenResponse.json()
|
||||
console.error('Access token error:', errorData)
|
||||
|
||||
// If there's a token error, we might need to reconnect the account
|
||||
setError('Authentication failed. Please reconnect your Confluence account.')
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
const tokenData = await tokenResponse.json()
|
||||
const accessToken = tokenData.accessToken
|
||||
|
||||
if (!accessToken) {
|
||||
console.error('No access token returned')
|
||||
setError('Authentication failed. Please reconnect your Confluence account.')
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
// Simply fetch pages directly using the endpoint
|
||||
const response = await fetch('/api/auth/oauth/confluence/pages', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
domain,
|
||||
accessToken,
|
||||
title: searchQuery || undefined,
|
||||
limit: 50,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
console.error('Confluence API error:', errorData)
|
||||
throw new Error(errorData.error || 'Failed to fetch pages')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
console.log(`Received ${data.files?.length || 0} files from API`)
|
||||
setFiles(data.files || [])
|
||||
|
||||
// If we have a selected file ID, find the file info
|
||||
if (selectedFileId) {
|
||||
const fileInfo = data.files.find((file: ConfluenceFileInfo) => file.id === selectedFileId)
|
||||
if (fileInfo) {
|
||||
setSelectedFile(fileInfo)
|
||||
onFileInfoChange?.(fileInfo)
|
||||
} else if (!searchQuery && selectedFileId) {
|
||||
// If we can't find the file in the list, try to fetch it directly
|
||||
fetchPageInfo(selectedFileId)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching pages:', error)
|
||||
setError((error as Error).message)
|
||||
setFiles([])
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
},
|
||||
[selectedCredentialId, domain, selectedFileId, onFileInfoChange, fetchPageInfo]
|
||||
)
|
||||
|
||||
// Fetch credentials on initial mount
|
||||
useEffect(() => {
|
||||
if (!initialFetchRef.current) {
|
||||
fetchCredentials()
|
||||
initialFetchRef.current = true
|
||||
}
|
||||
}, [fetchCredentials])
|
||||
|
||||
// Only fetch files when the dropdown is opened, not on credential selection
|
||||
const handleOpenChange = (isOpen: boolean) => {
|
||||
setOpen(isOpen)
|
||||
|
||||
// Only fetch files when opening the dropdown and if we have valid credentials and domain
|
||||
if (isOpen && selectedCredentialId && domain && domain.includes('.')) {
|
||||
fetchFiles()
|
||||
}
|
||||
}
|
||||
|
||||
// Update selected file when value changes externally
|
||||
useEffect(() => {
|
||||
if (value !== selectedFileId) {
|
||||
setSelectedFileId(value)
|
||||
|
||||
// Find file info if we have files loaded
|
||||
if (files.length > 0) {
|
||||
const fileInfo = files.find((file) => file.id === value) || null
|
||||
setSelectedFile(fileInfo)
|
||||
onFileInfoChange?.(fileInfo)
|
||||
} else if (value && !selectedFile && selectedCredentialId && domain && domain.includes('.')) {
|
||||
// If we don't have files loaded yet but have a value, try to fetch the file info
|
||||
// Only make the API call if we have everything we need and a proper domain
|
||||
fetchPageInfo(value)
|
||||
}
|
||||
}
|
||||
}, [value, files, selectedFile, selectedCredentialId, domain, onFileInfoChange, fetchPageInfo])
|
||||
|
||||
// Handle file selection
|
||||
const handleSelectFile = (file: ConfluenceFileInfo) => {
|
||||
setSelectedFileId(file.id)
|
||||
setSelectedFile(file)
|
||||
onChange(file.id, file)
|
||||
onFileInfoChange?.(file)
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
// Handle adding a new credential
|
||||
const handleAddCredential = () => {
|
||||
const effectiveServiceId = getServiceId()
|
||||
const providerId = getProviderId()
|
||||
|
||||
// Store information about the required connection
|
||||
saveToStorage<string>('pending_service_id', effectiveServiceId)
|
||||
saveToStorage<string[]>('pending_oauth_scopes', requiredScopes)
|
||||
saveToStorage<string>('pending_oauth_return_url', window.location.href)
|
||||
saveToStorage<string>('pending_oauth_provider_id', providerId)
|
||||
|
||||
// Show the OAuth modal
|
||||
setShowOAuthModal(true)
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
// Clear selection
|
||||
const handleClearSelection = () => {
|
||||
setSelectedFileId('')
|
||||
setSelectedFile(null)
|
||||
onChange('', undefined)
|
||||
onFileInfoChange?.(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Popover open={open} onOpenChange={handleOpenChange}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className="w-full justify-between"
|
||||
disabled={disabled || !domain}
|
||||
>
|
||||
{selectedFile ? (
|
||||
<div className="flex items-center gap-2 overflow-hidden">
|
||||
<ConfluenceIcon className="h-4 w-4" />
|
||||
<span className="font-normal truncate">{selectedFile.name}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<ConfluenceIcon className="h-4 w-4" />
|
||||
<span className="text-muted-foreground">{label}</span>
|
||||
</div>
|
||||
)}
|
||||
<ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0 w-[300px]" align="start">
|
||||
{/* Current account indicator */}
|
||||
{selectedCredentialId && credentials.length > 0 && (
|
||||
<div className="px-3 py-2 border-b flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<ConfluenceIcon className="h-4 w-4" />
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{credentials.find((cred) => cred.id === selectedCredentialId)?.name ||
|
||||
'Unknown'}
|
||||
</span>
|
||||
</div>
|
||||
{credentials.length > 1 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 px-2 text-xs"
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
Switch
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Command>
|
||||
<CommandInput placeholder="Search pages..." onValueChange={handleSearch} />
|
||||
<CommandList>
|
||||
<CommandEmpty>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center p-4">
|
||||
<RefreshCw className="h-4 w-4 animate-spin" />
|
||||
<span className="ml-2">Loading pages...</span>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="p-4 text-center">
|
||||
<p className="text-sm text-destructive">{error}</p>
|
||||
</div>
|
||||
) : credentials.length === 0 ? (
|
||||
<div className="p-4 text-center">
|
||||
<p className="text-sm font-medium">No accounts connected.</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Connect a Confluence account to continue.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-4 text-center">
|
||||
<p className="text-sm font-medium">No pages found.</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Try a different search or account.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CommandEmpty>
|
||||
|
||||
{/* Account selection - only show if we have multiple accounts */}
|
||||
{credentials.length > 1 && (
|
||||
<CommandGroup>
|
||||
<div className="px-2 py-1.5 text-xs font-medium text-muted-foreground">
|
||||
Switch Account
|
||||
</div>
|
||||
{credentials.map((cred) => (
|
||||
<CommandItem
|
||||
key={cred.id}
|
||||
value={`account-${cred.id}`}
|
||||
onSelect={() => setSelectedCredentialId(cred.id)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<ConfluenceIcon className="h-4 w-4" />
|
||||
<span className="font-normal">{cred.name}</span>
|
||||
</div>
|
||||
{cred.id === selectedCredentialId && <Check className="ml-auto h-4 w-4" />}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
)}
|
||||
|
||||
{/* Files list */}
|
||||
{files.length > 0 && (
|
||||
<CommandGroup>
|
||||
<div className="px-2 py-1.5 text-xs font-medium text-muted-foreground">
|
||||
Pages
|
||||
</div>
|
||||
{files.map((file) => (
|
||||
<CommandItem
|
||||
key={file.id}
|
||||
value={`file-${file.id}-${file.name}`}
|
||||
onSelect={() => handleSelectFile(file)}
|
||||
>
|
||||
<div className="flex items-center gap-2 overflow-hidden">
|
||||
<ConfluenceIcon className="h-4 w-4" />
|
||||
<span className="font-normal truncate">{file.name}</span>
|
||||
</div>
|
||||
{file.id === selectedFileId && <Check className="ml-auto h-4 w-4" />}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
)}
|
||||
|
||||
{/* Connect account option - only show if no credentials */}
|
||||
{credentials.length === 0 && (
|
||||
<CommandGroup>
|
||||
<CommandItem onSelect={handleAddCredential}>
|
||||
<div className="flex items-center gap-2 text-primary">
|
||||
<ConfluenceIcon className="h-4 w-4" />
|
||||
<span>Connect Confluence account</span>
|
||||
</div>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
)}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
{/* File preview */}
|
||||
{showPreview && selectedFile && (
|
||||
<div className="mt-2 rounded-md border border-muted bg-muted/10 p-2 relative">
|
||||
<div className="absolute top-2 right-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-5 w-5 hover:bg-muted"
|
||||
onClick={handleClearSelection}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 pr-4">
|
||||
<div className="flex-shrink-0 flex items-center justify-center h-6 w-6 bg-muted/20 rounded">
|
||||
<ConfluenceIcon className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="overflow-hidden flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="text-xs font-medium truncate">{selectedFile.name}</h4>
|
||||
{selectedFile.modifiedTime && (
|
||||
<span className="text-xs text-muted-foreground whitespace-nowrap">
|
||||
{new Date(selectedFile.modifiedTime).toLocaleDateString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{selectedFile.webViewLink ? (
|
||||
<a
|
||||
href={selectedFile.webViewLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs text-primary hover:underline flex items-center gap-1"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<span>Open in Confluence</span>
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
</a>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showOAuthModal && (
|
||||
<OAuthRequiredModal
|
||||
isOpen={showOAuthModal}
|
||||
onClose={() => setShowOAuthModal(false)}
|
||||
provider={provider}
|
||||
toolName="Confluence"
|
||||
requiredScopes={requiredScopes}
|
||||
serviceId={getServiceId()}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
+32
-3
@@ -3,6 +3,7 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import { SubBlockConfig } from '@/blocks/types'
|
||||
import { ConfluenceFileInfo, ConfluenceFileSelector } from './components/confluence-file-selector'
|
||||
import { FileInfo, GoogleDrivePicker } from './components/google-drive-picker'
|
||||
|
||||
interface FileSelectorInputProps {
|
||||
@@ -14,7 +15,15 @@ interface FileSelectorInputProps {
|
||||
export function FileSelectorInput({ blockId, subBlock, disabled = false }: FileSelectorInputProps) {
|
||||
const { getValue, setValue } = useSubBlockStore()
|
||||
const [selectedFileId, setSelectedFileId] = useState<string>('')
|
||||
const [fileInfo, setFileInfo] = useState<FileInfo | null>(null)
|
||||
const [fileInfo, setFileInfo] = useState<FileInfo | ConfluenceFileInfo | null>(null)
|
||||
|
||||
// Get provider-specific values
|
||||
const provider = subBlock.provider || 'google-drive'
|
||||
const isConfluence = provider === 'confluence'
|
||||
|
||||
// For Confluence, we need the domain and credentials
|
||||
const domain = isConfluence ? (getValue(blockId, 'domain') as string) || '' : ''
|
||||
const credentials = isConfluence ? (getValue(blockId, 'credential') as string) || '' : ''
|
||||
|
||||
// Get the current value from the store
|
||||
useEffect(() => {
|
||||
@@ -25,20 +34,40 @@ export function FileSelectorInput({ blockId, subBlock, disabled = false }: FileS
|
||||
}, [blockId, subBlock.id, getValue])
|
||||
|
||||
// Handle file selection
|
||||
const handleFileChange = (fileId: string, info?: FileInfo) => {
|
||||
const handleFileChange = (fileId: string, info?: any) => {
|
||||
setSelectedFileId(fileId)
|
||||
setFileInfo(info || null)
|
||||
setValue(blockId, subBlock.id, fileId)
|
||||
}
|
||||
|
||||
// For Google Drive
|
||||
const clientId = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || ''
|
||||
const apiKey = process.env.NEXT_PUBLIC_GOOGLE_API_KEY || ''
|
||||
|
||||
// Render the appropriate picker based on provider
|
||||
if (isConfluence) {
|
||||
return (
|
||||
<ConfluenceFileSelector
|
||||
value={selectedFileId}
|
||||
onChange={handleFileChange}
|
||||
domain={domain}
|
||||
provider="confluence"
|
||||
requiredScopes={subBlock.requiredScopes || []}
|
||||
serviceId={subBlock.serviceId}
|
||||
label={subBlock.placeholder || 'Select Confluence page'}
|
||||
disabled={disabled}
|
||||
showPreview={true}
|
||||
onFileInfoChange={setFileInfo as (info: ConfluenceFileInfo | null) => void}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// Default to Google Drive picker
|
||||
return (
|
||||
<GoogleDrivePicker
|
||||
value={selectedFileId}
|
||||
onChange={handleFileChange}
|
||||
provider={subBlock.provider || 'google-drive'}
|
||||
provider={provider}
|
||||
requiredScopes={subBlock.requiredScopes || []}
|
||||
label={subBlock.placeholder || 'Select file'}
|
||||
disabled={disabled}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
import { getBlock } from '@/blocks/index'
|
||||
import { SubBlockConfig } from '@/blocks/types'
|
||||
@@ -33,6 +34,8 @@ export function SubBlock({ blockId, config, isConnecting }: SubBlockProps) {
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
const { getValue } = useSubBlockStore()
|
||||
|
||||
const isFieldRequired = () => {
|
||||
const blockType = useWorkflowStore.getState().blocks[blockId]?.type
|
||||
if (!blockType) return false
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { ConfluenceIcon } from '@/components/icons'
|
||||
import { ConfluenceRetrieveResponse } from '@/tools/confluence/types'
|
||||
import { ConfluenceRetrieveResponse, ConfluenceUpdateResponse } from '@/tools/confluence/types'
|
||||
import { BlockConfig } from '../types'
|
||||
|
||||
export const ConfluenceBlock: BlockConfig<ConfluenceRetrieveResponse> = {
|
||||
type ConfluenceResponse = ConfluenceRetrieveResponse | ConfluenceUpdateResponse
|
||||
|
||||
export const ConfluenceBlock: BlockConfig<ConfluenceResponse> = {
|
||||
type: 'confluence',
|
||||
name: 'Confluence',
|
||||
description: 'Interact with Confluence',
|
||||
@@ -12,12 +14,23 @@ export const ConfluenceBlock: BlockConfig<ConfluenceRetrieveResponse> = {
|
||||
bgColor: '#E0E0E0',
|
||||
icon: ConfluenceIcon,
|
||||
subBlocks: [
|
||||
// Operation selector
|
||||
{
|
||||
id: 'operation',
|
||||
title: 'Operation',
|
||||
type: 'dropdown',
|
||||
layout: 'full',
|
||||
options: [
|
||||
{ label: 'Read Page', id: 'read' },
|
||||
{ label: 'Update Page', id: 'update' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'domain',
|
||||
title: 'Domain',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
placeholder: 'Enter Confluence domain (e.g., yourcompany.atlassian.net)',
|
||||
placeholder: 'Enter Confluence domain (e.g., simstudio.atlassian.net)',
|
||||
},
|
||||
{
|
||||
id: 'credential',
|
||||
@@ -26,21 +39,56 @@ export const ConfluenceBlock: BlockConfig<ConfluenceRetrieveResponse> = {
|
||||
layout: 'full',
|
||||
provider: 'confluence',
|
||||
serviceId: 'confluence',
|
||||
requiredScopes: ['read:confluence-content.all', 'read:me', 'offline_access'],
|
||||
requiredScopes: [
|
||||
'read:page:confluence',
|
||||
'read:confluence-content.all',
|
||||
'write:confluence-content',
|
||||
'read:me',
|
||||
'offline_access',
|
||||
],
|
||||
placeholder: 'Select Confluence account',
|
||||
},
|
||||
// Use file-selector component for page selection
|
||||
{
|
||||
id: 'pageId',
|
||||
title: 'Page ID',
|
||||
title: 'Select Page',
|
||||
type: 'file-selector',
|
||||
layout: 'full',
|
||||
provider: 'confluence',
|
||||
serviceId: 'confluence',
|
||||
placeholder: 'Select Confluence page',
|
||||
},
|
||||
// Update page fields
|
||||
{
|
||||
id: 'title',
|
||||
title: 'New Title',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
placeholder: 'Enter the confluence page ID (e.g., 12340)',
|
||||
placeholder: 'Enter new title for the page',
|
||||
condition: { field: 'operation', value: 'update' },
|
||||
},
|
||||
{
|
||||
id: 'content',
|
||||
title: 'New Content',
|
||||
type: 'long-input',
|
||||
layout: 'full',
|
||||
placeholder: 'Enter new content for the page',
|
||||
condition: { field: 'operation', value: 'update' },
|
||||
},
|
||||
],
|
||||
tools: {
|
||||
access: ['confluence_retrieve'],
|
||||
access: ['confluence_retrieve', 'confluence_update'],
|
||||
config: {
|
||||
tool: () => 'confluence_retrieve',
|
||||
tool: (params) => {
|
||||
switch (params.operation) {
|
||||
case 'read':
|
||||
return 'confluence_retrieve'
|
||||
case 'update':
|
||||
return 'confluence_update'
|
||||
default:
|
||||
return 'confluence_retrieve'
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
const { credential, ...rest } = params
|
||||
|
||||
@@ -52,9 +100,13 @@ export const ConfluenceBlock: BlockConfig<ConfluenceRetrieveResponse> = {
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
operation: { type: 'string', required: true },
|
||||
domain: { type: 'string', required: true },
|
||||
credential: { type: 'string', required: true },
|
||||
pageId: { type: 'string', required: true },
|
||||
// Update operation inputs
|
||||
title: { type: 'string', required: false },
|
||||
content: { type: 'string', required: false },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
@@ -63,6 +115,7 @@ export const ConfluenceBlock: BlockConfig<ConfluenceRetrieveResponse> = {
|
||||
pageId: 'string',
|
||||
content: 'string',
|
||||
title: 'string',
|
||||
success: 'boolean',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -41,7 +41,7 @@ export const GoogleDriveBlock: BlockConfig<GoogleDriveResponse> = {
|
||||
layout: 'full',
|
||||
provider: 'google-drive',
|
||||
serviceId: 'google-drive',
|
||||
requiredScopes: ['https://www.googleapis.com/auth/drive'],
|
||||
requiredScopes: ['https://www.googleapis.com/auth/drive.file'],
|
||||
placeholder: 'Select Google Drive account',
|
||||
},
|
||||
// Upload Fields
|
||||
|
||||
+7
-2
@@ -208,7 +208,6 @@ export const auth = betterAuth({
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
],
|
||||
prompt: 'consent',
|
||||
@@ -305,7 +304,13 @@ export const auth = betterAuth({
|
||||
authorizationUrl: 'https://auth.atlassian.com/authorize',
|
||||
tokenUrl: 'https://auth.atlassian.com/oauth/token',
|
||||
userInfoUrl: 'https://api.atlassian.com/me',
|
||||
scopes: ['read:confluence-content.all', 'read:me', 'offline_access'],
|
||||
scopes: [
|
||||
'read:page:confluence',
|
||||
'read:confluence-content.all',
|
||||
'read:me',
|
||||
'offline_access',
|
||||
'write:confluence-content',
|
||||
],
|
||||
responseType: 'code',
|
||||
pkce: true,
|
||||
accessType: 'offline',
|
||||
|
||||
+8
-5
@@ -75,10 +75,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
providerId: 'google-drive',
|
||||
icon: (props) => GoogleDriveIcon(props),
|
||||
baseProviderIcon: (props) => GoogleIcon(props),
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
],
|
||||
scopes: ['https://www.googleapis.com/auth/drive.file'],
|
||||
},
|
||||
'google-docs': {
|
||||
id: 'google-docs',
|
||||
@@ -185,7 +182,13 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
providerId: 'confluence',
|
||||
icon: (props) => ConfluenceIcon(props),
|
||||
baseProviderIcon: (props) => ConfluenceIcon(props),
|
||||
scopes: ['read:confluence-content.all', 'read:me', 'offline_access'],
|
||||
scopes: [
|
||||
'read:page:confluence',
|
||||
'read:confluence-content.all',
|
||||
'read:me',
|
||||
'offline_access',
|
||||
'write:confluence-content',
|
||||
],
|
||||
},
|
||||
},
|
||||
defaultService: 'confluence',
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './types'
|
||||
export { confluenceRetrieveTool } from './retrieve'
|
||||
export { confluenceListTool } from './list'
|
||||
export { confluenceUpdateTool } from './update'
|
||||
@@ -0,0 +1,104 @@
|
||||
import { ToolConfig } from '../types'
|
||||
import { ConfluenceListParams, ConfluenceListResponse } from './types'
|
||||
|
||||
export const confluenceListTool: ToolConfig<ConfluenceListParams, ConfluenceListResponse> = {
|
||||
id: 'confluence_list',
|
||||
name: 'Confluence List Pages',
|
||||
description: 'List pages from Confluence using the Confluence API.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'confluence',
|
||||
additionalScopes: [
|
||||
'read:page:confluence',
|
||||
'read:confluence-content.all',
|
||||
'read:me',
|
||||
'offline_access',
|
||||
],
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'OAuth access token for Confluence',
|
||||
},
|
||||
domain: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
requiredForToolCall: true,
|
||||
description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
description: 'Maximum number of pages to return (default: 25, max: 100)',
|
||||
},
|
||||
spaceKey: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: 'Filter pages by space key',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: 'Filter pages by title',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: ConfluenceListParams) => {
|
||||
const baseUrl = `https://${params.domain}/wiki/api/v2/pages`
|
||||
const queryParams = new URLSearchParams()
|
||||
|
||||
if (params.limit) {
|
||||
queryParams.append('limit', params.limit.toString())
|
||||
}
|
||||
|
||||
if (params.spaceKey) {
|
||||
queryParams.append('space-id', params.spaceKey)
|
||||
}
|
||||
|
||||
if (params.title) {
|
||||
queryParams.append('title', params.title)
|
||||
}
|
||||
|
||||
const queryString = queryParams.toString()
|
||||
return queryString ? `${baseUrl}?${queryString}` : baseUrl
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params: ConfluenceListParams) => {
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || 'Confluence API error')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
pages: data.results.map((page: any) => ({
|
||||
id: page.id,
|
||||
title: page.title,
|
||||
spaceKey: page.spaceKey,
|
||||
url: page._links?.webui || '',
|
||||
lastModified: page.version?.when || '',
|
||||
})),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error: any) => {
|
||||
const message = error.message || 'Confluence list pages failed'
|
||||
return message
|
||||
},
|
||||
}
|
||||
@@ -14,7 +14,12 @@ export const confluenceRetrieveTool: ToolConfig<
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'confluence',
|
||||
additionalScopes: ['read:confluence-content.all', 'read:me', 'offline_access'],
|
||||
additionalScopes: [
|
||||
'read:page:confluence',
|
||||
'read:confluence-content.all',
|
||||
'read:me',
|
||||
'offline_access',
|
||||
],
|
||||
},
|
||||
|
||||
params: {
|
||||
@@ -38,7 +43,7 @@ export const confluenceRetrieveTool: ToolConfig<
|
||||
|
||||
request: {
|
||||
url: (params: ConfluenceRetrieveParams) => {
|
||||
return `https://${params.domain}/wiki/rest/api/content/${params.pageId}?expand=body.view`
|
||||
return `https://${params.domain}/wiki/api/v2/pages/${params.pageId}?expand=body.view`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params: ConfluenceRetrieveParams) => {
|
||||
|
||||
@@ -14,3 +14,44 @@ export interface ConfluenceRetrieveResponse extends ToolResponse {
|
||||
title: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface ConfluenceListParams {
|
||||
accessToken: string
|
||||
domain: string
|
||||
limit?: number
|
||||
spaceKey?: string
|
||||
title?: string
|
||||
}
|
||||
|
||||
export interface ConfluencePage {
|
||||
id: string
|
||||
title: string
|
||||
spaceKey?: string
|
||||
url?: string
|
||||
lastModified?: string
|
||||
}
|
||||
|
||||
export interface ConfluenceListResponse extends ToolResponse {
|
||||
output: {
|
||||
ts: string
|
||||
pages: ConfluencePage[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface ConfluenceUpdateParams {
|
||||
accessToken: string
|
||||
domain: string
|
||||
pageId: string
|
||||
title?: string
|
||||
content?: string
|
||||
version?: number
|
||||
}
|
||||
|
||||
export interface ConfluenceUpdateResponse extends ToolResponse {
|
||||
output: {
|
||||
ts: string
|
||||
pageId: string
|
||||
title: string
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { ToolConfig } from '../types'
|
||||
import { ConfluenceUpdateParams, ConfluenceUpdateResponse } from './types'
|
||||
|
||||
export const confluenceUpdateTool: ToolConfig<ConfluenceUpdateParams, ConfluenceUpdateResponse> = {
|
||||
id: 'confluence_update',
|
||||
name: 'Confluence Update',
|
||||
description: 'Update a Confluence page using the Confluence API.',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'confluence',
|
||||
additionalScopes: [
|
||||
'read:confluence-content.all',
|
||||
'write:confluence-content',
|
||||
'read:me',
|
||||
'offline_access',
|
||||
],
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'OAuth access token for Confluence',
|
||||
},
|
||||
domain: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
requiredForToolCall: true,
|
||||
description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)',
|
||||
},
|
||||
pageId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Confluence page ID to update',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: 'New title for the page',
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: 'New content for the page in Confluence storage format',
|
||||
},
|
||||
version: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
description: 'Version number of the page (required for preventing conflicts)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: ConfluenceUpdateParams) => {
|
||||
return `https://${params.domain}/wiki/api/v2/pages/${params.pageId}`
|
||||
},
|
||||
method: 'PUT',
|
||||
headers: (params: ConfluenceUpdateParams) => {
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}
|
||||
},
|
||||
body: (params: ConfluenceUpdateParams) => {
|
||||
const body: Record<string, any> = {}
|
||||
|
||||
if (params.title) {
|
||||
body.title = params.title
|
||||
}
|
||||
|
||||
if (params.content) {
|
||||
body.body = {
|
||||
representation: 'storage',
|
||||
value: params.content,
|
||||
}
|
||||
}
|
||||
|
||||
if (params.version) {
|
||||
body.version = {
|
||||
number: params.version,
|
||||
message: 'Updated via Sim Studio',
|
||||
}
|
||||
}
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || 'Confluence API error')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
ts: new Date().toISOString(),
|
||||
pageId: data.id,
|
||||
title: data.title,
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error: any) => {
|
||||
const message = error.message || 'Confluence update failed'
|
||||
return message
|
||||
},
|
||||
}
|
||||
@@ -9,7 +9,7 @@ export const downloadTool: ToolConfig<GoogleDriveToolParams, GoogleDriveDownload
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-drive',
|
||||
additionalScopes: ['https://www.googleapis.com/auth/drive'],
|
||||
additionalScopes: ['https://www.googleapis.com/auth/drive.file'],
|
||||
},
|
||||
params: {
|
||||
accessToken: { type: 'string', required: true },
|
||||
|
||||
@@ -16,7 +16,7 @@ export const exportTool: ToolConfig<
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-drive',
|
||||
additionalScopes: ['https://www.googleapis.com/auth/drive'],
|
||||
additionalScopes: ['https://www.googleapis.com/auth/drive.file'],
|
||||
},
|
||||
params: {
|
||||
accessToken: { type: 'string', required: true },
|
||||
|
||||
@@ -9,7 +9,7 @@ export const listTool: ToolConfig<GoogleDriveToolParams, GoogleDriveListResponse
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-drive',
|
||||
additionalScopes: ['https://www.googleapis.com/auth/drive'],
|
||||
additionalScopes: ['https://www.googleapis.com/auth/drive.file'],
|
||||
},
|
||||
params: {
|
||||
accessToken: { type: 'string', required: true },
|
||||
|
||||
+3
-1
@@ -1,6 +1,7 @@
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { useCustomToolsStore } from '@/stores/custom-tools/store'
|
||||
import { useEnvironmentStore } from '@/stores/settings/environment/store'
|
||||
import { confluenceListTool, confluenceRetrieveTool, confluenceUpdateTool } from './confluence'
|
||||
import { docsCreateTool, docsReadTool, docsWriteTool } from './docs'
|
||||
import { driveDownloadTool, driveListTool, driveUploadTool } from './drive'
|
||||
import { exaAnswerTool, exaFindSimilarLinksTool, exaGetContentsTool, exaSearchTool } from './exa'
|
||||
@@ -40,7 +41,6 @@ import { visionTool } from './vision/vision'
|
||||
import { whatsappSendMessageTool } from './whatsapp'
|
||||
import { xReadTool, xSearchTool, xUserTool, xWriteTool } from './x'
|
||||
import { youtubeSearchTool } from './youtube/search'
|
||||
import { confluenceRetrieveTool } from './confluence/retrieve'
|
||||
|
||||
const logger = createLogger('Tools')
|
||||
|
||||
@@ -100,6 +100,8 @@ export const tools: Record<string, ToolConfig> = {
|
||||
guesty_guest: guestyGuestTool,
|
||||
perplexity_chat: perplexityChatTool,
|
||||
confluence_retrieve: confluenceRetrieveTool,
|
||||
confluence_list: confluenceListTool,
|
||||
confluence_update: confluenceUpdateTool,
|
||||
}
|
||||
|
||||
// Get a tool by its ID
|
||||
|
||||
Reference in New Issue
Block a user