mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-10 23:48:09 -05:00
Compare commits
2 Commits
SIM-514-us
...
fix/tool-o
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92a08b0a33 | ||
|
|
5d3b216922 |
@@ -130,7 +130,6 @@ When running with Docker, use `host.docker.internal` if vLLM is on your host mac
|
||||
|
||||
**Requirements:**
|
||||
- [Bun](https://bun.sh/) runtime
|
||||
- [Node.js](https://nodejs.org/) v20+ (required for sandboxed code execution)
|
||||
- PostgreSQL 12+ with [pgvector extension](https://github.com/pgvector/pgvector) (required for AI embeddings)
|
||||
|
||||
**Note:** Sim uses vector embeddings for AI features like knowledge bases and semantic search, which requires the `pgvector` PostgreSQL extension.
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type React from 'react'
|
||||
import { findNeighbour } from 'fumadocs-core/page-tree'
|
||||
import defaultMdxComponents from 'fumadocs-ui/mdx'
|
||||
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/page'
|
||||
@@ -11,15 +10,14 @@ import { LLMCopyButton } from '@/components/page-actions'
|
||||
import { StructuredData } from '@/components/structured-data'
|
||||
import { CodeBlock } from '@/components/ui/code-block'
|
||||
import { Heading } from '@/components/ui/heading'
|
||||
import { type PageData, source } from '@/lib/source'
|
||||
import { source } from '@/lib/source'
|
||||
|
||||
export default async function Page(props: { params: Promise<{ slug?: string[]; lang: string }> }) {
|
||||
const params = await props.params
|
||||
const page = source.getPage(params.slug, params.lang)
|
||||
if (!page) notFound()
|
||||
|
||||
const data = page.data as PageData
|
||||
const MDX = data.body
|
||||
const MDX = page.data.body
|
||||
const baseUrl = 'https://docs.sim.ai'
|
||||
|
||||
const pageTreeRecord = source.pageTree as Record<string, any>
|
||||
@@ -53,7 +51,7 @@ export default async function Page(props: { params: Promise<{ slug?: string[]; l
|
||||
|
||||
if (index === urlParts.length - 1) {
|
||||
breadcrumbs.push({
|
||||
name: data.title,
|
||||
name: page.data.title,
|
||||
url: `${baseUrl}${page.url}`,
|
||||
})
|
||||
} else {
|
||||
@@ -170,15 +168,15 @@ export default async function Page(props: { params: Promise<{ slug?: string[]; l
|
||||
return (
|
||||
<>
|
||||
<StructuredData
|
||||
title={data.title}
|
||||
description={data.description || ''}
|
||||
title={page.data.title}
|
||||
description={page.data.description || ''}
|
||||
url={`${baseUrl}${page.url}`}
|
||||
lang={params.lang}
|
||||
breadcrumb={breadcrumbs}
|
||||
/>
|
||||
<DocsPage
|
||||
toc={data.toc}
|
||||
full={data.full}
|
||||
toc={page.data.toc}
|
||||
full={page.data.full}
|
||||
breadcrumb={{
|
||||
enabled: false,
|
||||
}}
|
||||
@@ -209,32 +207,20 @@ export default async function Page(props: { params: Promise<{ slug?: string[]; l
|
||||
</div>
|
||||
<PageNavigationArrows previous={neighbours?.previous} next={neighbours?.next} />
|
||||
</div>
|
||||
<DocsTitle>{data.title}</DocsTitle>
|
||||
<DocsDescription>{data.description}</DocsDescription>
|
||||
<DocsTitle>{page.data.title}</DocsTitle>
|
||||
<DocsDescription>{page.data.description}</DocsDescription>
|
||||
</div>
|
||||
<DocsBody>
|
||||
<MDX
|
||||
components={{
|
||||
...defaultMdxComponents,
|
||||
CodeBlock,
|
||||
h1: (props: React.HTMLAttributes<HTMLHeadingElement>) => (
|
||||
<Heading as='h1' {...props} />
|
||||
),
|
||||
h2: (props: React.HTMLAttributes<HTMLHeadingElement>) => (
|
||||
<Heading as='h2' {...props} />
|
||||
),
|
||||
h3: (props: React.HTMLAttributes<HTMLHeadingElement>) => (
|
||||
<Heading as='h3' {...props} />
|
||||
),
|
||||
h4: (props: React.HTMLAttributes<HTMLHeadingElement>) => (
|
||||
<Heading as='h4' {...props} />
|
||||
),
|
||||
h5: (props: React.HTMLAttributes<HTMLHeadingElement>) => (
|
||||
<Heading as='h5' {...props} />
|
||||
),
|
||||
h6: (props: React.HTMLAttributes<HTMLHeadingElement>) => (
|
||||
<Heading as='h6' {...props} />
|
||||
),
|
||||
h1: (props) => <Heading as='h1' {...props} />,
|
||||
h2: (props) => <Heading as='h2' {...props} />,
|
||||
h3: (props) => <Heading as='h3' {...props} />,
|
||||
h4: (props) => <Heading as='h4' {...props} />,
|
||||
h5: (props) => <Heading as='h5' {...props} />,
|
||||
h6: (props) => <Heading as='h6' {...props} />,
|
||||
}}
|
||||
/>
|
||||
</DocsBody>
|
||||
@@ -254,17 +240,16 @@ export async function generateMetadata(props: {
|
||||
const page = source.getPage(params.slug, params.lang)
|
||||
if (!page) notFound()
|
||||
|
||||
const data = page.data as PageData
|
||||
const baseUrl = 'https://docs.sim.ai'
|
||||
const fullUrl = `${baseUrl}${page.url}`
|
||||
|
||||
const description = data.description || ''
|
||||
const ogImageUrl = `${baseUrl}/api/og?title=${encodeURIComponent(data.title)}&category=DOCUMENTATION${description ? `&description=${encodeURIComponent(description)}` : ''}`
|
||||
const description = page.data.description || ''
|
||||
const ogImageUrl = `${baseUrl}/api/og?title=${encodeURIComponent(page.data.title)}&category=DOCUMENTATION${description ? `&description=${encodeURIComponent(description)}` : ''}`
|
||||
|
||||
return {
|
||||
title: data.title,
|
||||
title: page.data.title,
|
||||
description:
|
||||
data.description || 'Sim visual workflow builder for AI applications documentation',
|
||||
page.data.description || 'Sim visual workflow builder for AI applications documentation',
|
||||
keywords: [
|
||||
'AI workflow builder',
|
||||
'visual workflow editor',
|
||||
@@ -273,16 +258,16 @@ export async function generateMetadata(props: {
|
||||
'AI agents',
|
||||
'no-code AI',
|
||||
'drag and drop workflows',
|
||||
data.title?.toLowerCase().split(' '),
|
||||
page.data.title?.toLowerCase().split(' '),
|
||||
]
|
||||
.flat()
|
||||
.filter(Boolean),
|
||||
authors: [{ name: 'Sim Team' }],
|
||||
category: 'Developer Tools',
|
||||
openGraph: {
|
||||
title: data.title,
|
||||
title: page.data.title,
|
||||
description:
|
||||
data.description || 'Sim visual workflow builder for AI applications documentation',
|
||||
page.data.description || 'Sim visual workflow builder for AI applications documentation',
|
||||
url: fullUrl,
|
||||
siteName: 'Sim Documentation',
|
||||
type: 'article',
|
||||
@@ -295,15 +280,15 @@ export async function generateMetadata(props: {
|
||||
url: ogImageUrl,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: data.title,
|
||||
alt: page.data.title,
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: data.title,
|
||||
title: page.data.title,
|
||||
description:
|
||||
data.description || 'Sim visual workflow builder for AI applications documentation',
|
||||
page.data.description || 'Sim visual workflow builder for AI applications documentation',
|
||||
images: [ogImageUrl],
|
||||
creator: '@simdotai',
|
||||
site: '@simdotai',
|
||||
|
||||
@@ -43,6 +43,7 @@ export async function GET(request: NextRequest) {
|
||||
const description = searchParams.get('description') || ''
|
||||
|
||||
const baseUrl = new URL(request.url).origin
|
||||
const backgroundImageUrl = `${baseUrl}/static/og-background.png`
|
||||
|
||||
const allText = `${title}${category}${description}docs.sim.ai`
|
||||
const fontData = await loadGoogleFont('Geist', '400;500;600', allText)
|
||||
@@ -54,49 +55,36 @@ export async function GET(request: NextRequest) {
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
background: '#0c0c0c',
|
||||
background: 'linear-gradient(315deg, #1e1e3f 0%, #1a1a2e 40%, #0f0f0f 100%)',
|
||||
position: 'relative',
|
||||
fontFamily: 'Geist',
|
||||
}}
|
||||
>
|
||||
{/* Base gradient layer - very subtle purple tint across the entire image */}
|
||||
<div
|
||||
{/* Background texture */}
|
||||
<img
|
||||
src={backgroundImageUrl}
|
||||
alt=''
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
background:
|
||||
'radial-gradient(ellipse 150% 100% at 50% 100%, rgba(88, 28, 135, 0.15) 0%, rgba(88, 28, 135, 0.08) 25%, rgba(88, 28, 135, 0.03) 50%, transparent 80%)',
|
||||
display: 'flex',
|
||||
objectFit: 'cover',
|
||||
opacity: 0.04,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Secondary glow - adds depth without harsh edges */}
|
||||
{/* Subtle purple glow from bottom right */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
width: '50%',
|
||||
height: '100%',
|
||||
background:
|
||||
'radial-gradient(ellipse 100% 80% at 80% 90%, rgba(112, 31, 252, 0.12) 0%, rgba(112, 31, 252, 0.04) 40%, transparent 70%)',
|
||||
display: 'flex',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Top darkening - creates natural vignette */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
background:
|
||||
'linear-gradient(180deg, rgba(0, 0, 0, 0.3) 0%, transparent 40%, transparent 100%)',
|
||||
'radial-gradient(ellipse at bottom right, rgba(112, 31, 252, 0.1) 0%, transparent 50%)',
|
||||
display: 'flex',
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -3335,24 +3335,6 @@ export function SalesforceIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function ServiceNowIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 1570 1403'
|
||||
width='48'
|
||||
height='48'
|
||||
>
|
||||
<path
|
||||
fill='#62d84e'
|
||||
fillRule='evenodd'
|
||||
d='M1228.4 138.9c129.2 88.9 228.9 214.3 286.3 360.2 57.5 145.8 70 305.5 36 458.5S1437.8 1250 1324 1357.9c-13.3 12.9-28.8 23.4-45.8 30.8-17 7.5-35.2 11.9-53.7 12.9-18.5 1.1-37.1-1.1-54.8-6.6-17.7-5.4-34.3-13.9-49.1-25.2-48.2-35.9-101.8-63.8-158.8-82.6-57.1-18.9-116.7-28.5-176.8-28.5s-119.8 9.6-176.8 28.5c-57 18.8-110.7 46.7-158.9 82.6-14.6 11.2-31 19.8-48.6 25.3s-36 7.8-54.4 6.8c-18.4-.9-36.5-5.1-53.4-12.4s-32.4-17.5-45.8-30.2C132.5 1251 53 1110.8 19 956.8s-20.9-314.6 37.6-461c58.5-146.5 159.6-272 290.3-360.3S631.8.1 789.6.5c156.8 1.3 309.6 49.6 438.8 138.4m-291.8 1014c48.2-19.2 92-48 128.7-84.6 36.7-36.7 65.5-80.4 84.7-128.6 19.2-48.1 28.4-99.7 27-151.5 0-103.9-41.3-203.5-114.8-277S889 396.4 785 396.4s-203.7 41.3-277.2 114.8S393 684.3 393 788.2c-1.4 51.8 7.8 103.4 27 151.5 19.2 48.2 48 91.9 84.7 128.6 36.7 36.6 80.5 65.4 128.6 84.6 48.2 19.2 99.8 28.4 151.7 27 51.8 1.4 103.4-7.8 151.6-27'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function ApolloIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -85,7 +85,6 @@ import {
|
||||
SendgridIcon,
|
||||
SentryIcon,
|
||||
SerperIcon,
|
||||
ServiceNowIcon,
|
||||
SftpIcon,
|
||||
ShopifyIcon,
|
||||
SlackIcon,
|
||||
@@ -120,117 +119,116 @@ import {
|
||||
type IconComponent = ComponentType<SVGProps<SVGSVGElement>>
|
||||
|
||||
export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
calendly: CalendlyIcon,
|
||||
mailchimp: MailchimpIcon,
|
||||
postgresql: PostgresIcon,
|
||||
twilio_voice: TwilioIcon,
|
||||
elasticsearch: ElasticsearchIcon,
|
||||
rds: RDSIcon,
|
||||
translate: TranslateIcon,
|
||||
dynamodb: DynamoDBIcon,
|
||||
wordpress: WordpressIcon,
|
||||
tavily: TavilyIcon,
|
||||
zoom: ZoomIcon,
|
||||
zep: ZepIcon,
|
||||
zendesk: ZendeskIcon,
|
||||
youtube: YouTubeIcon,
|
||||
supabase: SupabaseIcon,
|
||||
vision: EyeIcon,
|
||||
zoom: ZoomIcon,
|
||||
confluence: ConfluenceIcon,
|
||||
arxiv: ArxivIcon,
|
||||
webflow: WebflowIcon,
|
||||
pinecone: PineconeIcon,
|
||||
apollo: ApolloIcon,
|
||||
servicenow: ServiceNowIcon,
|
||||
whatsapp: WhatsAppIcon,
|
||||
typeform: TypeformIcon,
|
||||
qdrant: QdrantIcon,
|
||||
shopify: ShopifyIcon,
|
||||
asana: AsanaIcon,
|
||||
sqs: SQSIcon,
|
||||
apify: ApifyIcon,
|
||||
memory: BrainIcon,
|
||||
gitlab: GitLabIcon,
|
||||
polymarket: PolymarketIcon,
|
||||
serper: SerperIcon,
|
||||
linear: LinearIcon,
|
||||
exa: ExaAIIcon,
|
||||
telegram: TelegramIcon,
|
||||
salesforce: SalesforceIcon,
|
||||
hubspot: HubspotIcon,
|
||||
hunter: HunterIOIcon,
|
||||
linkup: LinkupIcon,
|
||||
mongodb: MongoDBIcon,
|
||||
airtable: AirtableIcon,
|
||||
discord: DiscordIcon,
|
||||
ahrefs: AhrefsIcon,
|
||||
neo4j: Neo4jIcon,
|
||||
tts: TTSIcon,
|
||||
jina: JinaAIIcon,
|
||||
google_docs: GoogleDocsIcon,
|
||||
perplexity: PerplexityIcon,
|
||||
google_search: GoogleIcon,
|
||||
x: xIcon,
|
||||
kalshi: KalshiIcon,
|
||||
google_calendar: GoogleCalendarIcon,
|
||||
zep: ZepIcon,
|
||||
posthog: PosthogIcon,
|
||||
grafana: GrafanaIcon,
|
||||
google_slides: GoogleSlidesIcon,
|
||||
microsoft_planner: MicrosoftPlannerIcon,
|
||||
thinking: BrainIcon,
|
||||
pipedrive: PipedriveIcon,
|
||||
dropbox: DropboxIcon,
|
||||
stagehand: StagehandIcon,
|
||||
google_forms: GoogleFormsIcon,
|
||||
file: DocumentIcon,
|
||||
mistral_parse: MistralIcon,
|
||||
gmail: GmailIcon,
|
||||
openai: OpenAIIcon,
|
||||
outlook: OutlookIcon,
|
||||
incidentio: IncidentioIcon,
|
||||
onedrive: MicrosoftOneDriveIcon,
|
||||
resend: ResendIcon,
|
||||
google_vault: GoogleVaultIcon,
|
||||
sharepoint: MicrosoftSharepointIcon,
|
||||
huggingface: HuggingFaceIcon,
|
||||
sendgrid: SendgridIcon,
|
||||
video_generator: VideoIcon,
|
||||
smtp: SmtpIcon,
|
||||
google_groups: GoogleGroupsIcon,
|
||||
mailgun: MailgunIcon,
|
||||
clay: ClayIcon,
|
||||
jira: JiraIcon,
|
||||
search: SearchIcon,
|
||||
linkedin: LinkedInIcon,
|
||||
wealthbox: WealthboxIcon,
|
||||
notion: NotionIcon,
|
||||
elevenlabs: ElevenLabsIcon,
|
||||
microsoft_teams: MicrosoftTeamsIcon,
|
||||
github: GithubIcon,
|
||||
sftp: SftpIcon,
|
||||
ssh: SshIcon,
|
||||
google_drive: GoogleDriveIcon,
|
||||
sentry: SentryIcon,
|
||||
reddit: RedditIcon,
|
||||
parallel_ai: ParallelIcon,
|
||||
spotify: SpotifyIcon,
|
||||
stripe: StripeIcon,
|
||||
s3: S3Icon,
|
||||
trello: TrelloIcon,
|
||||
mem0: Mem0Icon,
|
||||
knowledge: PackageSearchIcon,
|
||||
intercom: IntercomIcon,
|
||||
twilio_sms: TwilioIcon,
|
||||
duckduckgo: DuckDuckGoIcon,
|
||||
slack: SlackIcon,
|
||||
datadog: DatadogIcon,
|
||||
microsoft_excel: MicrosoftExcelIcon,
|
||||
image_generator: ImageIcon,
|
||||
google_sheets: GoogleSheetsIcon,
|
||||
wordpress: WordpressIcon,
|
||||
wikipedia: WikipediaIcon,
|
||||
cursor: CursorIcon,
|
||||
firecrawl: FirecrawlIcon,
|
||||
mysql: MySQLIcon,
|
||||
browser_use: BrowserUseIcon,
|
||||
whatsapp: WhatsAppIcon,
|
||||
webflow: WebflowIcon,
|
||||
wealthbox: WealthboxIcon,
|
||||
vision: EyeIcon,
|
||||
video_generator: VideoIcon,
|
||||
typeform: TypeformIcon,
|
||||
twilio_voice: TwilioIcon,
|
||||
twilio_sms: TwilioIcon,
|
||||
tts: TTSIcon,
|
||||
trello: TrelloIcon,
|
||||
translate: TranslateIcon,
|
||||
thinking: BrainIcon,
|
||||
telegram: TelegramIcon,
|
||||
tavily: TavilyIcon,
|
||||
supabase: SupabaseIcon,
|
||||
stt: STTIcon,
|
||||
stripe: StripeIcon,
|
||||
stagehand: StagehandIcon,
|
||||
ssh: SshIcon,
|
||||
sqs: SQSIcon,
|
||||
spotify: SpotifyIcon,
|
||||
smtp: SmtpIcon,
|
||||
slack: SlackIcon,
|
||||
shopify: ShopifyIcon,
|
||||
sharepoint: MicrosoftSharepointIcon,
|
||||
sftp: SftpIcon,
|
||||
serper: SerperIcon,
|
||||
sentry: SentryIcon,
|
||||
sendgrid: SendgridIcon,
|
||||
search: SearchIcon,
|
||||
salesforce: SalesforceIcon,
|
||||
s3: S3Icon,
|
||||
resend: ResendIcon,
|
||||
reddit: RedditIcon,
|
||||
rds: RDSIcon,
|
||||
qdrant: QdrantIcon,
|
||||
posthog: PosthogIcon,
|
||||
postgresql: PostgresIcon,
|
||||
polymarket: PolymarketIcon,
|
||||
pipedrive: PipedriveIcon,
|
||||
pinecone: PineconeIcon,
|
||||
perplexity: PerplexityIcon,
|
||||
parallel_ai: ParallelIcon,
|
||||
outlook: OutlookIcon,
|
||||
openai: OpenAIIcon,
|
||||
onedrive: MicrosoftOneDriveIcon,
|
||||
notion: NotionIcon,
|
||||
neo4j: Neo4jIcon,
|
||||
mysql: MySQLIcon,
|
||||
mongodb: MongoDBIcon,
|
||||
mistral_parse: MistralIcon,
|
||||
microsoft_teams: MicrosoftTeamsIcon,
|
||||
microsoft_planner: MicrosoftPlannerIcon,
|
||||
microsoft_excel: MicrosoftExcelIcon,
|
||||
memory: BrainIcon,
|
||||
mem0: Mem0Icon,
|
||||
mailgun: MailgunIcon,
|
||||
mailchimp: MailchimpIcon,
|
||||
linkup: LinkupIcon,
|
||||
linkedin: LinkedInIcon,
|
||||
linear: LinearIcon,
|
||||
knowledge: PackageSearchIcon,
|
||||
kalshi: KalshiIcon,
|
||||
jira: JiraIcon,
|
||||
jina: JinaAIIcon,
|
||||
intercom: IntercomIcon,
|
||||
incidentio: IncidentioIcon,
|
||||
image_generator: ImageIcon,
|
||||
hunter: HunterIOIcon,
|
||||
huggingface: HuggingFaceIcon,
|
||||
hubspot: HubspotIcon,
|
||||
grafana: GrafanaIcon,
|
||||
google_vault: GoogleVaultIcon,
|
||||
google_slides: GoogleSlidesIcon,
|
||||
google_sheets: GoogleSheetsIcon,
|
||||
google_groups: GoogleGroupsIcon,
|
||||
google_forms: GoogleFormsIcon,
|
||||
google_drive: GoogleDriveIcon,
|
||||
google_docs: GoogleDocsIcon,
|
||||
google_calendar: GoogleCalendarIcon,
|
||||
google_search: GoogleIcon,
|
||||
gmail: GmailIcon,
|
||||
gitlab: GitLabIcon,
|
||||
github: GithubIcon,
|
||||
firecrawl: FirecrawlIcon,
|
||||
file: DocumentIcon,
|
||||
exa: ExaAIIcon,
|
||||
elevenlabs: ElevenLabsIcon,
|
||||
elasticsearch: ElasticsearchIcon,
|
||||
dynamodb: DynamoDBIcon,
|
||||
duckduckgo: DuckDuckGoIcon,
|
||||
dropbox: DropboxIcon,
|
||||
discord: DiscordIcon,
|
||||
datadog: DatadogIcon,
|
||||
cursor: CursorIcon,
|
||||
confluence: ConfluenceIcon,
|
||||
clay: ClayIcon,
|
||||
calendly: CalendlyIcon,
|
||||
browser_use: BrowserUseIcon,
|
||||
asana: AsanaIcon,
|
||||
arxiv: ArxivIcon,
|
||||
apollo: ApolloIcon,
|
||||
apify: ApifyIcon,
|
||||
airtable: AirtableIcon,
|
||||
ahrefs: AhrefsIcon,
|
||||
}
|
||||
|
||||
@@ -48,13 +48,8 @@ Ruft detaillierte Informationen zu einem bestimmten Jira-Issue ab
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Zeitstempel der Operation |
|
||||
| `issueKey` | string | Issue-Key \(z.B. PROJ-123\) |
|
||||
| `summary` | string | Issue-Zusammenfassung |
|
||||
| `description` | json | Inhalt der Issue-Beschreibung |
|
||||
| `created` | string | Zeitstempel der Issue-Erstellung |
|
||||
| `updated` | string | Zeitstempel der letzten Issue-Aktualisierung |
|
||||
| `issue` | json | Vollständiges Issue-Objekt mit allen Feldern |
|
||||
| `success` | boolean | Status des Operationserfolgs |
|
||||
| `output` | object | Jira-Issue-Details mit Issue-Key, Zusammenfassung, Beschreibung, Erstellungs- und Aktualisierungszeitstempeln |
|
||||
|
||||
### `jira_update`
|
||||
|
||||
@@ -78,9 +73,8 @@ Ein Jira-Issue aktualisieren
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Zeitstempel der Operation |
|
||||
| `issueKey` | string | Aktualisierter Issue-Key \(z.B. PROJ-123\) |
|
||||
| `summary` | string | Issue-Zusammenfassung nach der Aktualisierung |
|
||||
| `success` | boolean | Erfolgsstatus der Operation |
|
||||
| `output` | object | Aktualisierte Jira-Issue-Details mit Zeitstempel, Issue-Key, Zusammenfassung und Erfolgsstatus |
|
||||
|
||||
### `jira_write`
|
||||
|
||||
@@ -103,10 +97,8 @@ Ein Jira-Issue erstellen
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Zeitstempel der Operation |
|
||||
| `issueKey` | string | Erstellter Issue-Key \(z.B. PROJ-123\) |
|
||||
| `summary` | string | Issue-Zusammenfassung |
|
||||
| `url` | string | URL zum erstellten Issue |
|
||||
| `success` | boolean | Erfolgsstatus der Operation |
|
||||
| `output` | object | Erstellte Jira-Issue-Details mit Zeitstempel, Issue-Key, Zusammenfassung, Erfolgsstatus und URL |
|
||||
|
||||
### `jira_bulk_read`
|
||||
|
||||
@@ -124,7 +116,8 @@ Mehrere Jira-Issues in Masse abrufen
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `issues` | array | Array von Jira-Issues mit Zeitstempel, Zusammenfassung, Beschreibung, Erstellungs- und Aktualisierungszeitstempeln |
|
||||
| `success` | boolean | Erfolgsstatus der Operation |
|
||||
| `output` | array | Array von Jira-Issues mit Zusammenfassung, Beschreibung, Erstellungs- und Aktualisierungszeitstempeln |
|
||||
|
||||
### `jira_delete_issue`
|
||||
|
||||
@@ -143,8 +136,8 @@ Ein Jira-Issue löschen
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Zeitstempel der Operation |
|
||||
| `issueKey` | string | Gelöschter Issue-Key |
|
||||
| `success` | boolean | Erfolgsstatus der Operation |
|
||||
| `output` | object | Details zum gelöschten Issue mit Zeitstempel, Issue-Key und Erfolgsstatus |
|
||||
|
||||
### `jira_assign_issue`
|
||||
|
||||
@@ -163,9 +156,8 @@ Ein Jira-Issue einem Benutzer zuweisen
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Zeitstempel der Operation |
|
||||
| `issueKey` | string | Issue-Key, der zugewiesen wurde |
|
||||
| `assigneeId` | string | Konto-ID des Bearbeiters |
|
||||
| `success` | boolean | Erfolgsstatus der Operation |
|
||||
| `output` | object | Zuweisungsdetails mit Zeitstempel, Issue-Key, Bearbeiter-ID und Erfolgsstatus |
|
||||
|
||||
### `jira_transition_issue`
|
||||
|
||||
@@ -185,9 +177,8 @@ Ein Jira-Issue zwischen Workflow-Status verschieben (z.B. To Do -> In Progress)
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Zeitstempel der Operation |
|
||||
| `issueKey` | string | Issue-Key, der übergangen wurde |
|
||||
| `transitionId` | string | Angewendete Übergangs-ID |
|
||||
| `success` | boolean | Erfolgsstatus der Operation |
|
||||
| `output` | object | Übergangsdetails mit Zeitstempel, Issue-Key, Übergangs-ID und Erfolgsstatus |
|
||||
|
||||
### `jira_search_issues`
|
||||
|
||||
@@ -208,11 +199,8 @@ Nach Jira-Issues mit JQL (Jira Query Language) suchen
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Zeitstempel der Operation |
|
||||
| `total` | number | Gesamtanzahl der übereinstimmenden Issues |
|
||||
| `startAt` | number | Paginierungsstartindex |
|
||||
| `maxResults` | number | Maximale Ergebnisse pro Seite |
|
||||
| `issues` | array | Array übereinstimmender Issues mit Key, Zusammenfassung, Status, Bearbeiter, Erstellungs- und Aktualisierungsdatum |
|
||||
| `success` | boolean | Erfolgsstatus der Operation |
|
||||
| `output` | object | Suchergebnisse mit Zeitstempel, Gesamtanzahl, Paginierungsdetails und Array der übereinstimmenden Issues |
|
||||
|
||||
### `jira_add_comment`
|
||||
|
||||
@@ -231,10 +219,8 @@ Einen Kommentar zu einem Jira-Issue hinzufügen
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Zeitstempel der Operation |
|
||||
| `issueKey` | string | Issue-Key, zu dem der Kommentar hinzugefügt wurde |
|
||||
| `commentId` | string | Erstellte Kommentar-ID |
|
||||
| `body` | string | Kommentartextinhalt |
|
||||
| `success` | boolean | Erfolgsstatus der Operation |
|
||||
| `output` | object | Kommentardetails mit Zeitstempel, Issue-Key, Kommentar-ID, Inhalt und Erfolgsstatus |
|
||||
|
||||
### `jira_get_comments`
|
||||
|
||||
@@ -254,10 +240,8 @@ Alle Kommentare eines Jira-Issues abrufen
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Zeitstempel der Operation |
|
||||
| `issueKey` | string | Issue-Key |
|
||||
| `total` | number | Gesamtanzahl der Kommentare |
|
||||
| `comments` | array | Array von Kommentaren mit ID, Autor, Inhalt, Erstellungs- und Aktualisierungsdatum |
|
||||
| `success` | boolean | Erfolgsstatus der Operation |
|
||||
| `output` | object | Kommentardaten mit Zeitstempel, Issue-Key, Gesamtanzahl und Array von Kommentaren |
|
||||
|
||||
### `jira_update_comment`
|
||||
|
||||
@@ -277,10 +261,8 @@ Einen bestehenden Kommentar zu einem Jira-Issue aktualisieren
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Zeitstempel der Operation |
|
||||
| `issueKey` | string | Issue-Key |
|
||||
| `commentId` | string | Aktualisierte Kommentar-ID |
|
||||
| `body` | string | Aktualisierter Kommentartext |
|
||||
| `success` | boolean | Erfolgsstatus der Operation |
|
||||
| `output` | object | Aktualisierte Kommentardetails mit Zeitstempel, Issue-Key, Kommentar-ID, Textinhalt und Erfolgsstatus |
|
||||
|
||||
### `jira_delete_comment`
|
||||
|
||||
@@ -299,9 +281,8 @@ Einen Kommentar aus einem Jira-Issue löschen
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Zeitstempel der Operation |
|
||||
| `issueKey` | string | Issue-Key |
|
||||
| `commentId` | string | ID des gelöschten Kommentars |
|
||||
| `success` | boolean | Erfolgsstatus der Operation |
|
||||
| `output` | object | Löschdetails mit Zeitstempel, Issue-Key, Kommentar-ID und Erfolgsstatus |
|
||||
|
||||
### `jira_get_attachments`
|
||||
|
||||
@@ -319,9 +300,8 @@ Alle Anhänge eines Jira-Issues abrufen
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Zeitstempel der Operation |
|
||||
| `issueKey` | string | Issue-Key |
|
||||
| `attachments` | array | Array von Anhängen mit ID, Dateiname, Größe, MIME-Typ, Erstellungsdatum und Autor |
|
||||
| `success` | boolean | Erfolgsstatus der Operation |
|
||||
| `output` | object | Anhangsdaten mit Zeitstempel, Issue-Key und Array von Anhängen |
|
||||
|
||||
### `jira_delete_attachment`
|
||||
|
||||
@@ -339,8 +319,8 @@ Einen Anhang von einem Jira-Issue löschen
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Zeitstempel der Operation |
|
||||
| `attachmentId` | string | ID des gelöschten Anhangs |
|
||||
| `success` | boolean | Erfolgsstatus der Operation |
|
||||
| `output` | object | Löschdetails mit Zeitstempel, Anhangs-ID und Erfolgsstatus |
|
||||
|
||||
### `jira_add_worklog`
|
||||
|
||||
@@ -361,10 +341,8 @@ Einen Zeiterfassungseintrag zu einem Jira-Issue hinzufügen
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Zeitstempel der Operation |
|
||||
| `issueKey` | string | Issue-Key, zu dem der Worklog hinzugefügt wurde |
|
||||
| `worklogId` | string | ID des erstellten Worklogs |
|
||||
| `timeSpentSeconds` | number | Aufgewendete Zeit in Sekunden |
|
||||
| `success` | boolean | Erfolgsstatus der Operation |
|
||||
| `output` | object | Worklog-Details mit Zeitstempel, Issue-Key, Worklog-ID, aufgewendeter Zeit in Sekunden und Erfolgsstatus |
|
||||
|
||||
### `jira_get_worklogs`
|
||||
|
||||
@@ -384,10 +362,8 @@ Alle Worklog-Einträge eines Jira-Issues abrufen
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Zeitstempel der Operation |
|
||||
| `issueKey` | string | Issue-Key |
|
||||
| `total` | number | Gesamtanzahl der Worklogs |
|
||||
| `worklogs` | array | Array von Worklogs mit ID, Autor, aufgewendeter Zeit in Sekunden, aufgewendeter Zeit, Kommentar, Erstellungs-, Aktualisierungs- und Startdatum |
|
||||
| `success` | boolean | Erfolgsstatus der Operation |
|
||||
| `output` | object | Worklog-Daten mit Zeitstempel, Issue-Key, Gesamtanzahl und Array von Worklogs |
|
||||
|
||||
### `jira_update_worklog`
|
||||
|
||||
@@ -409,9 +385,8 @@ Aktualisieren eines vorhandenen Worklog-Eintrags in einem Jira-Issue
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Zeitstempel der Operation |
|
||||
| `issueKey` | string | Issue-Key |
|
||||
| `worklogId` | string | ID des aktualisierten Worklogs |
|
||||
| `success` | boolean | Erfolgsstatus der Operation |
|
||||
| `output` | object | Worklog-Aktualisierungsdetails mit Zeitstempel, Issue-Key, Worklog-ID und Erfolgsstatus |
|
||||
|
||||
### `jira_delete_worklog`
|
||||
|
||||
@@ -430,9 +405,8 @@ Löschen eines Worklog-Eintrags aus einem Jira-Issue
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Zeitstempel der Operation |
|
||||
| `issueKey` | string | Issue-Key |
|
||||
| `worklogId` | string | ID des gelöschten Worklogs |
|
||||
| `success` | boolean | Erfolgsstatus der Operation |
|
||||
| `output` | object | Löschdetails mit Zeitstempel, Issue-Key, Worklog-ID und Erfolgsstatus |
|
||||
|
||||
### `jira_create_issue_link`
|
||||
|
||||
@@ -453,11 +427,8 @@ Eine Verknüpfungsbeziehung zwischen zwei Jira-Issues erstellen
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Zeitstempel der Operation |
|
||||
| `inwardIssue` | string | Key des eingehenden Issues |
|
||||
| `outwardIssue` | string | Key des ausgehenden Issues |
|
||||
| `linkType` | string | Art der Issue-Verknüpfung |
|
||||
| `linkId` | string | ID der erstellten Verknüpfung |
|
||||
| `success` | boolean | Erfolgsstatus der Operation |
|
||||
| `output` | object | Issue-Verknüpfungsdetails mit Zeitstempel, eingehendem Issue-Key, ausgehendem Issue-Key, Verknüpfungstyp und Erfolgsstatus |
|
||||
|
||||
### `jira_delete_issue_link`
|
||||
|
||||
@@ -475,8 +446,8 @@ Eine Verknüpfung zwischen zwei Jira-Issues löschen
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Zeitstempel der Operation |
|
||||
| `linkId` | string | ID der gelöschten Verknüpfung |
|
||||
| `success` | boolean | Erfolgsstatus der Operation |
|
||||
| `output` | object | Löschdetails mit Zeitstempel, Link-ID und Erfolgsstatus |
|
||||
|
||||
### `jira_add_watcher`
|
||||
|
||||
@@ -495,9 +466,8 @@ Einen Beobachter zu einem Jira-Issue hinzufügen, um Benachrichtigungen über Ak
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Zeitstempel der Operation |
|
||||
| `issueKey` | string | Issue-Key |
|
||||
| `watcherAccountId` | string | Account-ID des hinzugefügten Beobachters |
|
||||
| `success` | boolean | Erfolgsstatus der Operation |
|
||||
| `output` | object | Beobachterdetails mit Zeitstempel, Issue-Key, Beobachter-Account-ID und Erfolgsstatus |
|
||||
|
||||
### `jira_remove_watcher`
|
||||
|
||||
@@ -516,9 +486,8 @@ Einen Beobachter von einem Jira-Issue entfernen
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Zeitstempel der Operation |
|
||||
| `issueKey` | string | Issue-Key |
|
||||
| `watcherAccountId` | string | Account-ID des entfernten Beobachters |
|
||||
| `success` | boolean | Erfolgsstatus der Operation |
|
||||
| `output` | object | Entfernungsdetails mit Zeitstempel, Issue-Key, Beobachter-Konto-ID und Erfolgsstatus |
|
||||
|
||||
## Hinweise
|
||||
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
---
|
||||
title: ServiceNow
|
||||
description: Erstellen, lesen, aktualisieren, löschen und Massenimport von
|
||||
ServiceNow-Datensätzen
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="servicenow"
|
||||
color="#032D42"
|
||||
/>
|
||||
|
||||
## Nutzungsanleitung
|
||||
|
||||
Integrieren Sie ServiceNow in Ihren Workflow. Kann Datensätze in jeder ServiceNow-Tabelle erstellen, lesen, aktualisieren und löschen (Vorfälle, Aufgaben, Benutzer usw.). Unterstützt Massenimport-Operationen für Datenmigration und ETL.
|
||||
|
||||
## Tools
|
||||
|
||||
### `servicenow_create_record`
|
||||
|
||||
Erstellen eines neuen Datensatzes in einer ServiceNow-Tabelle
|
||||
|
||||
#### Eingabe
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `instanceUrl` | string | Ja | ServiceNow-Instanz-URL \(z. B. https://instance.service-now.com\) |
|
||||
| `credential` | string | Nein | ServiceNow OAuth-Anmeldeinformations-ID |
|
||||
| `tableName` | string | Ja | Tabellenname \(z. B. incident, task, sys_user\) |
|
||||
| `fields` | json | Ja | Felder, die für den Datensatz festgelegt werden sollen \(JSON-Objekt\) |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `record` | json | Erstellter ServiceNow-Datensatz mit sys_id und anderen Feldern |
|
||||
| `metadata` | json | Metadaten der Operation |
|
||||
|
||||
### `servicenow_read_record`
|
||||
|
||||
Lesen von Datensätzen aus einer ServiceNow-Tabelle
|
||||
|
||||
#### Eingabe
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `instanceUrl` | string | Nein | ServiceNow-Instanz-URL \(automatisch aus OAuth erkannt, falls nicht angegeben\) |
|
||||
| `credential` | string | Nein | ServiceNow OAuth-Anmeldeinformations-ID |
|
||||
| `tableName` | string | Ja | Tabellenname |
|
||||
| `sysId` | string | Nein | Spezifische Datensatz-sys_id |
|
||||
| `number` | string | Nein | Datensatznummer \(z. B. INC0010001\) |
|
||||
| `query` | string | Nein | Kodierte Abfragezeichenfolge \(z. B. "active=true^priority=1"\) |
|
||||
| `limit` | number | Nein | Maximale Anzahl der zurückzugebenden Datensätze |
|
||||
| `fields` | string | Nein | Durch Kommas getrennte Liste der zurückzugebenden Felder |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `records` | array | Array von ServiceNow-Datensätzen |
|
||||
| `metadata` | json | Metadaten der Operation |
|
||||
|
||||
### `servicenow_update_record`
|
||||
|
||||
Einen bestehenden Datensatz in einer ServiceNow-Tabelle aktualisieren
|
||||
|
||||
#### Eingabe
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `instanceUrl` | string | Nein | ServiceNow-Instanz-URL \(wird automatisch aus OAuth erkannt, falls nicht angegeben\) |
|
||||
| `credential` | string | Nein | ServiceNow-OAuth-Credential-ID |
|
||||
| `tableName` | string | Ja | Tabellenname |
|
||||
| `sysId` | string | Ja | Sys_id des zu aktualisierenden Datensatzes |
|
||||
| `fields` | json | Ja | Zu aktualisierende Felder \(JSON-Objekt\) |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `record` | json | Aktualisierter ServiceNow-Datensatz |
|
||||
| `metadata` | json | Metadaten der Operation |
|
||||
|
||||
### `servicenow_delete_record`
|
||||
|
||||
Einen Datensatz aus einer ServiceNow-Tabelle löschen
|
||||
|
||||
#### Eingabe
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `instanceUrl` | string | Nein | ServiceNow-Instanz-URL \(wird automatisch aus OAuth erkannt, falls nicht angegeben\) |
|
||||
| `credential` | string | Nein | ServiceNow-OAuth-Credential-ID |
|
||||
| `tableName` | string | Ja | Tabellenname |
|
||||
| `sysId` | string | Ja | Sys_id des zu löschenden Datensatzes |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Ob das Löschen erfolgreich war |
|
||||
| `metadata` | json | Metadaten der Operation |
|
||||
|
||||
## Hinweise
|
||||
|
||||
- Kategorie: `tools`
|
||||
- Typ: `servicenow`
|
||||
@@ -54,7 +54,7 @@ Integriert Slack in den Workflow. Kann Nachrichten senden, aktualisieren und lö
|
||||
|
||||
### `slack_message`
|
||||
|
||||
Sende Nachrichten an Slack-Kanäle oder Direktnachrichten. Unterstützt Slack mrkdwn-Formatierung.
|
||||
Senden Sie Nachrichten an Slack-Kanäle oder Benutzer über die Slack-API. Unterstützt Slack mrkdwn-Formatierung.
|
||||
|
||||
#### Eingabe
|
||||
|
||||
@@ -62,9 +62,8 @@ Sende Nachrichten an Slack-Kanäle oder Direktnachrichten. Unterstützt Slack mr
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `authMethod` | string | Nein | Authentifizierungsmethode: oauth oder bot_token |
|
||||
| `botToken` | string | Nein | Bot-Token für benutzerdefinierten Bot |
|
||||
| `channel` | string | Nein | Ziel-Slack-Kanal \(z.B. #general\) |
|
||||
| `userId` | string | Nein | Ziel-Slack-Benutzer-ID für Direktnachrichten \(z.B. U1234567890\) |
|
||||
| `text` | string | Ja | Zu sendender Nachrichtentext \(unterstützt Slack mrkdwn-Formatierung\) |
|
||||
| `channel` | string | Ja | Ziel-Slack-Kanal \(z.B. #general\) |
|
||||
| `text` | string | Ja | Nachrichtentext zum Senden \(unterstützt Slack mrkdwn-Formatierung\) |
|
||||
| `thread_ts` | string | Nein | Thread-Zeitstempel für Antworten \(erstellt Thread-Antwort\) |
|
||||
| `files` | file[] | Nein | Dateien, die an die Nachricht angehängt werden sollen |
|
||||
|
||||
@@ -110,11 +109,10 @@ Lesen Sie die neuesten Nachrichten aus Slack-Kanälen. Rufen Sie den Konversatio
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `authMethod` | string | Nein | Authentifizierungsmethode: oauth oder bot_token |
|
||||
| `botToken` | string | Nein | Bot-Token für benutzerdefinierten Bot |
|
||||
| `channel` | string | Nein | Slack-Kanal, aus dem Nachrichten gelesen werden sollen \(z.B. #general\) |
|
||||
| `userId` | string | Nein | Benutzer-ID für DM-Konversation \(z.B. U1234567890\) |
|
||||
| `limit` | number | Nein | Anzahl der abzurufenden Nachrichten \(Standard: 10, max: 100\) |
|
||||
| `oldest` | string | Nein | Beginn des Zeitraums \(Zeitstempel\) |
|
||||
| `latest` | string | Nein | Ende des Zeitraums \(Zeitstempel\) |
|
||||
| `channel` | string | Ja | Slack-Kanal, aus dem Nachrichten gelesen werden sollen (z.B. #general) |
|
||||
| `limit` | number | Nein | Anzahl der abzurufenden Nachrichten (Standard: 10, max: 100) |
|
||||
| `oldest` | string | Nein | Beginn des Zeitraums (Zeitstempel) |
|
||||
| `latest` | string | Nein | Ende des Zeitraums (Zeitstempel) |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
|
||||
@@ -27,14 +27,12 @@ In Sim ermöglicht die Zoom-Integration Ihren Agenten die Automatisierung der Pl
|
||||
- Details oder Einladungen für jedes Meeting abzurufen
|
||||
- Bestehende Meetings direkt aus Ihren Automatisierungen zu aktualisieren oder zu löschen
|
||||
|
||||
Um eine Verbindung zu Zoom herzustellen, fügen Sie den Zoom-Block ein und klicken Sie auf `Connect`, um sich mit Ihrem Zoom-Konto zu authentifizieren. Nach der Verbindung können Sie die Zoom-Tools verwenden, um Zoom-Meetings zu erstellen, aufzulisten, zu aktualisieren und zu löschen. Sie können Ihr Zoom-Konto jederzeit trennen, indem Sie unter Einstellungen > Integrationen auf `Disconnect` klicken, und der Zugriff auf Ihr Zoom-Konto wird sofort widerrufen.
|
||||
|
||||
Diese Funktionen ermöglichen es Ihnen, die Zusammenarbeit aus der Ferne zu optimieren, wiederkehrende Videositzungen zu automatisieren und die Zoom-Umgebung Ihrer Organisation als Teil Ihrer Workflows zu verwalten.
|
||||
Diese Funktionen ermöglichen es Ihnen, die Remote-Zusammenarbeit zu optimieren, wiederkehrende Videositzungen zu automatisieren und die Zoom-Umgebung Ihrer Organisation als Teil Ihrer Workflows zu verwalten.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
## Gebrauchsanweisung
|
||||
## Nutzungsanleitung
|
||||
|
||||
Integrieren Sie Zoom in Workflows. Erstellen, listen, aktualisieren und löschen Sie Zoom-Meetings. Erhalten Sie Meeting-Details, Einladungen, Aufzeichnungen und Teilnehmerinformationen. Verwalten Sie Cloud-Aufzeichnungen programmatisch.
|
||||
Integrieren Sie Zoom in Workflows. Erstellen, listen, aktualisieren und löschen Sie Zoom-Meetings. Rufen Sie Meeting-Details, Einladungen, Aufzeichnungen und Teilnehmer ab. Verwalten Sie Cloud-Aufzeichnungen programmgesteuert.
|
||||
|
||||
## Tools
|
||||
|
||||
@@ -51,7 +49,7 @@ Ein neues Zoom-Meeting erstellen
|
||||
| `type` | number | Nein | Meeting-Typ: 1=sofort, 2=geplant, 3=wiederkehrend ohne feste Zeit, 8=wiederkehrend mit fester Zeit |
|
||||
| `startTime` | string | Nein | Meeting-Startzeit im ISO 8601-Format \(z.B. 2025-06-03T10:00:00Z\) |
|
||||
| `duration` | number | Nein | Meeting-Dauer in Minuten |
|
||||
| `timezone` | string | Nein | Zeitzone für das Meeting \(z.B. America/Los_Angeles\) |
|
||||
| `timezone` | string | Nein | Zeitzone für das Meeting \(z.B. Europe/Berlin\) |
|
||||
| `password` | string | Nein | Meeting-Passwort |
|
||||
| `agenda` | string | Nein | Meeting-Agenda |
|
||||
| `hostVideo` | boolean | Nein | Mit eingeschaltetem Host-Video starten |
|
||||
@@ -61,7 +59,7 @@ Ein neues Zoom-Meeting erstellen
|
||||
| `waitingRoom` | boolean | Nein | Warteraum aktivieren |
|
||||
| `autoRecording` | string | Nein | Automatische Aufzeichnungseinstellung: local, cloud oder none |
|
||||
|
||||
#### Ausgabe
|
||||
#### Output
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
@@ -69,18 +67,18 @@ Ein neues Zoom-Meeting erstellen
|
||||
|
||||
### `zoom_list_meetings`
|
||||
|
||||
Alle Meetings für einen Zoom-Benutzer auflisten
|
||||
Alle Meetings eines Zoom-Benutzers auflisten
|
||||
|
||||
#### Eingabe
|
||||
#### Input
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `userId` | string | Ja | Die Benutzer-ID oder E-Mail-Adresse. Verwenden Sie "me" für den authentifizierten Benutzer. |
|
||||
| `type` | string | Nein | Meeting-Typ-Filter: scheduled, live, upcoming, upcoming_meetings oder previous_meetings |
|
||||
| `pageSize` | number | Nein | Anzahl der Datensätze pro Seite \(max. 300\) |
|
||||
| `pageSize` | number | Nein | Anzahl der Datensätze pro Seite (max. 300) |
|
||||
| `nextPageToken` | string | Nein | Token für Paginierung, um die nächste Ergebnisseite zu erhalten |
|
||||
|
||||
#### Ausgabe
|
||||
#### Output
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
@@ -91,7 +89,7 @@ Alle Meetings für einen Zoom-Benutzer auflisten
|
||||
|
||||
Details eines bestimmten Zoom-Meetings abrufen
|
||||
|
||||
#### Eingabe
|
||||
#### Input
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
@@ -99,13 +97,13 @@ Details eines bestimmten Zoom-Meetings abrufen
|
||||
| `occurrenceId` | string | Nein | Vorkommnis-ID für wiederkehrende Meetings |
|
||||
| `showPreviousOccurrences` | boolean | Nein | Frühere Vorkommnisse für wiederkehrende Meetings anzeigen |
|
||||
|
||||
#### Ausgabe
|
||||
#### Output
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `meeting` | object | Die Meeting-Details |
|
||||
|
||||
Details eines bestimmten Zoom-Meetings abrufen
|
||||
### `zoom_update_meeting`
|
||||
|
||||
Ein bestehendes Zoom-Meeting aktualisieren
|
||||
|
||||
@@ -118,11 +116,11 @@ Ein bestehendes Zoom-Meeting aktualisieren
|
||||
| `type` | number | Nein | Meeting-Typ: 1=sofort, 2=geplant, 3=wiederkehrend ohne feste Zeit, 8=wiederkehrend mit fester Zeit |
|
||||
| `startTime` | string | Nein | Meeting-Startzeit im ISO 8601-Format \(z.B. 2025-06-03T10:00:00Z\) |
|
||||
| `duration` | number | Nein | Meeting-Dauer in Minuten |
|
||||
| `timezone` | string | Nein | Zeitzone für das Meeting \(z.B. Europe/Berlin\) |
|
||||
| `timezone` | string | Nein | Zeitzone für das Meeting \(z.B. America/Los_Angeles\) |
|
||||
| `password` | string | Nein | Meeting-Passwort |
|
||||
| `agenda` | string | Nein | Meeting-Agenda |
|
||||
| `hostVideo` | boolean | Nein | Mit eingeschaltetem Host-Video starten |
|
||||
| `participantVideo` | boolean | Nein | Mit eingeschaltetem Teilnehmer-Video starten |
|
||||
| `hostVideo` | boolean | Nein | Mit eingeschalteter Host-Kamera starten |
|
||||
| `participantVideo` | boolean | Nein | Mit eingeschalteter Teilnehmer-Kamera starten |
|
||||
| `joinBeforeHost` | boolean | Nein | Teilnehmern erlauben, vor dem Host beizutreten |
|
||||
| `muteUponEntry` | boolean | Nein | Teilnehmer beim Betreten stummschalten |
|
||||
| `waitingRoom` | boolean | Nein | Warteraum aktivieren |
|
||||
@@ -134,7 +132,7 @@ Ein bestehendes Zoom-Meeting aktualisieren
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Ob das Meeting erfolgreich aktualisiert wurde |
|
||||
|
||||
Ein Zoom-Meeting löschen
|
||||
### `zoom_delete_meeting`
|
||||
|
||||
Ein Zoom-Meeting löschen oder abbrechen
|
||||
|
||||
@@ -143,9 +141,9 @@ Ein Zoom-Meeting löschen oder abbrechen
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `meetingId` | string | Ja | Die zu löschende Meeting-ID |
|
||||
| `occurrenceId` | string | Nein | Occurrence-ID zum Löschen eines bestimmten Termins eines wiederkehrenden Meetings |
|
||||
| `occurrenceId` | string | Nein | Vorkommnis-ID zum Löschen eines bestimmten Vorkommnisses eines wiederkehrenden Meetings |
|
||||
| `scheduleForReminder` | boolean | Nein | Erinnerungs-E-Mail zur Stornierung an Teilnehmer senden |
|
||||
| `cancelMeetingReminder` | boolean | Nein | Stornierungs-E-Mail an Teilnehmer und alternative Hosts senden |
|
||||
| `cancelMeetingReminder` | boolean | Nein | Stornierungsmail an Teilnehmer und alternative Hosts senden |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
@@ -155,7 +153,7 @@ Ein Zoom-Meeting löschen oder abbrechen
|
||||
|
||||
### `zoom_get_meeting_invitation`
|
||||
|
||||
Den Einladungstext für ein Zoom-Meeting abrufen
|
||||
Abrufen des Einladungstextes für ein Zoom-Meeting
|
||||
|
||||
#### Eingabe
|
||||
|
||||
@@ -167,20 +165,20 @@ Den Einladungstext für ein Zoom-Meeting abrufen
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `invitation` | string | Der Meeting-Einladungstext |
|
||||
| `invitation` | string | Der Einladungstext für das Meeting |
|
||||
|
||||
### `zoom_list_recordings`
|
||||
|
||||
Alle Cloud-Aufzeichnungen eines Zoom-Benutzers auflisten
|
||||
Alle Cloud-Aufzeichnungen für einen Zoom-Benutzer auflisten
|
||||
|
||||
#### Eingabe
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `userId` | string | Ja | Die Benutzer-ID oder E-Mail-Adresse. Verwenden Sie "me" für den authentifizierten Benutzer. |
|
||||
| `from` | string | Nein | Startdatum im Format yyyy-mm-dd (innerhalb der letzten 6 Monate) |
|
||||
| `to` | string | Nein | Enddatum im Format yyyy-mm-dd |
|
||||
| `pageSize` | number | Nein | Anzahl der Datensätze pro Seite (max. 300) |
|
||||
| `from` | string | Nein | Startdatum im Format jjjj-mm-tt \(innerhalb der letzten 6 Monate\) |
|
||||
| `to` | string | Nein | Enddatum im Format jjjj-mm-tt |
|
||||
| `pageSize` | number | Nein | Anzahl der Datensätze pro Seite \(max. 300\) |
|
||||
| `nextPageToken` | string | Nein | Token für die Paginierung, um die nächste Ergebnisseite zu erhalten |
|
||||
| `trash` | boolean | Nein | Auf true setzen, um Aufzeichnungen aus dem Papierkorb aufzulisten |
|
||||
|
||||
@@ -191,7 +189,7 @@ Alle Cloud-Aufzeichnungen eines Zoom-Benutzers auflisten
|
||||
| `recordings` | array | Liste der Aufzeichnungen |
|
||||
| `pageInfo` | object | Paginierungsinformationen |
|
||||
|
||||
Alle Aufzeichnungen für ein bestimmtes Zoom-Meeting abrufen
|
||||
### `zoom_get_meeting_recordings`
|
||||
|
||||
Alle Aufzeichnungen für ein bestimmtes Zoom-Meeting abrufen
|
||||
|
||||
@@ -200,8 +198,8 @@ Alle Aufzeichnungen für ein bestimmtes Zoom-Meeting abrufen
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `meetingId` | string | Ja | Die Meeting-ID oder Meeting-UUID |
|
||||
| `includeFolderItems` | boolean | Nein | Elemente innerhalb eines Ordners einbeziehen |
|
||||
| `ttl` | number | Nein | Gültigkeitsdauer für Download-URLs in Sekunden \(max. 604800\) |
|
||||
| `includeFolderItems` | boolean | Nein | Elemente innerhalb eines Ordners einschließen |
|
||||
| `ttl` | number | Nein | Gültigkeitsdauer für Download-URLs in Sekunden \(max 604800\) |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
@@ -209,7 +207,7 @@ Alle Aufzeichnungen für ein bestimmtes Zoom-Meeting abrufen
|
||||
| --------- | ---- | ----------- |
|
||||
| `recording` | object | Die Meeting-Aufzeichnung mit allen Dateien |
|
||||
|
||||
Cloud-Aufzeichnungen für ein Zoom-Meeting löschen
|
||||
### `zoom_delete_recording`
|
||||
|
||||
Cloud-Aufzeichnungen für ein Zoom-Meeting löschen
|
||||
|
||||
@@ -227,7 +225,7 @@ Cloud-Aufzeichnungen für ein Zoom-Meeting löschen
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Ob die Aufzeichnung erfolgreich gelöscht wurde |
|
||||
|
||||
Teilnehmer eines vergangenen Zoom-Meetings auflisten
|
||||
### `zoom_list_past_participants`
|
||||
|
||||
Teilnehmer eines vergangenen Zoom-Meetings auflisten
|
||||
|
||||
@@ -236,14 +234,14 @@ Teilnehmer eines vergangenen Zoom-Meetings auflisten
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `meetingId` | string | Ja | Die vergangene Meeting-ID oder UUID |
|
||||
| `pageSize` | number | Nein | Anzahl der Datensätze pro Seite \(max. 300\) |
|
||||
| `pageSize` | number | Nein | Anzahl der Datensätze pro Seite \(max 300\) |
|
||||
| `nextPageToken` | string | Nein | Token für Paginierung, um die nächste Ergebnisseite zu erhalten |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
| Parameter | Typ | Beschreibung |
|
||||
| --------- | ---- | ----------- |
|
||||
| `participants` | array | Liste der Meeting-Teilnehmer |
|
||||
| `participants` | array | Liste der Besprechungsteilnehmer |
|
||||
| `pageInfo` | object | Paginierungsinformationen |
|
||||
|
||||
## Hinweise
|
||||
|
||||
@@ -51,13 +51,8 @@ Retrieve detailed information about a specific Jira issue
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Timestamp of the operation |
|
||||
| `issueKey` | string | Issue key \(e.g., PROJ-123\) |
|
||||
| `summary` | string | Issue summary |
|
||||
| `description` | json | Issue description content |
|
||||
| `created` | string | Issue creation timestamp |
|
||||
| `updated` | string | Issue last updated timestamp |
|
||||
| `issue` | json | Complete issue object with all fields |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Jira issue details with issue key, summary, description, created and updated timestamps |
|
||||
|
||||
### `jira_update`
|
||||
|
||||
@@ -81,9 +76,8 @@ Update a Jira issue
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Timestamp of the operation |
|
||||
| `issueKey` | string | Updated issue key \(e.g., PROJ-123\) |
|
||||
| `summary` | string | Issue summary after update |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Updated Jira issue details with timestamp, issue key, summary, and success status |
|
||||
|
||||
### `jira_write`
|
||||
|
||||
@@ -106,10 +100,8 @@ Write a Jira issue
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Timestamp of the operation |
|
||||
| `issueKey` | string | Created issue key \(e.g., PROJ-123\) |
|
||||
| `summary` | string | Issue summary |
|
||||
| `url` | string | URL to the created issue |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Created Jira issue details with timestamp, issue key, summary, success status, and URL |
|
||||
|
||||
### `jira_bulk_read`
|
||||
|
||||
@@ -127,7 +119,8 @@ Retrieve multiple Jira issues in bulk
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `issues` | array | Array of Jira issues with ts, summary, description, created, and updated timestamps |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | array | Array of Jira issues with summary, description, created and updated timestamps |
|
||||
|
||||
### `jira_delete_issue`
|
||||
|
||||
@@ -146,8 +139,8 @@ Delete a Jira issue
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Timestamp of the operation |
|
||||
| `issueKey` | string | Deleted issue key |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Deleted issue details with timestamp, issue key, and success status |
|
||||
|
||||
### `jira_assign_issue`
|
||||
|
||||
@@ -166,9 +159,8 @@ Assign a Jira issue to a user
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Timestamp of the operation |
|
||||
| `issueKey` | string | Issue key that was assigned |
|
||||
| `assigneeId` | string | Account ID of the assignee |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Assignment details with timestamp, issue key, assignee ID, and success status |
|
||||
|
||||
### `jira_transition_issue`
|
||||
|
||||
@@ -188,9 +180,8 @@ Move a Jira issue between workflow statuses (e.g., To Do -> In Progress)
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Timestamp of the operation |
|
||||
| `issueKey` | string | Issue key that was transitioned |
|
||||
| `transitionId` | string | Applied transition ID |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Transition details with timestamp, issue key, transition ID, and success status |
|
||||
|
||||
### `jira_search_issues`
|
||||
|
||||
@@ -211,11 +202,8 @@ Search for Jira issues using JQL (Jira Query Language)
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Timestamp of the operation |
|
||||
| `total` | number | Total number of matching issues |
|
||||
| `startAt` | number | Pagination start index |
|
||||
| `maxResults` | number | Maximum results per page |
|
||||
| `issues` | array | Array of matching issues with key, summary, status, assignee, created, updated |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Search results with timestamp, total count, pagination details, and array of matching issues |
|
||||
|
||||
### `jira_add_comment`
|
||||
|
||||
@@ -234,10 +222,8 @@ Add a comment to a Jira issue
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Timestamp of the operation |
|
||||
| `issueKey` | string | Issue key the comment was added to |
|
||||
| `commentId` | string | Created comment ID |
|
||||
| `body` | string | Comment text content |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Comment details with timestamp, issue key, comment ID, body, and success status |
|
||||
|
||||
### `jira_get_comments`
|
||||
|
||||
@@ -257,10 +243,8 @@ Get all comments from a Jira issue
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Timestamp of the operation |
|
||||
| `issueKey` | string | Issue key |
|
||||
| `total` | number | Total number of comments |
|
||||
| `comments` | array | Array of comments with id, author, body, created, updated |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Comments data with timestamp, issue key, total count, and array of comments |
|
||||
|
||||
### `jira_update_comment`
|
||||
|
||||
@@ -280,10 +264,8 @@ Update an existing comment on a Jira issue
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Timestamp of the operation |
|
||||
| `issueKey` | string | Issue key |
|
||||
| `commentId` | string | Updated comment ID |
|
||||
| `body` | string | Updated comment text |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Updated comment details with timestamp, issue key, comment ID, body text, and success status |
|
||||
|
||||
### `jira_delete_comment`
|
||||
|
||||
@@ -302,9 +284,8 @@ Delete a comment from a Jira issue
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Timestamp of the operation |
|
||||
| `issueKey` | string | Issue key |
|
||||
| `commentId` | string | Deleted comment ID |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Deletion details with timestamp, issue key, comment ID, and success status |
|
||||
|
||||
### `jira_get_attachments`
|
||||
|
||||
@@ -322,9 +303,8 @@ Get all attachments from a Jira issue
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Timestamp of the operation |
|
||||
| `issueKey` | string | Issue key |
|
||||
| `attachments` | array | Array of attachments with id, filename, size, mimeType, created, author |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Attachments data with timestamp, issue key, and array of attachments |
|
||||
|
||||
### `jira_delete_attachment`
|
||||
|
||||
@@ -342,8 +322,8 @@ Delete an attachment from a Jira issue
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Timestamp of the operation |
|
||||
| `attachmentId` | string | Deleted attachment ID |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Deletion details with timestamp, attachment ID, and success status |
|
||||
|
||||
### `jira_add_worklog`
|
||||
|
||||
@@ -364,10 +344,8 @@ Add a time tracking worklog entry to a Jira issue
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Timestamp of the operation |
|
||||
| `issueKey` | string | Issue key the worklog was added to |
|
||||
| `worklogId` | string | Created worklog ID |
|
||||
| `timeSpentSeconds` | number | Time spent in seconds |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Worklog details with timestamp, issue key, worklog ID, time spent in seconds, and success status |
|
||||
|
||||
### `jira_get_worklogs`
|
||||
|
||||
@@ -387,10 +365,8 @@ Get all worklog entries from a Jira issue
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Timestamp of the operation |
|
||||
| `issueKey` | string | Issue key |
|
||||
| `total` | number | Total number of worklogs |
|
||||
| `worklogs` | array | Array of worklogs with id, author, timeSpentSeconds, timeSpent, comment, created, updated, started |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Worklogs data with timestamp, issue key, total count, and array of worklogs |
|
||||
|
||||
### `jira_update_worklog`
|
||||
|
||||
@@ -412,9 +388,8 @@ Update an existing worklog entry on a Jira issue
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Timestamp of the operation |
|
||||
| `issueKey` | string | Issue key |
|
||||
| `worklogId` | string | Updated worklog ID |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Worklog update details with timestamp, issue key, worklog ID, and success status |
|
||||
|
||||
### `jira_delete_worklog`
|
||||
|
||||
@@ -433,9 +408,8 @@ Delete a worklog entry from a Jira issue
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Timestamp of the operation |
|
||||
| `issueKey` | string | Issue key |
|
||||
| `worklogId` | string | Deleted worklog ID |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Deletion details with timestamp, issue key, worklog ID, and success status |
|
||||
|
||||
### `jira_create_issue_link`
|
||||
|
||||
@@ -456,11 +430,8 @@ Create a link relationship between two Jira issues
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Timestamp of the operation |
|
||||
| `inwardIssue` | string | Inward issue key |
|
||||
| `outwardIssue` | string | Outward issue key |
|
||||
| `linkType` | string | Type of issue link |
|
||||
| `linkId` | string | Created link ID |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Issue link details with timestamp, inward issue key, outward issue key, link type, and success status |
|
||||
|
||||
### `jira_delete_issue_link`
|
||||
|
||||
@@ -478,8 +449,8 @@ Delete a link between two Jira issues
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Timestamp of the operation |
|
||||
| `linkId` | string | Deleted link ID |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Deletion details with timestamp, link ID, and success status |
|
||||
|
||||
### `jira_add_watcher`
|
||||
|
||||
@@ -498,9 +469,8 @@ Add a watcher to a Jira issue to receive notifications about updates
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Timestamp of the operation |
|
||||
| `issueKey` | string | Issue key |
|
||||
| `watcherAccountId` | string | Added watcher account ID |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Watcher details with timestamp, issue key, watcher account ID, and success status |
|
||||
|
||||
### `jira_remove_watcher`
|
||||
|
||||
@@ -519,9 +489,8 @@ Remove a watcher from a Jira issue
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Timestamp of the operation |
|
||||
| `issueKey` | string | Issue key |
|
||||
| `watcherAccountId` | string | Removed watcher account ID |
|
||||
| `success` | boolean | Operation success status |
|
||||
| `output` | object | Removal details with timestamp, issue key, watcher account ID, and success status |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -80,7 +80,6 @@
|
||||
"sendgrid",
|
||||
"sentry",
|
||||
"serper",
|
||||
"servicenow",
|
||||
"sftp",
|
||||
"sharepoint",
|
||||
"shopify",
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
---
|
||||
title: ServiceNow
|
||||
description: Create, read, update, delete, and bulk import ServiceNow records
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="servicenow"
|
||||
color="#032D42"
|
||||
/>
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate ServiceNow into your workflow. Can create, read, update, and delete records in any ServiceNow table (incidents, tasks, users, etc.). Supports bulk import operations for data migration and ETL.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `servicenow_create_record`
|
||||
|
||||
Create a new record in a ServiceNow table
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `instanceUrl` | string | Yes | ServiceNow instance URL \(e.g., https://instance.service-now.com\) |
|
||||
| `credential` | string | No | ServiceNow OAuth credential ID |
|
||||
| `tableName` | string | Yes | Table name \(e.g., incident, task, sys_user\) |
|
||||
| `fields` | json | Yes | Fields to set on the record \(JSON object\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `record` | json | Created ServiceNow record with sys_id and other fields |
|
||||
| `metadata` | json | Operation metadata |
|
||||
|
||||
### `servicenow_read_record`
|
||||
|
||||
Read records from a ServiceNow table
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `instanceUrl` | string | No | ServiceNow instance URL \(auto-detected from OAuth if not provided\) |
|
||||
| `credential` | string | No | ServiceNow OAuth credential ID |
|
||||
| `tableName` | string | Yes | Table name |
|
||||
| `sysId` | string | No | Specific record sys_id |
|
||||
| `number` | string | No | Record number \(e.g., INC0010001\) |
|
||||
| `query` | string | No | Encoded query string \(e.g., "active=true^priority=1"\) |
|
||||
| `limit` | number | No | Maximum number of records to return |
|
||||
| `fields` | string | No | Comma-separated list of fields to return |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `records` | array | Array of ServiceNow records |
|
||||
| `metadata` | json | Operation metadata |
|
||||
|
||||
### `servicenow_update_record`
|
||||
|
||||
Update an existing record in a ServiceNow table
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `instanceUrl` | string | No | ServiceNow instance URL \(auto-detected from OAuth if not provided\) |
|
||||
| `credential` | string | No | ServiceNow OAuth credential ID |
|
||||
| `tableName` | string | Yes | Table name |
|
||||
| `sysId` | string | Yes | Record sys_id to update |
|
||||
| `fields` | json | Yes | Fields to update \(JSON object\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `record` | json | Updated ServiceNow record |
|
||||
| `metadata` | json | Operation metadata |
|
||||
|
||||
### `servicenow_delete_record`
|
||||
|
||||
Delete a record from a ServiceNow table
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `instanceUrl` | string | No | ServiceNow instance URL \(auto-detected from OAuth if not provided\) |
|
||||
| `credential` | string | No | ServiceNow OAuth credential ID |
|
||||
| `tableName` | string | Yes | Table name |
|
||||
| `sysId` | string | Yes | Record sys_id to delete |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Whether the deletion was successful |
|
||||
| `metadata` | json | Operation metadata |
|
||||
|
||||
|
||||
|
||||
## Notes
|
||||
|
||||
- Category: `tools`
|
||||
- Type: `servicenow`
|
||||
@@ -56,7 +56,7 @@ Integrate Slack into the workflow. Can send, update, and delete messages, create
|
||||
|
||||
### `slack_message`
|
||||
|
||||
Send messages to Slack channels or direct messages. Supports Slack mrkdwn formatting.
|
||||
Send messages to Slack channels or users through the Slack API. Supports Slack mrkdwn formatting.
|
||||
|
||||
#### Input
|
||||
|
||||
@@ -64,8 +64,7 @@ Send messages to Slack channels or direct messages. Supports Slack mrkdwn format
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `authMethod` | string | No | Authentication method: oauth or bot_token |
|
||||
| `botToken` | string | No | Bot token for Custom Bot |
|
||||
| `channel` | string | No | Target Slack channel \(e.g., #general\) |
|
||||
| `userId` | string | No | Target Slack user ID for direct messages \(e.g., U1234567890\) |
|
||||
| `channel` | string | Yes | Target Slack channel \(e.g., #general\) |
|
||||
| `text` | string | Yes | Message text to send \(supports Slack mrkdwn formatting\) |
|
||||
| `thread_ts` | string | No | Thread timestamp to reply to \(creates thread reply\) |
|
||||
| `files` | file[] | No | Files to attach to the message |
|
||||
@@ -112,8 +111,7 @@ Read the latest messages from Slack channels. Retrieve conversation history with
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `authMethod` | string | No | Authentication method: oauth or bot_token |
|
||||
| `botToken` | string | No | Bot token for Custom Bot |
|
||||
| `channel` | string | No | Slack channel to read messages from \(e.g., #general\) |
|
||||
| `userId` | string | No | User ID for DM conversation \(e.g., U1234567890\) |
|
||||
| `channel` | string | Yes | Slack channel to read messages from \(e.g., #general\) |
|
||||
| `limit` | number | No | Number of messages to retrieve \(default: 10, max: 100\) |
|
||||
| `oldest` | string | No | Start of time range \(timestamp\) |
|
||||
| `latest` | string | No | End of time range \(timestamp\) |
|
||||
|
||||
@@ -27,8 +27,6 @@ In Sim, the Zoom integration empowers your agents to automate scheduling and mee
|
||||
- Retrieve details or invitations for any meeting
|
||||
- Update or delete existing meetings directly from your automations
|
||||
|
||||
To connect to Zoom, drop the Zoom block and click `Connect` to authenticate with your Zoom account. Once connected, you can use the Zoom tools to create, list, update, and delete Zoom meetings. At any given time, you can disconnect your Zoom account by clicking `Disconnect` in Settings > Integrations, and access to your Zoom account will be revoked immediatley.
|
||||
|
||||
These capabilities let you streamline remote collaboration, automate recurring video sessions, and manage your organization's Zoom environment all as part of your workflows.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
@@ -48,13 +48,8 @@ Recupera información detallada sobre una incidencia específica de Jira
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Marca de tiempo de la operación |
|
||||
| `issueKey` | string | Clave de la incidencia (p. ej., PROJ-123) |
|
||||
| `summary` | string | Resumen de la incidencia |
|
||||
| `description` | json | Contenido de la descripción de la incidencia |
|
||||
| `created` | string | Marca de tiempo de creación de la incidencia |
|
||||
| `updated` | string | Marca de tiempo de última actualización de la incidencia |
|
||||
| `issue` | json | Objeto completo de la incidencia con todos los campos |
|
||||
| `success` | boolean | Estado de éxito de la operación |
|
||||
| `output` | object | Detalles de la incidencia de Jira con clave de incidencia, resumen, descripción, marcas de tiempo de creación y actualización |
|
||||
|
||||
### `jira_update`
|
||||
|
||||
@@ -78,9 +73,8 @@ Actualizar una incidencia de Jira
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Marca de tiempo de la operación |
|
||||
| `issueKey` | string | Clave de la incidencia actualizada (p. ej., PROJ-123) |
|
||||
| `summary` | string | Resumen de la incidencia después de la actualización |
|
||||
| `success` | boolean | Estado de éxito de la operación |
|
||||
| `output` | object | Detalles actualizados de la incidencia de Jira con marca de tiempo, clave de incidencia, resumen y estado de éxito |
|
||||
|
||||
### `jira_write`
|
||||
|
||||
@@ -103,10 +97,8 @@ Escribir una incidencia de Jira
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Marca de tiempo de la operación |
|
||||
| `issueKey` | string | Clave de la incidencia creada (p. ej., PROJ-123) |
|
||||
| `summary` | string | Resumen de la incidencia |
|
||||
| `url` | string | URL de la incidencia creada |
|
||||
| `success` | boolean | Estado de éxito de la operación |
|
||||
| `output` | object | Detalles de la incidencia de Jira creada con marca de tiempo, clave de incidencia, resumen, estado de éxito y URL |
|
||||
|
||||
### `jira_bulk_read`
|
||||
|
||||
@@ -124,7 +116,8 @@ Recuperar múltiples incidencias de Jira en bloque
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `issues` | array | Array de incidencias de Jira con marca de tiempo, resumen, descripción, y marcas de tiempo de creación y actualización |
|
||||
| `success` | boolean | Estado de éxito de la operación |
|
||||
| `output` | array | Array de incidencias de Jira con resumen, descripción, marcas de tiempo de creación y actualización |
|
||||
|
||||
### `jira_delete_issue`
|
||||
|
||||
@@ -143,8 +136,8 @@ Eliminar una incidencia de Jira
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Marca de tiempo de la operación |
|
||||
| `issueKey` | string | Clave de la incidencia eliminada |
|
||||
| `success` | boolean | Estado de éxito de la operación |
|
||||
| `output` | object | Detalles de la incidencia eliminada con marca de tiempo, clave de incidencia y estado de éxito |
|
||||
|
||||
### `jira_assign_issue`
|
||||
|
||||
@@ -163,9 +156,8 @@ Asignar una incidencia de Jira a un usuario
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Marca de tiempo de la operación |
|
||||
| `issueKey` | string | Clave de la incidencia que fue asignada |
|
||||
| `assigneeId` | string | ID de cuenta del asignado |
|
||||
| `success` | boolean | Estado de éxito de la operación |
|
||||
| `output` | object | Detalles de la asignación con marca de tiempo, clave de incidencia, ID del asignado y estado de éxito |
|
||||
|
||||
### `jira_transition_issue`
|
||||
|
||||
@@ -185,9 +177,8 @@ Mover una incidencia de Jira entre estados de flujo de trabajo (p. ej., Pendient
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Marca de tiempo de la operación |
|
||||
| `issueKey` | string | Clave de incidencia que fue transicionada |
|
||||
| `transitionId` | string | ID de transición aplicada |
|
||||
| `success` | boolean | Estado de éxito de la operación |
|
||||
| `output` | object | Detalles de la transición con marca de tiempo, clave de incidencia, ID de transición y estado de éxito |
|
||||
|
||||
### `jira_search_issues`
|
||||
|
||||
@@ -208,11 +199,8 @@ Buscar incidencias de Jira usando JQL (Jira Query Language)
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Marca de tiempo de la operación |
|
||||
| `total` | number | Número total de incidencias coincidentes |
|
||||
| `startAt` | number | Índice de inicio de paginación |
|
||||
| `maxResults` | number | Máximo de resultados por página |
|
||||
| `issues` | array | Array de incidencias coincidentes con clave, resumen, estado, asignado, creado, actualizado |
|
||||
| `success` | boolean | Estado de éxito de la operación |
|
||||
| `output` | object | Resultados de búsqueda con marca de tiempo, recuento total, detalles de paginación y array de incidencias coincidentes |
|
||||
|
||||
### `jira_add_comment`
|
||||
|
||||
@@ -231,10 +219,8 @@ Añadir un comentario a una incidencia de Jira
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Marca de tiempo de la operación |
|
||||
| `issueKey` | string | Clave de incidencia a la que se añadió el comentario |
|
||||
| `commentId` | string | ID del comentario creado |
|
||||
| `body` | string | Contenido de texto del comentario |
|
||||
| `success` | boolean | Estado de éxito de la operación |
|
||||
| `output` | object | Detalles del comentario con marca de tiempo, clave de incidencia, ID del comentario, cuerpo y estado de éxito |
|
||||
|
||||
### `jira_get_comments`
|
||||
|
||||
@@ -254,10 +240,8 @@ Obtener todos los comentarios de una incidencia de Jira
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Marca de tiempo de la operación |
|
||||
| `issueKey` | string | Clave de incidencia |
|
||||
| `total` | number | Número total de comentarios |
|
||||
| `comments` | array | Array de comentarios con id, autor, cuerpo, creado, actualizado |
|
||||
| `success` | boolean | Estado de éxito de la operación |
|
||||
| `output` | object | Datos de comentarios con marca de tiempo, clave de incidencia, recuento total y array de comentarios |
|
||||
|
||||
### `jira_update_comment`
|
||||
|
||||
@@ -277,10 +261,8 @@ Actualizar un comentario existente en una incidencia de Jira
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Marca de tiempo de la operación |
|
||||
| `issueKey` | string | Clave de incidencia |
|
||||
| `commentId` | string | ID del comentario actualizado |
|
||||
| `body` | string | Texto actualizado del comentario |
|
||||
| `success` | boolean | Estado de éxito de la operación |
|
||||
| `output` | object | Detalles del comentario actualizado con marca de tiempo, clave de incidencia, ID de comentario, texto del cuerpo y estado de éxito |
|
||||
|
||||
### `jira_delete_comment`
|
||||
|
||||
@@ -299,9 +281,8 @@ Eliminar un comentario de una incidencia de Jira
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Marca de tiempo de la operación |
|
||||
| `issueKey` | string | Clave de incidencia |
|
||||
| `commentId` | string | ID del comentario eliminado |
|
||||
| `success` | boolean | Estado de éxito de la operación |
|
||||
| `output` | object | Detalles de eliminación con marca de tiempo, clave de incidencia, ID de comentario y estado de éxito |
|
||||
|
||||
### `jira_get_attachments`
|
||||
|
||||
@@ -319,9 +300,8 @@ Obtener todos los adjuntos de una incidencia de Jira
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Marca de tiempo de la operación |
|
||||
| `issueKey` | string | Clave de incidencia |
|
||||
| `attachments` | array | Array de adjuntos con id, nombre de archivo, tamaño, tipo MIME, fecha de creación y autor |
|
||||
| `success` | boolean | Estado de éxito de la operación |
|
||||
| `output` | object | Datos de adjuntos con marca de tiempo, clave de incidencia y array de adjuntos |
|
||||
|
||||
### `jira_delete_attachment`
|
||||
|
||||
@@ -339,8 +319,8 @@ Eliminar un adjunto de una incidencia de Jira
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Marca de tiempo de la operación |
|
||||
| `attachmentId` | string | ID del adjunto eliminado |
|
||||
| `success` | boolean | Estado de éxito de la operación |
|
||||
| `output` | object | Detalles de eliminación con marca de tiempo, ID de adjunto y estado de éxito |
|
||||
|
||||
### `jira_add_worklog`
|
||||
|
||||
@@ -361,10 +341,8 @@ Añadir una entrada de registro de trabajo de seguimiento de tiempo a una incide
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Marca de tiempo de la operación |
|
||||
| `issueKey` | string | Clave de incidencia a la que se añadió el registro de trabajo |
|
||||
| `worklogId` | string | ID del registro de trabajo creado |
|
||||
| `timeSpentSeconds` | number | Tiempo empleado en segundos |
|
||||
| `success` | boolean | Estado de éxito de la operación |
|
||||
| `output` | object | Detalles del registro de trabajo con marca de tiempo, clave de incidencia, ID del registro de trabajo, tiempo dedicado en segundos y estado de éxito |
|
||||
|
||||
### `jira_get_worklogs`
|
||||
|
||||
@@ -384,10 +362,8 @@ Obtener todas las entradas de registro de trabajo de una incidencia de Jira
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Marca de tiempo de la operación |
|
||||
| `issueKey` | string | Clave de incidencia |
|
||||
| `total` | number | Número total de registros de trabajo |
|
||||
| `worklogs` | array | Array de registros de trabajo con id, autor, segundos empleados, tiempo empleado, comentario, fecha de creación, actualización e inicio |
|
||||
| `success` | boolean | Estado de éxito de la operación |
|
||||
| `output` | object | Datos de registros de trabajo con marca de tiempo, clave de incidencia, recuento total y array de registros de trabajo |
|
||||
|
||||
### `jira_update_worklog`
|
||||
|
||||
@@ -409,9 +385,8 @@ Actualizar una entrada existente de registro de trabajo en una incidencia de Jir
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Marca de tiempo de la operación |
|
||||
| `issueKey` | string | Clave de incidencia |
|
||||
| `worklogId` | string | ID del registro de trabajo actualizado |
|
||||
| `success` | boolean | Estado de éxito de la operación |
|
||||
| `output` | object | Detalles de actualización del registro de trabajo con marca de tiempo, clave de incidencia, ID de registro de trabajo y estado de éxito |
|
||||
|
||||
### `jira_delete_worklog`
|
||||
|
||||
@@ -430,9 +405,8 @@ Eliminar una entrada de registro de trabajo de una incidencia de Jira
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Marca de tiempo de la operación |
|
||||
| `issueKey` | string | Clave de incidencia |
|
||||
| `worklogId` | string | ID del registro de trabajo eliminado |
|
||||
| `success` | boolean | Estado de éxito de la operación |
|
||||
| `output` | object | Detalles de eliminación con marca de tiempo, clave de incidencia, ID de registro de trabajo y estado de éxito |
|
||||
|
||||
### `jira_create_issue_link`
|
||||
|
||||
@@ -453,11 +427,8 @@ Crear una relación de enlace entre dos incidencias de Jira
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Marca de tiempo de la operación |
|
||||
| `inwardIssue` | string | Clave de incidencia de entrada |
|
||||
| `outwardIssue` | string | Clave de incidencia de salida |
|
||||
| `linkType` | string | Tipo de enlace de incidencia |
|
||||
| `linkId` | string | ID del enlace creado |
|
||||
| `success` | boolean | Estado de éxito de la operación |
|
||||
| `output` | object | Detalles del enlace de incidencia con marca de tiempo, clave de incidencia de entrada, clave de incidencia de salida, tipo de enlace y estado de éxito |
|
||||
|
||||
### `jira_delete_issue_link`
|
||||
|
||||
@@ -475,8 +446,8 @@ Eliminar un enlace entre dos incidencias de Jira
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Marca de tiempo de la operación |
|
||||
| `linkId` | string | ID del enlace eliminado |
|
||||
| `success` | boolean | Estado de éxito de la operación |
|
||||
| `output` | object | Detalles de eliminación con marca de tiempo, ID del enlace y estado de éxito |
|
||||
|
||||
### `jira_add_watcher`
|
||||
|
||||
@@ -495,9 +466,8 @@ Añadir un observador a una incidencia de Jira para recibir notificaciones sobre
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Marca de tiempo de la operación |
|
||||
| `issueKey` | string | Clave de incidencia |
|
||||
| `watcherAccountId` | string | ID de cuenta del observador añadido |
|
||||
| `success` | boolean | Estado de éxito de la operación |
|
||||
| `output` | object | Detalles del observador con marca de tiempo, clave de incidencia, ID de cuenta del observador y estado de éxito |
|
||||
|
||||
### `jira_remove_watcher`
|
||||
|
||||
@@ -516,9 +486,8 @@ Eliminar un observador de una incidencia de Jira
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Marca de tiempo de la operación |
|
||||
| `issueKey` | string | Clave de incidencia |
|
||||
| `watcherAccountId` | string | ID de cuenta del observador eliminado |
|
||||
| `success` | boolean | Estado de éxito de la operación |
|
||||
| `output` | object | Detalles de eliminación con marca de tiempo, clave de incidencia, ID de cuenta del observador y estado de éxito |
|
||||
|
||||
## Notas
|
||||
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
---
|
||||
title: ServiceNow
|
||||
description: Crea, lee, actualiza, elimina e importa masivamente registros de ServiceNow
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="servicenow"
|
||||
color="#032D42"
|
||||
/>
|
||||
|
||||
## Instrucciones de uso
|
||||
|
||||
Integra ServiceNow en tu flujo de trabajo. Puede crear, leer, actualizar y eliminar registros en cualquier tabla de ServiceNow (incidentes, tareas, usuarios, etc.). Admite operaciones de importación masiva para migración de datos y ETL.
|
||||
|
||||
## Herramientas
|
||||
|
||||
### `servicenow_create_record`
|
||||
|
||||
Crea un nuevo registro en una tabla de ServiceNow
|
||||
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Requerido | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `instanceUrl` | string | Sí | URL de la instancia de ServiceNow \(ej., https://instance.service-now.com\) |
|
||||
| `credential` | string | No | ID de credencial OAuth de ServiceNow |
|
||||
| `tableName` | string | Sí | Nombre de la tabla \(ej., incident, task, sys_user\) |
|
||||
| `fields` | json | Sí | Campos a establecer en el registro \(objeto JSON\) |
|
||||
|
||||
#### Salida
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `record` | json | Registro de ServiceNow creado con sys_id y otros campos |
|
||||
| `metadata` | json | Metadatos de la operación |
|
||||
|
||||
### `servicenow_read_record`
|
||||
|
||||
Lee registros de una tabla de ServiceNow
|
||||
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Requerido | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `instanceUrl` | string | No | URL de la instancia de ServiceNow \(detectada automáticamente desde OAuth si no se proporciona\) |
|
||||
| `credential` | string | No | ID de credencial OAuth de ServiceNow |
|
||||
| `tableName` | string | Sí | Nombre de la tabla |
|
||||
| `sysId` | string | No | sys_id específico del registro |
|
||||
| `number` | string | No | Número de registro \(ej., INC0010001\) |
|
||||
| `query` | string | No | Cadena de consulta codificada \(ej., "active=true^priority=1"\) |
|
||||
| `limit` | number | No | Número máximo de registros a devolver |
|
||||
| `fields` | string | No | Lista de campos separados por comas a devolver |
|
||||
|
||||
#### Salida
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `records` | array | Array de registros de ServiceNow |
|
||||
| `metadata` | json | Metadatos de la operación |
|
||||
|
||||
### `servicenow_update_record`
|
||||
|
||||
Actualizar un registro existente en una tabla de ServiceNow
|
||||
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Requerido | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `instanceUrl` | string | No | URL de la instancia de ServiceNow \(detectada automáticamente desde OAuth si no se proporciona\) |
|
||||
| `credential` | string | No | ID de credencial OAuth de ServiceNow |
|
||||
| `tableName` | string | Sí | Nombre de la tabla |
|
||||
| `sysId` | string | Sí | sys_id del registro a actualizar |
|
||||
| `fields` | json | Sí | Campos a actualizar \(objeto JSON\) |
|
||||
|
||||
#### Salida
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `record` | json | Registro de ServiceNow actualizado |
|
||||
| `metadata` | json | Metadatos de la operación |
|
||||
|
||||
### `servicenow_delete_record`
|
||||
|
||||
Eliminar un registro de una tabla de ServiceNow
|
||||
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Requerido | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `instanceUrl` | string | No | URL de la instancia de ServiceNow \(detectada automáticamente desde OAuth si no se proporciona\) |
|
||||
| `credential` | string | No | ID de credencial OAuth de ServiceNow |
|
||||
| `tableName` | string | Sí | Nombre de la tabla |
|
||||
| `sysId` | string | Sí | sys_id del registro a eliminar |
|
||||
|
||||
#### Salida
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Si la eliminación fue exitosa |
|
||||
| `metadata` | json | Metadatos de la operación |
|
||||
|
||||
## Notas
|
||||
|
||||
- Categoría: `tools`
|
||||
- Tipo: `servicenow`
|
||||
@@ -54,7 +54,7 @@ Integra Slack en el flujo de trabajo. Puede enviar, actualizar y eliminar mensaj
|
||||
|
||||
### `slack_message`
|
||||
|
||||
Envía mensajes a canales de Slack o mensajes directos. Compatible con el formato mrkdwn de Slack.
|
||||
Envía mensajes a canales o usuarios de Slack a través de la API de Slack. Compatible con el formato mrkdwn de Slack.
|
||||
|
||||
#### Entrada
|
||||
|
||||
@@ -62,10 +62,9 @@ Envía mensajes a canales de Slack o mensajes directos. Compatible con el format
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `authMethod` | string | No | Método de autenticación: oauth o bot_token |
|
||||
| `botToken` | string | No | Token del bot para Bot personalizado |
|
||||
| `channel` | string | No | Canal de Slack objetivo (p. ej., #general) |
|
||||
| `userId` | string | No | ID de usuario de Slack objetivo para mensajes directos (p. ej., U1234567890) |
|
||||
| `channel` | string | Sí | Canal de Slack objetivo (p. ej., #general) |
|
||||
| `text` | string | Sí | Texto del mensaje a enviar (admite formato mrkdwn de Slack) |
|
||||
| `thread_ts` | string | No | Marca de tiempo del hilo al que responder (crea respuesta en hilo) |
|
||||
| `thread_ts` | string | No | Marca de tiempo del hilo para responder (crea respuesta en hilo) |
|
||||
| `files` | file[] | No | Archivos para adjuntar al mensaje |
|
||||
|
||||
#### Salida
|
||||
@@ -110,8 +109,7 @@ Lee los últimos mensajes de los canales de Slack. Recupera el historial de conv
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `authMethod` | string | No | Método de autenticación: oauth o bot_token |
|
||||
| `botToken` | string | No | Token del bot para Bot personalizado |
|
||||
| `channel` | string | No | Canal de Slack del que leer mensajes (p. ej., #general) |
|
||||
| `userId` | string | No | ID de usuario para conversación por MD (p. ej., U1234567890) |
|
||||
| `channel` | string | Sí | Canal de Slack del que leer mensajes (p. ej., #general) |
|
||||
| `limit` | number | No | Número de mensajes a recuperar (predeterminado: 10, máx: 100) |
|
||||
| `oldest` | string | No | Inicio del rango de tiempo (marca de tiempo) |
|
||||
| `latest` | string | No | Fin del rango de tiempo (marca de tiempo) |
|
||||
|
||||
@@ -27,14 +27,12 @@ En Sim, la integración con Zoom permite a tus agentes automatizar la programaci
|
||||
- Obtener detalles o invitaciones para cualquier reunión
|
||||
- Actualizar o eliminar reuniones existentes directamente desde tus automatizaciones
|
||||
|
||||
Para conectarte a Zoom, arrastra el bloque de Zoom y haz clic en `Connect` para autenticarte con tu cuenta de Zoom. Una vez conectado, puedes usar las herramientas de Zoom para crear, listar, actualizar y eliminar reuniones de Zoom. En cualquier momento, puedes desconectar tu cuenta de Zoom haciendo clic en `Disconnect` en Configuración > Integraciones, y el acceso a tu cuenta de Zoom será revocado inmediatamente.
|
||||
|
||||
Estas capacidades te permiten agilizar la colaboración remota, automatizar sesiones de video recurrentes y gestionar el entorno de Zoom de tu organización, todo como parte de tus flujos de trabajo.
|
||||
Estas capacidades te permiten agilizar la colaboración remota, automatizar sesiones de video recurrentes y gestionar el entorno Zoom de tu organización como parte de tus flujos de trabajo.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
## Instrucciones de uso
|
||||
|
||||
Integra Zoom en los flujos de trabajo. Crea, lista, actualiza y elimina reuniones de Zoom. Obtén detalles de reuniones, invitaciones, grabaciones y participantes. Gestiona grabaciones en la nube de forma programática.
|
||||
Integra Zoom en flujos de trabajo. Crea, lista, actualiza y elimina reuniones de Zoom. Obtén detalles de reuniones, invitaciones, grabaciones y participantes. Gestiona grabaciones en la nube de forma programática.
|
||||
|
||||
## Herramientas
|
||||
|
||||
@@ -46,12 +44,12 @@ Crear una nueva reunión de Zoom
|
||||
|
||||
| Parámetro | Tipo | Obligatorio | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `userId` | string | Sí | El ID de usuario o dirección de correo electrónico. Use "me" para el usuario autenticado. |
|
||||
| `userId` | string | Sí | El ID de usuario o dirección de correo electrónico. Usa "me" para el usuario autenticado. |
|
||||
| `topic` | string | Sí | Tema de la reunión |
|
||||
| `type` | number | No | Tipo de reunión: 1=instantánea, 2=programada, 3=recurrente sin hora fija, 8=recurrente con hora fija |
|
||||
| `startTime` | string | No | Hora de inicio de la reunión en formato ISO 8601 \(ej., 2025-06-03T10:00:00Z\) |
|
||||
| `startTime` | string | No | Hora de inicio de la reunión en formato ISO 8601 \(p. ej., 2025-06-03T10:00:00Z\) |
|
||||
| `duration` | number | No | Duración de la reunión en minutos |
|
||||
| `timezone` | string | No | Zona horaria para la reunión \(ej., America/Los_Angeles\) |
|
||||
| `timezone` | string | No | Zona horaria para la reunión \(p. ej., America/Los_Angeles\) |
|
||||
| `password` | string | No | Contraseña de la reunión |
|
||||
| `agenda` | string | No | Agenda de la reunión |
|
||||
| `hostVideo` | boolean | No | Iniciar con video del anfitrión activado |
|
||||
@@ -69,13 +67,13 @@ Crear una nueva reunión de Zoom
|
||||
|
||||
### `zoom_list_meetings`
|
||||
|
||||
Listar todas las reuniones de un usuario de Zoom
|
||||
Listar todas las reuniones para un usuario de Zoom
|
||||
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Obligatorio | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `userId` | string | Sí | El ID de usuario o dirección de correo electrónico. Usa "me" para el usuario autenticado. |
|
||||
| `userId` | string | Sí | El ID de usuario o dirección de correo electrónico. Use "me" para el usuario autenticado. |
|
||||
| `type` | string | No | Filtro de tipo de reunión: scheduled, live, upcoming, upcoming_meetings, o previous_meetings |
|
||||
| `pageSize` | number | No | Número de registros por página \(máximo 300\) |
|
||||
| `nextPageToken` | string | No | Token para paginación para obtener la siguiente página de resultados |
|
||||
@@ -105,7 +103,7 @@ Obtener detalles de una reunión específica de Zoom
|
||||
| --------- | ---- | ----------- |
|
||||
| `meeting` | object | Los detalles de la reunión |
|
||||
|
||||
Obtener detalles de una reunión específica de Zoom
|
||||
### `zoom_update_meeting`
|
||||
|
||||
Actualizar una reunión existente de Zoom
|
||||
|
||||
@@ -124,9 +122,9 @@ Actualizar una reunión existente de Zoom
|
||||
| `hostVideo` | boolean | No | Iniciar con video del anfitrión activado |
|
||||
| `participantVideo` | boolean | No | Iniciar con video de participantes activado |
|
||||
| `joinBeforeHost` | boolean | No | Permitir que los participantes se unan antes que el anfitrión |
|
||||
| `muteUponEntry` | boolean | No | Silenciar participantes al entrar |
|
||||
| `muteUponEntry` | boolean | No | Silenciar a los participantes al entrar |
|
||||
| `waitingRoom` | boolean | No | Habilitar sala de espera |
|
||||
| `autoRecording` | string | No | Configuración de grabación automática: local, cloud o none |
|
||||
| `autoRecording` | string | No | Configuración de grabación automática: local, en la nube o ninguna |
|
||||
|
||||
#### Salida
|
||||
|
||||
@@ -134,7 +132,7 @@ Actualizar una reunión existente de Zoom
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Si la reunión se actualizó correctamente |
|
||||
|
||||
Eliminar una reunión de Zoom
|
||||
### `zoom_delete_meeting`
|
||||
|
||||
Eliminar o cancelar una reunión de Zoom
|
||||
|
||||
@@ -144,14 +142,14 @@ Eliminar o cancelar una reunión de Zoom
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `meetingId` | string | Sí | El ID de la reunión a eliminar |
|
||||
| `occurrenceId` | string | No | ID de ocurrencia para eliminar una ocurrencia específica de una reunión recurrente |
|
||||
| `scheduleForReminder` | boolean | No | Enviar correo electrónico de recordatorio de cancelación a los registrados |
|
||||
| `cancelMeetingReminder` | boolean | No | Enviar correo electrónico de cancelación a los registrados y anfitriones alternativos |
|
||||
| `scheduleForReminder` | boolean | No | Enviar correo electrónico de recordatorio de cancelación a los inscritos |
|
||||
| `cancelMeetingReminder` | boolean | No | Enviar correo electrónico de cancelación a los inscritos y anfitriones alternativos |
|
||||
|
||||
#### Salida
|
||||
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Si la reunión se eliminó correctamente |
|
||||
| `success` | boolean | Indica si la reunión se eliminó correctamente |
|
||||
|
||||
### `zoom_get_meeting_invitation`
|
||||
|
||||
@@ -177,10 +175,10 @@ Listar todas las grabaciones en la nube para un usuario de Zoom
|
||||
|
||||
| Parámetro | Tipo | Obligatorio | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `userId` | string | Sí | El ID de usuario o dirección de correo electrónico. Usa "me" para el usuario autenticado. |
|
||||
| `from` | string | No | Fecha de inicio en formato aaaa-mm-dd (dentro de los últimos 6 meses) |
|
||||
| `userId` | string | Sí | El ID de usuario o dirección de correo electrónico. Use "me" para el usuario autenticado. |
|
||||
| `from` | string | No | Fecha de inicio en formato aaaa-mm-dd \(dentro de los últimos 6 meses\) |
|
||||
| `to` | string | No | Fecha de fin en formato aaaa-mm-dd |
|
||||
| `pageSize` | number | No | Número de registros por página (máx. 300) |
|
||||
| `pageSize` | number | No | Número de registros por página \(máximo 300\) |
|
||||
| `nextPageToken` | string | No | Token para paginación para obtener la siguiente página de resultados |
|
||||
| `trash` | boolean | No | Establecer como true para listar grabaciones de la papelera |
|
||||
|
||||
@@ -191,9 +189,9 @@ Listar todas las grabaciones en la nube para un usuario de Zoom
|
||||
| `recordings` | array | Lista de grabaciones |
|
||||
| `pageInfo` | object | Información de paginación |
|
||||
|
||||
Obtener todas las grabaciones para una reunión específica de Zoom
|
||||
### `zoom_get_meeting_recordings`
|
||||
|
||||
Obtener todas las grabaciones para una reunión específica de Zoom
|
||||
Obtener todas las grabaciones de una reunión específica de Zoom
|
||||
|
||||
#### Entrada
|
||||
|
||||
@@ -201,7 +199,7 @@ Obtener todas las grabaciones para una reunión específica de Zoom
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `meetingId` | string | Sí | El ID de la reunión o UUID de la reunión |
|
||||
| `includeFolderItems` | boolean | No | Incluir elementos dentro de una carpeta |
|
||||
| `ttl` | number | No | Tiempo de vida para URLs de descarga en segundos \(máx. 604800\) |
|
||||
| `ttl` | number | No | Tiempo de vida para las URLs de descarga en segundos \(máx. 604800\) |
|
||||
|
||||
#### Salida
|
||||
|
||||
@@ -209,9 +207,9 @@ Obtener todas las grabaciones para una reunión específica de Zoom
|
||||
| --------- | ---- | ----------- |
|
||||
| `recording` | object | La grabación de la reunión con todos los archivos |
|
||||
|
||||
Eliminar grabaciones en la nube para una reunión de Zoom
|
||||
### `zoom_delete_recording`
|
||||
|
||||
Eliminar grabaciones en la nube para una reunión de Zoom
|
||||
Eliminar grabaciones en la nube de una reunión de Zoom
|
||||
|
||||
#### Entrada
|
||||
|
||||
@@ -227,7 +225,7 @@ Eliminar grabaciones en la nube para una reunión de Zoom
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Si la grabación se eliminó correctamente |
|
||||
|
||||
Listar participantes de una reunión pasada de Zoom
|
||||
### `zoom_list_past_participants`
|
||||
|
||||
Listar participantes de una reunión pasada de Zoom
|
||||
|
||||
@@ -235,7 +233,7 @@ Listar participantes de una reunión pasada de Zoom
|
||||
|
||||
| Parámetro | Tipo | Obligatorio | Descripción |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `meetingId` | string | Sí | El ID de la reunión pasada o UUID |
|
||||
| `meetingId` | string | Sí | El ID o UUID de la reunión pasada |
|
||||
| `pageSize` | number | No | Número de registros por página \(máx. 300\) |
|
||||
| `nextPageToken` | string | No | Token para paginación para obtener la siguiente página de resultados |
|
||||
|
||||
|
||||
@@ -48,13 +48,8 @@ Récupérer des informations détaillées sur un ticket Jira spécifique
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | chaîne | Horodatage de l'opération |
|
||||
| `issueKey` | chaîne | Clé du ticket \(ex. : PROJ-123\) |
|
||||
| `summary` | chaîne | Résumé du ticket |
|
||||
| `description` | json | Contenu de la description du ticket |
|
||||
| `created` | chaîne | Horodatage de création du ticket |
|
||||
| `updated` | chaîne | Horodatage de dernière mise à jour du ticket |
|
||||
| `issue` | json | Objet complet du ticket avec tous les champs |
|
||||
| `success` | booléen | Statut de réussite de l'opération |
|
||||
| `output` | objet | Détails du ticket Jira avec la clé du ticket, le résumé, la description, les horodatages de création et de mise à jour |
|
||||
|
||||
### `jira_update`
|
||||
|
||||
@@ -78,9 +73,8 @@ Mettre à jour un ticket Jira
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | chaîne | Horodatage de l'opération |
|
||||
| `issueKey` | chaîne | Clé du ticket mis à jour \(ex. : PROJ-123\) |
|
||||
| `summary` | chaîne | Résumé du ticket après mise à jour |
|
||||
| `success` | boolean | Statut de réussite de l'opération |
|
||||
| `output` | object | Détails de la demande Jira mise à jour avec horodatage, clé de la demande, résumé et statut de réussite |
|
||||
|
||||
### `jira_write`
|
||||
|
||||
@@ -103,10 +97,8 @@ Rédiger une demande Jira
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | chaîne | Horodatage de l'opération |
|
||||
| `issueKey` | chaîne | Clé du ticket créé \(ex. : PROJ-123\) |
|
||||
| `summary` | chaîne | Résumé du ticket |
|
||||
| `url` | chaîne | URL vers le ticket créé |
|
||||
| `success` | boolean | Statut de réussite de l'opération |
|
||||
| `output` | object | Détails de la demande Jira créée avec horodatage, clé de la demande, résumé, statut de réussite et URL |
|
||||
|
||||
### `jira_bulk_read`
|
||||
|
||||
@@ -124,7 +116,8 @@ Récupérer plusieurs demandes Jira en masse
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `issues` | tableau | Tableau des tickets Jira avec horodatages ts, résumé, description, création et mise à jour |
|
||||
| `success` | boolean | Statut de réussite de l'opération |
|
||||
| `output` | array | Tableau des tickets Jira avec résumé, description, horodatages de création et de mise à jour |
|
||||
|
||||
### `jira_delete_issue`
|
||||
|
||||
@@ -143,8 +136,8 @@ Supprimer un ticket Jira
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | chaîne | Horodatage de l'opération |
|
||||
| `issueKey` | chaîne | Clé du ticket supprimé |
|
||||
| `success` | booléen | Statut de réussite de l'opération |
|
||||
| `output` | objet | Détails du ticket supprimé avec horodatage, clé du ticket et statut de réussite |
|
||||
|
||||
### `jira_assign_issue`
|
||||
|
||||
@@ -163,9 +156,8 @@ Assigner un ticket Jira à un utilisateur
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | chaîne | Horodatage de l'opération |
|
||||
| `issueKey` | chaîne | Clé du ticket qui a été assigné |
|
||||
| `assigneeId` | chaîne | ID de compte de l'assigné |
|
||||
| `success` | booléen | Statut de réussite de l'opération |
|
||||
| `output` | objet | Détails de l'assignation avec horodatage, clé du ticket, ID de l'assigné et statut de réussite |
|
||||
|
||||
### `jira_transition_issue`
|
||||
|
||||
@@ -185,9 +177,8 @@ Déplacer un ticket Jira entre les statuts de workflow (par ex., À faire -> En
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Horodatage de l'opération |
|
||||
| `issueKey` | string | Clé du ticket qui a été transitionné |
|
||||
| `transitionId` | string | ID de la transition appliquée |
|
||||
| `success` | booléen | Statut de réussite de l'opération |
|
||||
| `output` | objet | Détails de la transition avec horodatage, clé du ticket, ID de transition et statut de réussite |
|
||||
|
||||
### `jira_search_issues`
|
||||
|
||||
@@ -208,11 +199,8 @@ Rechercher des tickets Jira à l'aide de JQL (Jira Query Language)
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Horodatage de l'opération |
|
||||
| `total` | number | Nombre total de tickets correspondants |
|
||||
| `startAt` | number | Index de début de pagination |
|
||||
| `maxResults` | number | Nombre maximum de résultats par page |
|
||||
| `issues` | array | Tableau des tickets correspondants avec clé, résumé, statut, assigné, créé, mis à jour |
|
||||
| `success` | booléen | Statut de réussite de l'opération |
|
||||
| `output` | objet | Résultats de recherche avec horodatage, nombre total, détails de pagination et tableau des tickets correspondants |
|
||||
|
||||
### `jira_add_comment`
|
||||
|
||||
@@ -231,10 +219,8 @@ Ajouter un commentaire à un ticket Jira
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Horodatage de l'opération |
|
||||
| `issueKey` | string | Clé du ticket auquel le commentaire a été ajouté |
|
||||
| `commentId` | string | ID du commentaire créé |
|
||||
| `body` | string | Contenu textuel du commentaire |
|
||||
| `success` | booléen | Statut de réussite de l'opération |
|
||||
| `output` | objet | Détails du commentaire avec horodatage, clé du ticket, ID du commentaire, corps et statut de réussite |
|
||||
|
||||
### `jira_get_comments`
|
||||
|
||||
@@ -254,10 +240,8 @@ Obtenir tous les commentaires d'un ticket Jira
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Horodatage de l'opération |
|
||||
| `issueKey` | string | Clé du ticket |
|
||||
| `total` | number | Nombre total de commentaires |
|
||||
| `comments` | array | Tableau des commentaires avec id, auteur, corps, créé, mis à jour |
|
||||
| `success` | boolean | Statut de réussite de l'opération |
|
||||
| `output` | object | Données des commentaires avec horodatage, clé du ticket, nombre total et tableau de commentaires |
|
||||
|
||||
### `jira_update_comment`
|
||||
|
||||
@@ -277,10 +261,8 @@ Mettre à jour un commentaire existant sur un ticket Jira
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Horodatage de l'opération |
|
||||
| `issueKey` | string | Clé du ticket |
|
||||
| `commentId` | string | ID du commentaire mis à jour |
|
||||
| `body` | string | Texte du commentaire mis à jour |
|
||||
| `success` | boolean | Statut de réussite de l'opération |
|
||||
| `output` | object | Détails du commentaire mis à jour avec horodatage, clé du ticket, ID du commentaire, texte du corps et statut de réussite |
|
||||
|
||||
### `jira_delete_comment`
|
||||
|
||||
@@ -299,9 +281,8 @@ Supprimer un commentaire d'un ticket Jira
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Horodatage de l'opération |
|
||||
| `issueKey` | string | Clé du ticket |
|
||||
| `commentId` | string | ID du commentaire supprimé |
|
||||
| `success` | boolean | Statut de réussite de l'opération |
|
||||
| `output` | object | Détails de la suppression avec horodatage, clé du ticket, ID du commentaire et statut de réussite |
|
||||
|
||||
### `jira_get_attachments`
|
||||
|
||||
@@ -319,9 +300,8 @@ Obtenir toutes les pièces jointes d'un ticket Jira
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Horodatage de l'opération |
|
||||
| `issueKey` | string | Clé du ticket |
|
||||
| `attachments` | array | Tableau des pièces jointes avec id, nom de fichier, taille, type MIME, date de création, auteur |
|
||||
| `success` | boolean | Statut de réussite de l'opération |
|
||||
| `output` | object | Données des pièces jointes avec horodatage, clé du ticket et tableau des pièces jointes |
|
||||
|
||||
### `jira_delete_attachment`
|
||||
|
||||
@@ -339,8 +319,8 @@ Supprimer une pièce jointe d'un ticket Jira
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Horodatage de l'opération |
|
||||
| `attachmentId` | string | ID de la pièce jointe supprimée |
|
||||
| `success` | boolean | Statut de réussite de l'opération |
|
||||
| `output` | object | Détails de la suppression avec horodatage, ID de la pièce jointe et statut de réussite |
|
||||
|
||||
### `jira_add_worklog`
|
||||
|
||||
@@ -361,10 +341,8 @@ Ajouter une entrée de journal de travail pour le suivi du temps à un ticket Ji
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Horodatage de l'opération |
|
||||
| `issueKey` | string | Clé du ticket auquel le journal de travail a été ajouté |
|
||||
| `worklogId` | string | ID du journal de travail créé |
|
||||
| `timeSpentSeconds` | number | Temps passé en secondes |
|
||||
| `success` | booléen | Statut de réussite de l'opération |
|
||||
| `output` | objet | Détails du journal de travail avec horodatage, clé du ticket, ID du journal de travail, temps passé en secondes et statut de réussite |
|
||||
|
||||
### `jira_get_worklogs`
|
||||
|
||||
@@ -384,10 +362,8 @@ Obtenir toutes les entrées du journal de travail d'un ticket Jira
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Horodatage de l'opération |
|
||||
| `issueKey` | string | Clé du ticket |
|
||||
| `total` | number | Nombre total de journaux de travail |
|
||||
| `worklogs` | array | Tableau des journaux de travail avec id, auteur, temps passé en secondes, temps passé, commentaire, date de création, mise à jour, démarrage |
|
||||
| `success` | boolean | Statut de réussite de l'opération |
|
||||
| `output` | object | Données des journaux de travail avec horodatage, clé du ticket, nombre total et tableau des journaux de travail |
|
||||
|
||||
### `jira_update_worklog`
|
||||
|
||||
@@ -409,9 +385,8 @@ Mettre à jour une entrée de journal de travail existante sur un ticket Jira
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Horodatage de l'opération |
|
||||
| `issueKey` | string | Clé du ticket |
|
||||
| `worklogId` | string | ID du journal de travail mis à jour |
|
||||
| `success` | boolean | Statut de réussite de l'opération |
|
||||
| `output` | object | Détails de la mise à jour du journal de travail avec horodatage, clé du ticket, ID du journal de travail et statut de réussite |
|
||||
|
||||
### `jira_delete_worklog`
|
||||
|
||||
@@ -430,9 +405,8 @@ Supprimer une entrée de journal de travail d'un ticket Jira
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Horodatage de l'opération |
|
||||
| `issueKey` | string | Clé du ticket |
|
||||
| `worklogId` | string | ID du journal de travail supprimé |
|
||||
| `success` | boolean | Statut de réussite de l'opération |
|
||||
| `output` | object | Détails de la suppression avec horodatage, clé de la demande, ID du journal de travail et statut de réussite |
|
||||
|
||||
### `jira_create_issue_link`
|
||||
|
||||
@@ -453,11 +427,8 @@ Créer une relation de lien entre deux tickets Jira
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Horodatage de l'opération |
|
||||
| `inwardIssue` | string | Clé du ticket entrant |
|
||||
| `outwardIssue` | string | Clé du ticket sortant |
|
||||
| `linkType` | string | Type de lien entre tickets |
|
||||
| `linkId` | string | ID du lien créé |
|
||||
| `success` | boolean | Statut de réussite de l'opération |
|
||||
| `output` | object | Détails du lien entre tickets avec horodatage, clé du ticket entrant, clé du ticket sortant, type de lien et statut de réussite |
|
||||
|
||||
### `jira_delete_issue_link`
|
||||
|
||||
@@ -475,8 +446,8 @@ Supprimer un lien entre deux tickets Jira
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Horodatage de l'opération |
|
||||
| `linkId` | string | ID du lien supprimé |
|
||||
| `success` | boolean | Statut de réussite de l'opération |
|
||||
| `output` | object | Détails de la suppression avec horodatage, ID du lien et statut de réussite |
|
||||
|
||||
### `jira_add_watcher`
|
||||
|
||||
@@ -495,9 +466,8 @@ Ajouter un observateur à un ticket Jira pour recevoir des notifications sur les
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Horodatage de l'opération |
|
||||
| `issueKey` | string | Clé du ticket |
|
||||
| `watcherAccountId` | string | ID du compte observateur ajouté |
|
||||
| `success` | boolean | Statut de réussite de l'opération |
|
||||
| `output` | object | Détails de l'observateur avec horodatage, clé du ticket, ID de compte de l'observateur et statut de réussite |
|
||||
|
||||
### `jira_remove_watcher`
|
||||
|
||||
@@ -516,9 +486,8 @@ Supprimer un observateur d'un ticket Jira
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | Horodatage de l'opération |
|
||||
| `issueKey` | string | Clé du ticket |
|
||||
| `watcherAccountId` | string | ID du compte observateur supprimé |
|
||||
| `success` | boolean | Statut de réussite de l'opération |
|
||||
| `output` | object | Détails de la suppression avec horodatage, clé de la demande, ID du compte observateur et statut de réussite |
|
||||
|
||||
## Notes
|
||||
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
---
|
||||
title: ServiceNow
|
||||
description: Créer, lire, mettre à jour, supprimer et importer en masse des
|
||||
enregistrements ServiceNow
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="servicenow"
|
||||
color="#032D42"
|
||||
/>
|
||||
|
||||
## Instructions d'utilisation
|
||||
|
||||
Intégrez ServiceNow dans votre flux de travail. Permet de créer, lire, mettre à jour et supprimer des enregistrements dans n'importe quelle table ServiceNow (incidents, tâches, utilisateurs, etc.). Prend en charge les opérations d'importation en masse pour la migration de données et l'ETL.
|
||||
|
||||
## Outils
|
||||
|
||||
### `servicenow_create_record`
|
||||
|
||||
Créer un nouvel enregistrement dans une table ServiceNow
|
||||
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Requis | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `instanceUrl` | string | Oui | URL de l'instance ServiceNow \(par exemple, https://instance.service-now.com\) |
|
||||
| `credential` | string | Non | ID d'identification OAuth ServiceNow |
|
||||
| `tableName` | string | Oui | Nom de la table \(par exemple, incident, task, sys_user\) |
|
||||
| `fields` | json | Oui | Champs à définir sur l'enregistrement \(objet JSON\) |
|
||||
|
||||
#### Sortie
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `record` | json | Enregistrement ServiceNow créé avec sys_id et autres champs |
|
||||
| `metadata` | json | Métadonnées de l'opération |
|
||||
|
||||
### `servicenow_read_record`
|
||||
|
||||
Lire des enregistrements d'une table ServiceNow
|
||||
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Requis | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `instanceUrl` | string | Non | URL de l'instance ServiceNow \(détectée automatiquement depuis OAuth si non fournie\) |
|
||||
| `credential` | string | Non | ID d'identification OAuth ServiceNow |
|
||||
| `tableName` | string | Oui | Nom de la table |
|
||||
| `sysId` | string | Non | sys_id spécifique de l'enregistrement |
|
||||
| `number` | string | Non | Numéro d'enregistrement \(par exemple, INC0010001\) |
|
||||
| `query` | string | Non | Chaîne de requête encodée \(par exemple, "active=true^priority=1"\) |
|
||||
| `limit` | number | Non | Nombre maximum d'enregistrements à retourner |
|
||||
| `fields` | string | Non | Liste de champs séparés par des virgules à retourner |
|
||||
|
||||
#### Sortie
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `records` | array | Tableau des enregistrements ServiceNow |
|
||||
| `metadata` | json | Métadonnées de l'opération |
|
||||
|
||||
### `servicenow_update_record`
|
||||
|
||||
Mettre à jour un enregistrement existant dans une table ServiceNow
|
||||
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Requis | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `instanceUrl` | string | Non | URL de l'instance ServiceNow (détectée automatiquement depuis OAuth si non fournie) |
|
||||
| `credential` | string | Non | ID des identifiants OAuth ServiceNow |
|
||||
| `tableName` | string | Oui | Nom de la table |
|
||||
| `sysId` | string | Oui | sys_id de l'enregistrement à mettre à jour |
|
||||
| `fields` | json | Oui | Champs à mettre à jour (objet JSON) |
|
||||
|
||||
#### Sortie
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `record` | json | Enregistrement ServiceNow mis à jour |
|
||||
| `metadata` | json | Métadonnées de l'opération |
|
||||
|
||||
### `servicenow_delete_record`
|
||||
|
||||
Supprimer un enregistrement d'une table ServiceNow
|
||||
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Requis | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `instanceUrl` | string | Non | URL de l'instance ServiceNow (détectée automatiquement depuis OAuth si non fournie) |
|
||||
| `credential` | string | Non | ID des identifiants OAuth ServiceNow |
|
||||
| `tableName` | string | Oui | Nom de la table |
|
||||
| `sysId` | string | Oui | sys_id de l'enregistrement à supprimer |
|
||||
|
||||
#### Sortie
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Indique si la suppression a réussi |
|
||||
| `metadata` | json | Métadonnées de l'opération |
|
||||
|
||||
## Notes
|
||||
|
||||
- Catégorie : `tools`
|
||||
- Type : `servicenow`
|
||||
@@ -54,18 +54,17 @@ Intégrez Slack dans le flux de travail. Peut envoyer, mettre à jour et supprim
|
||||
|
||||
### `slack_message`
|
||||
|
||||
Envoyez des messages aux canaux Slack ou en messages directs. Prend en charge le formatage mrkdwn de Slack.
|
||||
Envoyez des messages aux canaux ou utilisateurs Slack via l'API Slack. Prend en charge le formatage mrkdwn de Slack.
|
||||
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
| --------- | ---- | ---------- | ----------- |
|
||||
| `authMethod` | chaîne | Non | Méthode d'authentification : oauth ou bot_token |
|
||||
| `botToken` | chaîne | Non | Jeton du bot pour Bot personnalisé |
|
||||
| `channel` | chaîne | Non | Canal Slack cible \(ex. : #general\) |
|
||||
| `userId` | chaîne | Non | ID utilisateur Slack cible pour les messages directs \(ex. : U1234567890\) |
|
||||
| `botToken` | chaîne | Non | Jeton du bot pour le Bot personnalisé |
|
||||
| `channel` | chaîne | Oui | Canal Slack cible \(par ex., #general\) |
|
||||
| `text` | chaîne | Oui | Texte du message à envoyer \(prend en charge le formatage mrkdwn de Slack\) |
|
||||
| `thread_ts` | chaîne | Non | Horodatage du fil de discussion auquel répondre \(crée une réponse dans le fil\) |
|
||||
| `thread_ts` | chaîne | Non | Horodatage du fil pour répondre \(crée une réponse dans le fil\) |
|
||||
| `files` | fichier[] | Non | Fichiers à joindre au message |
|
||||
|
||||
#### Sortie
|
||||
@@ -110,8 +109,7 @@ Lisez les derniers messages des canaux Slack. Récupérez l'historique des conve
|
||||
| --------- | ---- | ---------- | ----------- |
|
||||
| `authMethod` | chaîne | Non | Méthode d'authentification : oauth ou bot_token |
|
||||
| `botToken` | chaîne | Non | Jeton du bot pour Bot personnalisé |
|
||||
| `channel` | chaîne | Non | Canal Slack pour lire les messages \(ex. : #general\) |
|
||||
| `userId` | chaîne | Non | ID utilisateur pour la conversation en MP \(ex. : U1234567890\) |
|
||||
| `channel` | chaîne | Oui | Canal Slack pour lire les messages \(ex. : #general\) |
|
||||
| `limit` | nombre | Non | Nombre de messages à récupérer \(par défaut : 10, max : 100\) |
|
||||
| `oldest` | chaîne | Non | Début de la plage temporelle \(horodatage\) |
|
||||
| `latest` | chaîne | Non | Fin de la plage temporelle \(horodatage\) |
|
||||
|
||||
@@ -27,14 +27,12 @@ Dans Sim, l'intégration Zoom permet à vos agents d'automatiser la planificatio
|
||||
- Récupérer les détails ou les invitations pour n'importe quelle réunion
|
||||
- Mettre à jour ou supprimer des réunions existantes directement depuis vos automatisations
|
||||
|
||||
Pour vous connecter à Zoom, déposez le bloc Zoom et cliquez sur `Connect` pour vous authentifier avec votre compte Zoom. Une fois connecté, vous pouvez utiliser les outils Zoom pour créer, lister, mettre à jour et supprimer des réunions Zoom. À tout moment, vous pouvez déconnecter votre compte Zoom en cliquant sur `Disconnect` dans Paramètres > Intégrations, et l'accès à votre compte Zoom sera immédiatement révoqué.
|
||||
|
||||
Ces fonctionnalités vous permettent de rationaliser la collaboration à distance, d'automatiser les sessions vidéo récurrentes et de gérer l'environnement Zoom de votre organisation, le tout dans le cadre de vos flux de travail.
|
||||
Ces capacités vous permettent de rationaliser la collaboration à distance, d'automatiser les sessions vidéo récurrentes et de gérer l'environnement Zoom de votre organisation, le tout dans le cadre de vos flux de travail.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
## Instructions d'utilisation
|
||||
|
||||
Intégrez Zoom dans vos flux de travail. Créez, listez, mettez à jour et supprimez des réunions Zoom. Obtenez les détails des réunions, les invitations, les enregistrements et les participants. Gérez les enregistrements cloud de manière programmatique.
|
||||
Intégrez Zoom dans les flux de travail. Créez, listez, mettez à jour et supprimez des réunions Zoom. Obtenez les détails des réunions, les invitations, les enregistrements et les participants. Gérez les enregistrements cloud par programmation.
|
||||
|
||||
## Outils
|
||||
|
||||
@@ -45,7 +43,7 @@ Créer une nouvelle réunion Zoom
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
| --------- | ---- | ----------- | ----------- |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `userId` | string | Oui | L'ID utilisateur ou l'adresse e-mail. Utilisez "me" pour l'utilisateur authentifié. |
|
||||
| `topic` | string | Oui | Sujet de la réunion |
|
||||
| `type` | number | Non | Type de réunion : 1=instantanée, 2=programmée, 3=récurrente sans heure fixe, 8=récurrente à heure fixe |
|
||||
@@ -59,7 +57,7 @@ Créer une nouvelle réunion Zoom
|
||||
| `joinBeforeHost` | boolean | Non | Autoriser les participants à rejoindre avant l'hôte |
|
||||
| `muteUponEntry` | boolean | Non | Mettre les participants en sourdine à l'entrée |
|
||||
| `waitingRoom` | boolean | Non | Activer la salle d'attente |
|
||||
| `autoRecording` | string | Non | Paramètre d'enregistrement automatique : local, cloud, ou none |
|
||||
| `autoRecording` | string | Non | Paramètre d'enregistrement automatique : local, cloud ou none |
|
||||
|
||||
#### Sortie
|
||||
|
||||
@@ -74,11 +72,11 @@ Lister toutes les réunions pour un utilisateur Zoom
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| --------- | ---- | ----------- | ----------- |
|
||||
| `userId` | string | Oui | L'ID utilisateur ou l'adresse e-mail. Utilisez "me" pour l'utilisateur authentifié. |
|
||||
| `type` | string | Non | Filtre de type de réunion : scheduled, live, upcoming, upcoming_meetings, ou previous_meetings |
|
||||
| `pageSize` | number | Non | Nombre d'enregistrements par page \(max 300\) |
|
||||
| `nextPageToken` | string | Non | Jeton pour la pagination pour obtenir la page suivante de résultats |
|
||||
| `nextPageToken` | string | Non | Jeton pour la pagination afin d'obtenir la page suivante des résultats |
|
||||
|
||||
#### Sortie
|
||||
|
||||
@@ -94,7 +92,7 @@ Obtenir les détails d'une réunion Zoom spécifique
|
||||
#### Entrée
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| --------- | ---- | ----------- | ----------- |
|
||||
| `meetingId` | string | Oui | L'ID de la réunion |
|
||||
| `occurrenceId` | string | Non | ID d'occurrence pour les réunions récurrentes |
|
||||
| `showPreviousOccurrences` | boolean | Non | Afficher les occurrences précédentes pour les réunions récurrentes |
|
||||
@@ -105,7 +103,7 @@ Obtenir les détails d'une réunion Zoom spécifique
|
||||
| --------- | ---- | ----------- |
|
||||
| `meeting` | object | Les détails de la réunion |
|
||||
|
||||
Obtenir les détails d'une réunion Zoom spécifique
|
||||
### `zoom_update_meeting`
|
||||
|
||||
Mettre à jour une réunion Zoom existante
|
||||
|
||||
@@ -115,7 +113,7 @@ Mettre à jour une réunion Zoom existante
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `meetingId` | string | Oui | L'ID de la réunion à mettre à jour |
|
||||
| `topic` | string | Non | Sujet de la réunion |
|
||||
| `type` | number | Non | Type de réunion : 1=instantanée, 2=programmée, 3=récurrente sans heure fixe, 8=récurrente à heure fixe |
|
||||
| `type` | number | Non | Type de réunion : 1=instantanée, 2=programmée, 3=récurrente sans horaire fixe, 8=récurrente avec horaire fixe |
|
||||
| `startTime` | string | Non | Heure de début de la réunion au format ISO 8601 \(ex., 2025-06-03T10:00:00Z\) |
|
||||
| `duration` | number | Non | Durée de la réunion en minutes |
|
||||
| `timezone` | string | Non | Fuseau horaire pour la réunion \(ex., America/Los_Angeles\) |
|
||||
@@ -134,7 +132,7 @@ Mettre à jour une réunion Zoom existante
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Indique si la réunion a été mise à jour avec succès |
|
||||
|
||||
Supprimer une réunion Zoom
|
||||
### `zoom_delete_meeting`
|
||||
|
||||
Supprimer ou annuler une réunion Zoom
|
||||
|
||||
@@ -161,13 +159,13 @@ Obtenir le texte d'invitation pour une réunion Zoom
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `meetingId` | string | Oui | L'ID de la réunion |
|
||||
| `meetingId` | string | Oui | L'identifiant de la réunion |
|
||||
|
||||
#### Sortie
|
||||
|
||||
| Paramètre | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `invitation` | string | Le texte d'invitation de la réunion |
|
||||
| `invitation` | string | Le texte d'invitation à la réunion |
|
||||
|
||||
### `zoom_list_recordings`
|
||||
|
||||
@@ -177,11 +175,11 @@ Lister tous les enregistrements cloud pour un utilisateur Zoom
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `userId` | string | Oui | L'ID utilisateur ou l'adresse e-mail. Utilisez "me" pour l'utilisateur authentifié. |
|
||||
| `from` | string | Non | Date de début au format aaaa-mm-jj (dans les 6 derniers mois) |
|
||||
| `userId` | string | Oui | L'identifiant ou l'adresse e-mail de l'utilisateur. Utilisez "me" pour l'utilisateur authentifié. |
|
||||
| `from` | string | Non | Date de début au format aaaa-mm-jj \(dans les 6 derniers mois\) |
|
||||
| `to` | string | Non | Date de fin au format aaaa-mm-jj |
|
||||
| `pageSize` | number | Non | Nombre d'enregistrements par page (max 300) |
|
||||
| `nextPageToken` | string | Non | Jeton pour la pagination pour obtenir la page suivante de résultats |
|
||||
| `pageSize` | number | Non | Nombre d'enregistrements par page \(max 300\) |
|
||||
| `nextPageToken` | string | Non | Jeton pour la pagination afin d'obtenir la page suivante des résultats |
|
||||
| `trash` | boolean | Non | Définir sur true pour lister les enregistrements de la corbeille |
|
||||
|
||||
#### Sortie
|
||||
@@ -191,7 +189,7 @@ Lister tous les enregistrements cloud pour un utilisateur Zoom
|
||||
| `recordings` | array | Liste des enregistrements |
|
||||
| `pageInfo` | object | Informations de pagination |
|
||||
|
||||
Obtenir tous les enregistrements pour une réunion Zoom spécifique
|
||||
### `zoom_get_meeting_recordings`
|
||||
|
||||
Obtenir tous les enregistrements pour une réunion Zoom spécifique
|
||||
|
||||
@@ -209,9 +207,9 @@ Obtenir tous les enregistrements pour une réunion Zoom spécifique
|
||||
| --------- | ---- | ----------- |
|
||||
| `recording` | object | L'enregistrement de la réunion avec tous les fichiers |
|
||||
|
||||
Supprimer les enregistrements cloud pour une réunion Zoom
|
||||
### `zoom_delete_recording`
|
||||
|
||||
Supprimer les enregistrements cloud pour une réunion Zoom
|
||||
Supprimer les enregistrements cloud d'une réunion Zoom
|
||||
|
||||
#### Entrée
|
||||
|
||||
@@ -227,7 +225,7 @@ Supprimer les enregistrements cloud pour une réunion Zoom
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | Si l'enregistrement a été supprimé avec succès |
|
||||
|
||||
Lister les participants d'une réunion Zoom passée
|
||||
### `zoom_list_past_participants`
|
||||
|
||||
Lister les participants d'une réunion Zoom passée
|
||||
|
||||
@@ -235,7 +233,7 @@ Lister les participants d'une réunion Zoom passée
|
||||
|
||||
| Paramètre | Type | Obligatoire | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `meetingId` | string | Oui | L'ID de la réunion passée ou l'UUID |
|
||||
| `meetingId` | string | Oui | L'ID ou l'UUID de la réunion passée |
|
||||
| `pageSize` | number | Non | Nombre d'enregistrements par page \(max 300\) |
|
||||
| `nextPageToken` | string | Non | Jeton pour la pagination pour obtenir la page suivante de résultats |
|
||||
|
||||
|
||||
@@ -48,13 +48,8 @@ Jiraをワークフローに統合します。課題の読み取り、書き込
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作のタイムスタンプ |
|
||||
| `issueKey` | string | 課題キー(例:PROJ-123) |
|
||||
| `summary` | string | 課題の要約 |
|
||||
| `description` | json | 課題の説明内容 |
|
||||
| `created` | string | 課題作成タイムスタンプ |
|
||||
| `updated` | string | 課題最終更新タイムスタンプ |
|
||||
| `issue` | json | すべてのフィールドを含む完全な課題オブジェクト |
|
||||
| `success` | boolean | 操作成功ステータス |
|
||||
| `output` | object | 課題キー、要約、説明、作成日時、更新日時を含むJira課題の詳細 |
|
||||
|
||||
### `jira_update`
|
||||
|
||||
@@ -78,9 +73,8 @@ Jira課題を更新する
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作のタイムスタンプ |
|
||||
| `issueKey` | string | 更新された課題キー(例:PROJ-123) |
|
||||
| `summary` | string | 更新後の課題要約 |
|
||||
| `success` | boolean | 操作成功ステータス |
|
||||
| `output` | object | タイムスタンプ、課題キー、要約、成功ステータスを含む更新されたJira課題の詳細 |
|
||||
|
||||
### `jira_write`
|
||||
|
||||
@@ -103,10 +97,8 @@ Jira課題を作成する
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作のタイムスタンプ |
|
||||
| `issueKey` | string | 作成された課題キー(例:PROJ-123) |
|
||||
| `summary` | string | 課題の要約 |
|
||||
| `url` | string | 作成された課題へのURL |
|
||||
| `success` | boolean | 操作成功ステータス |
|
||||
| `output` | object | タイムスタンプ、課題キー、要約、成功ステータス、URLを含む作成されたJira課題の詳細 |
|
||||
|
||||
### `jira_bulk_read`
|
||||
|
||||
@@ -124,7 +116,8 @@ Jira課題を作成する
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `issues` | array | タイムスタンプ、要約、説明、作成日時、更新日時を含むJira課題の配列 |
|
||||
| `success` | boolean | 操作成功ステータス |
|
||||
| `output` | array | 概要、説明、作成日時、更新日時を含むJiraの課題の配列 |
|
||||
|
||||
### `jira_delete_issue`
|
||||
|
||||
@@ -143,8 +136,8 @@ Jira課題を削除する
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作のタイムスタンプ |
|
||||
| `issueKey` | string | 削除された課題キー |
|
||||
| `success` | boolean | 操作成功ステータス |
|
||||
| `output` | object | タイムスタンプ、課題キー、成功ステータスを含む削除された課題の詳細 |
|
||||
|
||||
### `jira_assign_issue`
|
||||
|
||||
@@ -163,9 +156,8 @@ Jira課題をユーザーに割り当てる
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作のタイムスタンプ |
|
||||
| `issueKey` | string | 割り当てられた課題キー |
|
||||
| `assigneeId` | string | 担当者のアカウントID |
|
||||
| `success` | boolean | 操作成功ステータス |
|
||||
| `output` | object | タイムスタンプ、課題キー、担当者ID、成功ステータスを含む割り当ての詳細 |
|
||||
|
||||
### `jira_transition_issue`
|
||||
|
||||
@@ -185,9 +177,8 @@ Jira課題をワークフローステータス間で移動する(例:To Do
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作のタイムスタンプ |
|
||||
| `issueKey` | string | 遷移した課題キー |
|
||||
| `transitionId` | string | 適用されたトランジションID |
|
||||
| `success` | boolean | 操作成功ステータス |
|
||||
| `output` | object | タイムスタンプ、課題キー、移行ID、成功ステータスを含む移行の詳細 |
|
||||
|
||||
### `jira_search_issues`
|
||||
|
||||
@@ -208,11 +199,8 @@ JQL(Jira Query Language)を使用してJira課題を検索する
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作のタイムスタンプ |
|
||||
| `total` | number | 一致する課題の総数 |
|
||||
| `startAt` | number | ページネーション開始インデックス |
|
||||
| `maxResults` | number | ページあたりの最大結果数 |
|
||||
| `issues` | array | キー、要約、ステータス、担当者、作成日時、更新日時を含む一致する課題の配列 |
|
||||
| `success` | boolean | 操作成功ステータス |
|
||||
| `output` | object | タイムスタンプ、合計数、ページネーション詳細、一致する課題の配列を含む検索結果 |
|
||||
|
||||
### `jira_add_comment`
|
||||
|
||||
@@ -231,10 +219,8 @@ Jira課題にコメントを追加する
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作のタイムスタンプ |
|
||||
| `issueKey` | string | コメントが追加された課題キー |
|
||||
| `commentId` | string | 作成されたコメントID |
|
||||
| `body` | string | コメントのテキスト内容 |
|
||||
| `success` | boolean | 操作成功ステータス |
|
||||
| `output` | object | タイムスタンプ、課題キー、コメントID、本文、成功ステータスを含むコメント詳細 |
|
||||
|
||||
### `jira_get_comments`
|
||||
|
||||
@@ -254,10 +240,8 @@ Jira課題からすべてのコメントを取得する
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作のタイムスタンプ |
|
||||
| `issueKey` | string | 課題キー |
|
||||
| `total` | number | コメントの総数 |
|
||||
| `comments` | array | ID、作成者、本文、作成日時、更新日時を含むコメントの配列 |
|
||||
| `success` | boolean | 操作成功ステータス |
|
||||
| `output` | object | タイムスタンプ、課題キー、合計数、コメントの配列を含むコメントデータ |
|
||||
|
||||
### `jira_update_comment`
|
||||
|
||||
@@ -277,10 +261,8 @@ Jira課題の既存コメントを更新する
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作のタイムスタンプ |
|
||||
| `issueKey` | string | 課題キー |
|
||||
| `commentId` | string | 更新されたコメントID |
|
||||
| `body` | string | 更新されたコメントテキスト |
|
||||
| `success` | boolean | 操作成功ステータス |
|
||||
| `output` | object | タイムスタンプ、課題キー、コメントID、本文テキスト、成功ステータスを含む更新されたコメントの詳細 |
|
||||
|
||||
### `jira_delete_comment`
|
||||
|
||||
@@ -299,9 +281,8 @@ Jira課題からコメントを削除する
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作のタイムスタンプ |
|
||||
| `issueKey` | string | 課題キー |
|
||||
| `commentId` | string | 削除されたコメントID |
|
||||
| `success` | boolean | 操作成功ステータス |
|
||||
| `output` | object | タイムスタンプ、課題キー、コメントID、成功ステータスを含む削除の詳細 |
|
||||
|
||||
### `jira_get_attachments`
|
||||
|
||||
@@ -319,9 +300,8 @@ Jira課題からすべての添付ファイルを取得する
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作のタイムスタンプ |
|
||||
| `issueKey` | string | 課題キー |
|
||||
| `attachments` | array | ID、ファイル名、サイズ、MIMEタイプ、作成日時、作成者を含む添付ファイルの配列 |
|
||||
| `success` | boolean | 操作成功ステータス |
|
||||
| `output` | object | タイムスタンプ、課題キー、添付ファイルの配列を含む添付ファイルデータ |
|
||||
|
||||
### `jira_delete_attachment`
|
||||
|
||||
@@ -339,8 +319,8 @@ Jira課題から添付ファイルを削除する
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作のタイムスタンプ |
|
||||
| `attachmentId` | string | 削除された添付ファイルID |
|
||||
| `success` | boolean | 操作成功ステータス |
|
||||
| `output` | object | タイムスタンプ、添付ファイルID、成功ステータスを含む削除の詳細 |
|
||||
|
||||
### `jira_add_worklog`
|
||||
|
||||
@@ -361,10 +341,8 @@ Jira課題に作業時間記録エントリを追加する
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作のタイムスタンプ |
|
||||
| `issueKey` | string | 作業ログが追加された課題キー |
|
||||
| `worklogId` | string | 作成された作業ログID |
|
||||
| `timeSpentSeconds` | number | 秒単位の作業時間 |
|
||||
| `success` | boolean | 操作成功ステータス |
|
||||
| `output` | object | タイムスタンプ、課題キー、作業ログID、秒単位の作業時間、成功ステータスを含む作業ログの詳細 |
|
||||
|
||||
### `jira_get_worklogs`
|
||||
|
||||
@@ -384,10 +362,8 @@ Jira課題からすべての作業ログエントリを取得する
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作のタイムスタンプ |
|
||||
| `issueKey` | string | 課題キー |
|
||||
| `total` | number | 作業ログの総数 |
|
||||
| `worklogs` | array | ID、作成者、秒単位の作業時間、作業時間、コメント、作成日時、更新日時、開始日時を含む作業ログの配列 |
|
||||
| `success` | boolean | 操作成功ステータス |
|
||||
| `output` | object | タイムスタンプ、課題キー、合計数、作業ログの配列を含む作業ログデータ |
|
||||
|
||||
### `jira_update_worklog`
|
||||
|
||||
@@ -409,9 +385,8 @@ Jira課題の既存の作業ログエントリを更新する
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作のタイムスタンプ |
|
||||
| `issueKey` | string | 課題キー |
|
||||
| `worklogId` | string | 更新された作業ログID |
|
||||
| `success` | boolean | 操作成功ステータス |
|
||||
| `output` | object | タイムスタンプ、課題キー、作業ログID、成功ステータスを含む作業ログ更新の詳細 |
|
||||
|
||||
### `jira_delete_worklog`
|
||||
|
||||
@@ -430,9 +405,8 @@ Jira課題から作業ログエントリを削除する
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作のタイムスタンプ |
|
||||
| `issueKey` | string | 課題キー |
|
||||
| `worklogId` | string | 削除された作業ログID |
|
||||
| `success` | boolean | 操作成功ステータス |
|
||||
| `output` | object | タイムスタンプ、課題キー、作業ログID、成功ステータスを含む削除の詳細 |
|
||||
|
||||
### `jira_create_issue_link`
|
||||
|
||||
@@ -453,11 +427,8 @@ Jira課題から作業ログエントリを削除する
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作のタイムスタンプ |
|
||||
| `inwardIssue` | string | インワード課題キー |
|
||||
| `outwardIssue` | string | アウトワード課題キー |
|
||||
| `linkType` | string | 課題リンクのタイプ |
|
||||
| `linkId` | string | 作成されたリンクID |
|
||||
| `success` | boolean | 操作成功ステータス |
|
||||
| `output` | object | タイムスタンプ、インワード課題キー、アウトワード課題キー、リンクタイプ、成功ステータスを含む課題リンクの詳細 |
|
||||
|
||||
### `jira_delete_issue_link`
|
||||
|
||||
@@ -475,8 +446,8 @@ Jira課題から作業ログエントリを削除する
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作のタイムスタンプ |
|
||||
| `linkId` | string | 削除されたリンクID |
|
||||
| `success` | boolean | 操作成功ステータス |
|
||||
| `output` | object | タイムスタンプ、リンクID、成功ステータスを含む削除の詳細 |
|
||||
|
||||
### `jira_add_watcher`
|
||||
|
||||
@@ -495,9 +466,8 @@ Jira課題から作業ログエントリを削除する
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作のタイムスタンプ |
|
||||
| `issueKey` | string | 課題キー |
|
||||
| `watcherAccountId` | string | 追加されたウォッチャーのアカウントID |
|
||||
| `success` | boolean | 操作成功ステータス |
|
||||
| `output` | object | タイムスタンプ、課題キー、ウォッチャーアカウントID、成功ステータスを含むウォッチャーの詳細 |
|
||||
|
||||
### `jira_remove_watcher`
|
||||
|
||||
@@ -516,9 +486,8 @@ Jira課題からウォッチャーを削除する
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作のタイムスタンプ |
|
||||
| `issueKey` | string | 課題キー |
|
||||
| `watcherAccountId` | string | 削除されたウォッチャーのアカウントID |
|
||||
| `success` | boolean | 操作成功ステータス |
|
||||
| `output` | object | タイムスタンプ、課題キー、ウォッチャーアカウントID、成功ステータスを含む削除詳細 |
|
||||
|
||||
## 注意事項
|
||||
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
---
|
||||
title: ServiceNow
|
||||
description: ServiceNowレコードの作成、読み取り、更新、削除、一括インポート
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="servicenow"
|
||||
color="#032D42"
|
||||
/>
|
||||
|
||||
## 使用方法
|
||||
|
||||
ServiceNowをワークフローに統合します。任意のServiceNowテーブル(インシデント、タスク、ユーザーなど)のレコードを作成、読み取り、更新、削除できます。データ移行とETLのための一括インポート操作をサポートします。
|
||||
|
||||
## ツール
|
||||
|
||||
### `servicenow_create_record`
|
||||
|
||||
ServiceNowテーブルに新しいレコードを作成
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `instanceUrl` | string | はい | ServiceNowインスタンスURL(例:https://instance.service-now.com) |
|
||||
| `credential` | string | いいえ | ServiceNow OAuth認証情報ID |
|
||||
| `tableName` | string | はい | テーブル名(例:incident、task、sys_user) |
|
||||
| `fields` | json | はい | レコードに設定するフィールド(JSONオブジェクト) |
|
||||
|
||||
#### 出力
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `record` | json | sys_idおよびその他のフィールドを含む作成されたServiceNowレコード |
|
||||
| `metadata` | json | 操作メタデータ |
|
||||
|
||||
### `servicenow_read_record`
|
||||
|
||||
ServiceNowテーブルからレコードを読み取り
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `instanceUrl` | string | いいえ | ServiceNowインスタンスURL(指定されていない場合はOAuthから自動検出) |
|
||||
| `credential` | string | いいえ | ServiceNow OAuth認証情報ID |
|
||||
| `tableName` | string | はい | テーブル名 |
|
||||
| `sysId` | string | いいえ | 特定のレコードsys_id |
|
||||
| `number` | string | いいえ | レコード番号(例:INC0010001) |
|
||||
| `query` | string | いいえ | エンコードされたクエリ文字列(例:"active=true^priority=1") |
|
||||
| `limit` | number | いいえ | 返す最大レコード数 |
|
||||
| `fields` | string | いいえ | 返すフィールドのカンマ区切りリスト |
|
||||
|
||||
#### 出力
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `records` | array | ServiceNowレコードの配列 |
|
||||
| `metadata` | json | 操作メタデータ |
|
||||
|
||||
### `servicenow_update_record`
|
||||
|
||||
ServiceNowテーブル内の既存のレコードを更新します
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `instanceUrl` | string | いいえ | ServiceNowインスタンスURL(指定されていない場合はOAuthから自動検出) |
|
||||
| `credential` | string | いいえ | ServiceNow OAuth認証情報ID |
|
||||
| `tableName` | string | はい | テーブル名 |
|
||||
| `sysId` | string | はい | 更新するレコードのsys_id |
|
||||
| `fields` | json | はい | 更新するフィールド(JSONオブジェクト) |
|
||||
|
||||
#### 出力
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `record` | json | 更新されたServiceNowレコード |
|
||||
| `metadata` | json | 操作メタデータ |
|
||||
|
||||
### `servicenow_delete_record`
|
||||
|
||||
ServiceNowテーブルからレコードを削除します
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `instanceUrl` | string | いいえ | ServiceNowインスタンスURL(指定されていない場合はOAuthから自動検出) |
|
||||
| `credential` | string | いいえ | ServiceNow OAuth認証情報ID |
|
||||
| `tableName` | string | はい | テーブル名 |
|
||||
| `sysId` | string | はい | 削除するレコードのsys_id |
|
||||
|
||||
#### 出力
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | 削除が成功したかどうか |
|
||||
| `metadata` | json | 操作メタデータ |
|
||||
|
||||
## 注記
|
||||
|
||||
- カテゴリー: `tools`
|
||||
- タイプ: `servicenow`
|
||||
@@ -53,7 +53,7 @@ Slackをワークフローに統合します。メッセージの送信、更新
|
||||
|
||||
### `slack_message`
|
||||
|
||||
Slackチャンネルまたはダイレクトメッセージにメッセージを送信します。Slack mrkdwn形式をサポートしています。
|
||||
Slack APIを通じてSlackチャンネルまたはユーザーにメッセージを送信します。Slack mrkdwnフォーマットをサポートしています。
|
||||
|
||||
#### 入力
|
||||
|
||||
@@ -61,8 +61,7 @@ Slackチャンネルまたはダイレクトメッセージにメッセージを
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `authMethod` | string | いいえ | 認証方法:oauthまたはbot_token |
|
||||
| `botToken` | string | いいえ | カスタムボット用のボットトークン |
|
||||
| `channel` | string | いいえ | 対象のSlackチャンネル(例:#general) |
|
||||
| `userId` | string | いいえ | ダイレクトメッセージ用の対象SlackユーザーID(例:U1234567890) |
|
||||
| `channel` | string | はい | 対象のSlackチャンネル(例:#general) |
|
||||
| `text` | string | はい | 送信するメッセージテキスト(Slack mrkdwn形式をサポート) |
|
||||
| `thread_ts` | string | いいえ | 返信するスレッドのタイムスタンプ(スレッド返信を作成) |
|
||||
| `files` | file[] | いいえ | メッセージに添付するファイル |
|
||||
@@ -109,8 +108,7 @@ Slackチャンネルから最新のメッセージを読み取ります。フィ
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `authMethod` | string | いいえ | 認証方法:oauthまたはbot_token |
|
||||
| `botToken` | string | いいえ | カスタムボット用のボットトークン |
|
||||
| `channel` | string | いいえ | メッセージを読み取るSlackチャンネル(例:#general) |
|
||||
| `userId` | string | いいえ | DMの会話用のユーザーID(例:U1234567890) |
|
||||
| `channel` | string | はい | メッセージを読み取るSlackチャンネル(例:#general) |
|
||||
| `limit` | number | いいえ | 取得するメッセージ数(デフォルト:10、最大:100) |
|
||||
| `oldest` | string | いいえ | 時間範囲の開始(タイムスタンプ) |
|
||||
| `latest` | string | いいえ | 時間範囲の終了(タイムスタンプ) |
|
||||
|
||||
@@ -27,12 +27,10 @@ Simでは、Zoom統合によりエージェントがスケジュール設定と
|
||||
- 任意の会議の詳細や招待状を取得
|
||||
- 自動化から直接既存の会議を更新または削除
|
||||
|
||||
Zoomに接続するには、Zoomブロックをドロップして `Connect` をクリックし、Zoomアカウントで認証します。接続後、Zoomツールを使用してZoomミーティングの作成、一覧表示、更新、削除ができます。いつでも設定 > 統合から `Disconnect` をクリックしてZoomアカウントの接続を解除でき、Zoomアカウントへのアクセスは直ちに取り消されます。
|
||||
|
||||
これらの機能により、リモートコラボレーションの効率化、定期的なビデオセッションの自動化、組織のZoom環境の管理をワークフローの一部として行うことができます。
|
||||
これらの機能により、リモートコラボレーションの効率化、定期的なビデオセッションの自動化、ワークフローの一部として組織のZoom環境を管理することができます。
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
## 使用手順
|
||||
## 使用方法
|
||||
|
||||
Zoomをワークフローに統合します。Zoomミーティングの作成、一覧表示、更新、削除ができます。ミーティングの詳細、招待状、録画、参加者を取得します。クラウド録画をプログラムで管理します。
|
||||
|
||||
@@ -44,11 +42,11 @@ Zoomをワークフローに統合します。Zoomミーティングの作成、
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| パラメータ | 種類 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `userId` | string | はい | ユーザーIDまたはメールアドレス。認証済みユーザーの場合は「me」を使用。 |
|
||||
| `topic` | string | はい | ミーティングのトピック |
|
||||
| `type` | number | いいえ | ミーティングタイプ: 1=即時、2=予定済み、3=固定時間なしの定期的、8=固定時間の定期的 |
|
||||
| `type` | number | いいえ | ミーティングタイプ: 1=即時、2=予定、3=固定時間なしの定期的、8=固定時間の定期的 |
|
||||
| `startTime` | string | いいえ | ISO 8601形式のミーティング開始時間(例:2025-06-03T10:00:00Z) |
|
||||
| `duration` | number | いいえ | ミーティング時間(分) |
|
||||
| `timezone` | string | いいえ | ミーティングのタイムゾーン(例:America/Los_Angeles) |
|
||||
@@ -58,8 +56,8 @@ Zoomをワークフローに統合します。Zoomミーティングの作成、
|
||||
| `participantVideo` | boolean | いいえ | 参加者のビデオをオンにして開始 |
|
||||
| `joinBeforeHost` | boolean | いいえ | ホスト前の参加者の入室を許可 |
|
||||
| `muteUponEntry` | boolean | いいえ | 入室時に参加者をミュート |
|
||||
| `waitingRoom` | boolean | いいえ | 待機室を有効にする |
|
||||
| `autoRecording` | string | いいえ | 自動録画設定: local、cloud、またはnone |
|
||||
| `waitingRoom` | boolean | いいえ | 待機室を有効化 |
|
||||
| `autoRecording` | string | いいえ | 自動録画設定:local、cloud、またはnone |
|
||||
|
||||
#### 出力
|
||||
|
||||
@@ -69,14 +67,14 @@ Zoomをワークフローに統合します。Zoomミーティングの作成、
|
||||
|
||||
### `zoom_list_meetings`
|
||||
|
||||
Zoomユーザーのすべてのミーティングをリスト表示する
|
||||
Zoomユーザーのすべてのミーティングを一覧表示する
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `userId` | string | はい | ユーザーIDまたはメールアドレス。認証済みユーザーの場合は「me」を使用。 |
|
||||
| `type` | string | いいえ | ミーティングタイプフィルター: scheduled、live、upcoming、upcoming_meetings、またはprevious_meetings |
|
||||
| `type` | string | いいえ | ミーティングタイプフィルター: scheduled、live、upcoming、upcoming_meetings、または previous_meetings |
|
||||
| `pageSize` | number | いいえ | ページあたりのレコード数(最大300) |
|
||||
| `nextPageToken` | string | いいえ | 次のページの結果を取得するためのページネーショントークン |
|
||||
|
||||
@@ -96,8 +94,8 @@ Zoomユーザーのすべてのミーティングをリスト表示する
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `meetingId` | string | はい | ミーティングID |
|
||||
| `occurrenceId` | string | いいえ | 定期的なミーティングの発生ID |
|
||||
| `showPreviousOccurrences` | boolean | いいえ | 定期的なミーティングの過去の発生を表示 |
|
||||
| `occurrenceId` | string | いいえ | 定期的なミーティングの開催ID |
|
||||
| `showPreviousOccurrences` | boolean | いいえ | 定期的なミーティングの過去の開催を表示 |
|
||||
|
||||
#### 出力
|
||||
|
||||
@@ -105,17 +103,17 @@ Zoomユーザーのすべてのミーティングをリスト表示する
|
||||
| --------- | ---- | ----------- |
|
||||
| `meeting` | object | ミーティングの詳細 |
|
||||
|
||||
特定のZoomミーティングの詳細を取得する
|
||||
### `zoom_update_meeting`
|
||||
|
||||
既存のZoomミーティングを更新する
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 種類 | 必須 | 説明 |
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `meetingId` | string | はい | 更新するミーティングID |
|
||||
| `topic` | string | いいえ | ミーティングのトピック |
|
||||
| `type` | number | いいえ | ミーティングタイプ: 1=即時、2=予定、3=固定時間なしの定期的、8=固定時間の定期的 |
|
||||
| `type` | number | いいえ | ミーティングタイプ: 1=インスタント、2=予定済み、3=定期的(固定時間なし)、8=定期的(固定時間) |
|
||||
| `startTime` | string | いいえ | ISO 8601形式のミーティング開始時間(例:2025-06-03T10:00:00Z) |
|
||||
| `duration` | number | いいえ | ミーティング時間(分) |
|
||||
| `timezone` | string | いいえ | ミーティングのタイムゾーン(例:America/Los_Angeles) |
|
||||
@@ -123,10 +121,10 @@ Zoomユーザーのすべてのミーティングをリスト表示する
|
||||
| `agenda` | string | いいえ | ミーティングの議題 |
|
||||
| `hostVideo` | boolean | いいえ | ホストのビデオをオンにして開始 |
|
||||
| `participantVideo` | boolean | いいえ | 参加者のビデオをオンにして開始 |
|
||||
| `joinBeforeHost` | boolean | いいえ | ホスト前の参加者の入室を許可 |
|
||||
| `joinBeforeHost` | boolean | いいえ | ホストより前の参加者の入室を許可 |
|
||||
| `muteUponEntry` | boolean | いいえ | 入室時に参加者をミュート |
|
||||
| `waitingRoom` | boolean | いいえ | 待機室を有効化 |
|
||||
| `autoRecording` | string | いいえ | 自動録画設定:local、cloud、またはnone |
|
||||
| `waitingRoom` | boolean | いいえ | 待機室を有効にする |
|
||||
| `autoRecording` | string | いいえ | 自動録画設定:ローカル、クラウド、または無し |
|
||||
|
||||
#### 出力
|
||||
|
||||
@@ -134,17 +132,17 @@ Zoomユーザーのすべてのミーティングをリスト表示する
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | ミーティングが正常に更新されたかどうか |
|
||||
|
||||
Zoomミーティングを削除またはキャンセルする
|
||||
### `zoom_delete_meeting`
|
||||
|
||||
Zoomミーティングを削除またはキャンセルする
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 種類 | 必須 | 説明 |
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `meetingId` | string | はい | 削除するミーティングID |
|
||||
| `occurrenceId` | string | いいえ | 定期的なミーティングの特定の回を削除するための回数ID |
|
||||
| `scheduleForReminder` | boolean | いいえ | 登録者にキャンセルのリマインダーメールを送信 |
|
||||
| `occurrenceId` | string | いいえ | 定期的なミーティングの特定の回を削除するための発生ID |
|
||||
| `scheduleForReminder` | boolean | いいえ | 登録者にキャンセルリマインダーメールを送信 |
|
||||
| `cancelMeetingReminder` | boolean | いいえ | 登録者と代替ホストにキャンセルメールを送信 |
|
||||
|
||||
#### 出力
|
||||
@@ -171,18 +169,18 @@ Zoomミーティングの招待テキストを取得する
|
||||
|
||||
### `zoom_list_recordings`
|
||||
|
||||
Zoomユーザーのすべてのクラウド録画を一覧表示する
|
||||
Zoomユーザーのすべてのクラウド録画をリスト表示する
|
||||
|
||||
#### 入力
|
||||
|
||||
| パラメータ | 型 | 必須 | 説明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `userId` | string | はい | ユーザーIDまたはメールアドレス。認証済みユーザーの場合は「me」を使用。 |
|
||||
| `from` | string | いいえ | 開始日(yyyy-mm-dd形式)(過去6ヶ月以内) |
|
||||
| `from` | string | いいえ | 開始日(yyyy-mm-dd形式)\(過去6ヶ月以内\) |
|
||||
| `to` | string | いいえ | 終了日(yyyy-mm-dd形式) |
|
||||
| `pageSize` | number | いいえ | 1ページあたりのレコード数(最大300) |
|
||||
| `pageSize` | number | いいえ | ページあたりのレコード数(最大300) |
|
||||
| `nextPageToken` | string | いいえ | 次のページの結果を取得するためのページネーショントークン |
|
||||
| `trash` | boolean | いいえ | ゴミ箱から録画を一覧表示するにはtrueに設定 |
|
||||
| `trash` | boolean | いいえ | ゴミ箱から録画をリスト表示するにはtrueに設定 |
|
||||
|
||||
#### 出力
|
||||
|
||||
@@ -191,9 +189,9 @@ Zoomユーザーのすべてのクラウド録画を一覧表示する
|
||||
| `recordings` | array | 録画のリスト |
|
||||
| `pageInfo` | object | ページネーション情報 |
|
||||
|
||||
特定のZoomミーティングのすべての録画を取得する
|
||||
### `zoom_get_meeting_recordings`
|
||||
|
||||
特定のZoomミーティングのすべての録画を取得する
|
||||
特定のZoomミーティングの全ての録画を取得する
|
||||
|
||||
#### 入力
|
||||
|
||||
@@ -209,7 +207,7 @@ Zoomユーザーのすべてのクラウド録画を一覧表示する
|
||||
| --------- | ---- | ----------- |
|
||||
| `recording` | object | すべてのファイルを含むミーティング録画 |
|
||||
|
||||
Zoomミーティングのクラウド録画を削除する
|
||||
### `zoom_delete_recording`
|
||||
|
||||
Zoomミーティングのクラウド録画を削除する
|
||||
|
||||
@@ -227,7 +225,7 @@ Zoomミーティングのクラウド録画を削除する
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | 録画が正常に削除されたかどうか |
|
||||
|
||||
過去のZoomミーティングの参加者を一覧表示する
|
||||
### `zoom_list_past_participants`
|
||||
|
||||
過去のZoomミーティングの参加者を一覧表示する
|
||||
|
||||
@@ -237,16 +235,16 @@ Zoomミーティングのクラウド録画を削除する
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `meetingId` | string | はい | 過去のミーティングIDまたはUUID |
|
||||
| `pageSize` | number | いいえ | ページあたりのレコード数(最大300) |
|
||||
| `nextPageToken` | string | いいえ | 結果の次のページを取得するための改ページトークン |
|
||||
| `nextPageToken` | string | いいえ | 結果の次のページを取得するためのページネーショントークン |
|
||||
|
||||
#### 出力
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `participants` | array | ミーティング参加者のリスト |
|
||||
| `participants` | array | 会議参加者のリスト |
|
||||
| `pageInfo` | object | ページネーション情報 |
|
||||
|
||||
## 注意事項
|
||||
|
||||
- カテゴリ: `tools`
|
||||
- カテゴリー: `tools`
|
||||
- タイプ: `zoom`
|
||||
|
||||
@@ -48,13 +48,8 @@ Jira 的主要功能包括:
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | 字符串 | 操作的时间戳 |
|
||||
| `issueKey` | 字符串 | 问题键 \(例如:PROJ-123\) |
|
||||
| `summary` | 字符串 | 问题摘要 |
|
||||
| `description` | JSON | 问题描述内容 |
|
||||
| `created` | 字符串 | 问题创建的时间戳 |
|
||||
| `updated` | 字符串 | 问题最后更新的时间戳 |
|
||||
| `issue` | JSON | 包含所有字段的完整问题对象 |
|
||||
| `success` | 布尔值 | 操作成功状态 |
|
||||
| `output` | 对象 | 包含问题键、摘要、描述、创建和更新时间戳的 Jira 问题详细信息 |
|
||||
|
||||
### `jira_update`
|
||||
|
||||
@@ -78,9 +73,8 @@ Jira 的主要功能包括:
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | 字符串 | 操作的时间戳 |
|
||||
| `issueKey` | 字符串 | 更新后的问题键 \(例如:PROJ-123\) |
|
||||
| `summary` | 字符串 | 更新后的问题摘要 |
|
||||
| `success` | 布尔值 | 操作成功状态 |
|
||||
| `output` | 对象 | 更新的 Jira 问题详情,包括时间戳、问题键、摘要和成功状态 |
|
||||
|
||||
### `jira_write`
|
||||
|
||||
@@ -103,10 +97,8 @@ Jira 的主要功能包括:
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | 字符串 | 操作的时间戳 |
|
||||
| `issueKey` | 字符串 | 创建的问题键 \(例如:PROJ-123\) |
|
||||
| `summary` | 字符串 | 问题摘要 |
|
||||
| `url` | 字符串 | 创建的问题的 URL |
|
||||
| `success` | 布尔值 | 操作成功状态 |
|
||||
| `output` | 对象 | 创建的 Jira 问题详情,包括时间戳、问题键、摘要、成功状态和 URL |
|
||||
|
||||
### `jira_bulk_read`
|
||||
|
||||
@@ -124,7 +116,8 @@ Jira 的主要功能包括:
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `issues` | 数组 | 包含时间戳、摘要、描述、创建和更新时间戳的 Jira 问题数组 |
|
||||
| `success` | boolean | 操作成功状态 |
|
||||
| `output` | array | 包含 Jira 问题的数组,包括摘要、描述、创建和更新的时间戳 |
|
||||
|
||||
### `jira_delete_issue`
|
||||
|
||||
@@ -143,8 +136,8 @@ Jira 的主要功能包括:
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | 字符串 | 操作的时间戳 |
|
||||
| `issueKey` | 字符串 | 删除的问题键 |
|
||||
| `success` | 布尔值 | 操作成功状态 |
|
||||
| `output` | 对象 | 删除的问题详情,包括时间戳、问题键和成功状态 |
|
||||
|
||||
### `jira_assign_issue`
|
||||
|
||||
@@ -163,9 +156,8 @@ Jira 的主要功能包括:
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | 字符串 | 操作的时间戳 |
|
||||
| `issueKey` | 字符串 | 被分配的任务键 |
|
||||
| `assigneeId` | 字符串 | 分配者的账户 ID |
|
||||
| `success` | 布尔值 | 操作成功状态 |
|
||||
| `output` | 对象 | 分配详情,包括时间戳、问题键、分配人 ID 和成功状态 |
|
||||
|
||||
### `jira_transition_issue`
|
||||
|
||||
@@ -185,9 +177,8 @@ Jira 的主要功能包括:
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作的时间戳 |
|
||||
| `issueKey` | string | 已转换的问题键 |
|
||||
| `transitionId` | string | 应用的转换 ID |
|
||||
| `success` | 布尔值 | 操作成功状态 |
|
||||
| `output` | 对象 | 转换详情,包括时间戳、问题键、转换 ID 和成功状态 |
|
||||
|
||||
### `jira_search_issues`
|
||||
|
||||
@@ -208,11 +199,8 @@ Jira 的主要功能包括:
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作的时间戳 |
|
||||
| `total` | number | 匹配问题的总数 |
|
||||
| `startAt` | number | 分页起始索引 |
|
||||
| `maxResults` | number | 每页的最大结果数 |
|
||||
| `issues` | array | 包含键、摘要、状态、负责人、创建时间和更新时间的匹配问题数组 |
|
||||
| `success` | 布尔值 | 操作成功状态 |
|
||||
| `output` | 对象 | 搜索结果,包括时间戳、总数、分页详情和匹配问题的数组 |
|
||||
|
||||
### `jira_add_comment`
|
||||
|
||||
@@ -231,10 +219,8 @@ Jira 的主要功能包括:
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作的时间戳 |
|
||||
| `issueKey` | string | 添加评论的问题键 |
|
||||
| `commentId` | string | 创建的评论 ID |
|
||||
| `body` | string | 评论的文本内容 |
|
||||
| `success` | 布尔值 | 操作成功状态 |
|
||||
| `output` | 对象 | 评论详情,包括时间戳、问题键、评论 ID、正文和成功状态 |
|
||||
|
||||
### `jira_get_comments`
|
||||
|
||||
@@ -254,10 +240,8 @@ Jira 的主要功能包括:
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作的时间戳 |
|
||||
| `issueKey` | string | 问题键 |
|
||||
| `total` | number | 评论的总数 |
|
||||
| `comments` | array | 包含 ID、作者、正文、创建时间和更新时间的评论数组 |
|
||||
| `success` | 布尔值 | 操作成功状态 |
|
||||
| `output` | 对象 | 评论数据,包括时间戳、问题键、总数和评论数组 |
|
||||
|
||||
### `jira_update_comment`
|
||||
|
||||
@@ -277,10 +261,8 @@ Jira 的主要功能包括:
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作的时间戳 |
|
||||
| `issueKey` | string | 问题键 |
|
||||
| `commentId` | string | 更新的评论 ID |
|
||||
| `body` | string | 更新的评论文本 |
|
||||
| `success` | 布尔值 | 操作成功状态 |
|
||||
| `output` | 对象 | 更新的评论详情,包括时间戳、问题键、评论 ID、正文文本和成功状态 |
|
||||
|
||||
### `jira_delete_comment`
|
||||
|
||||
@@ -299,9 +281,8 @@ Jira 的主要功能包括:
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作的时间戳 |
|
||||
| `issueKey` | string | 问题键 |
|
||||
| `commentId` | string | 已删除的评论 ID |
|
||||
| `success` | 布尔值 | 操作成功状态 |
|
||||
| `output` | 对象 | 删除详情,包括时间戳、问题键、评论 ID 和成功状态 |
|
||||
|
||||
### `jira_get_attachments`
|
||||
|
||||
@@ -319,9 +300,8 @@ Jira 的主要功能包括:
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作的时间戳 |
|
||||
| `issueKey` | string | 问题键 |
|
||||
| `attachments` | array | 附件数组,包括 id、文件名、大小、mimeType、创建时间、作者 |
|
||||
| `success` | 布尔值 | 操作成功状态 |
|
||||
| `output` | 对象 | 附件数据,包括时间戳、问题键和附件数组 |
|
||||
|
||||
### `jira_delete_attachment`
|
||||
|
||||
@@ -339,8 +319,8 @@ Jira 的主要功能包括:
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作的时间戳 |
|
||||
| `attachmentId` | string | 已删除的附件 ID |
|
||||
| `success` | 布尔值 | 操作成功状态 |
|
||||
| `output` | 对象 | 删除详情,包括时间戳、附件 ID 和成功状态 |
|
||||
|
||||
### `jira_add_worklog`
|
||||
|
||||
@@ -361,10 +341,8 @@ Jira 的主要功能包括:
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作的时间戳 |
|
||||
| `issueKey` | string | 添加工作日志的相关问题键 |
|
||||
| `worklogId` | string | 创建的工作日志 ID |
|
||||
| `timeSpentSeconds` | number | 花费的时间(以秒为单位) |
|
||||
| `success` | 布尔值 | 操作成功状态 |
|
||||
| `output` | 对象 | 工作日志详情,包括时间戳、问题键、工作日志 ID、花费的时间(以秒为单位)和成功状态 |
|
||||
|
||||
### `jira_get_worklogs`
|
||||
|
||||
@@ -384,10 +362,8 @@ Jira 的主要功能包括:
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作的时间戳 |
|
||||
| `issueKey` | string | 问题键 |
|
||||
| `total` | number | 工作日志的总数 |
|
||||
| `worklogs` | array | 工作日志数组,包括 id、作者、timeSpentSeconds、timeSpent、评论、创建时间、更新时间、开始时间 |
|
||||
| `success` | 布尔值 | 操作成功状态 |
|
||||
| `output` | 对象 | 包含时间戳、问题键、总数和工作日志数组的工作日志数据 |
|
||||
|
||||
### `jira_update_worklog`
|
||||
|
||||
@@ -409,9 +385,8 @@ Jira 的主要功能包括:
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作的时间戳 |
|
||||
| `issueKey` | string | 问题键 |
|
||||
| `worklogId` | string | 更新的工作日志 ID |
|
||||
| `success` | 布尔值 | 操作成功状态 |
|
||||
| `output` | 对象 | 包含时间戳、问题键、工作日志 ID 和成功状态的工作日志更新详情 |
|
||||
|
||||
### `jira_delete_worklog`
|
||||
|
||||
@@ -430,9 +405,8 @@ Jira 的主要功能包括:
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作的时间戳 |
|
||||
| `issueKey` | string | 问题键 |
|
||||
| `worklogId` | string | 已删除的工作日志 ID |
|
||||
| `success` | 布尔值 | 操作成功状态 |
|
||||
| `output` | 对象 | 删除详情,包括时间戳、问题键、工作日志 ID 和成功状态 |
|
||||
|
||||
### `jira_create_issue_link`
|
||||
|
||||
@@ -453,11 +427,8 @@ Jira 的主要功能包括:
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作的时间戳 |
|
||||
| `inwardIssue` | string | 内部问题键 |
|
||||
| `outwardIssue` | string | 外部问题键 |
|
||||
| `linkType` | string | 问题链接的类型 |
|
||||
| `linkId` | string | 创建的链接 ID |
|
||||
| `success` | 布尔值 | 操作成功状态 |
|
||||
| `output` | 对象 | 问题链接详情,包括时间戳、内部问题键、外部问题键、链接类型和成功状态 |
|
||||
|
||||
### `jira_delete_issue_link`
|
||||
|
||||
@@ -475,8 +446,8 @@ Jira 的主要功能包括:
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作的时间戳 |
|
||||
| `linkId` | string | 已删除的链接 ID |
|
||||
| `success` | 布尔值 | 操作成功状态 |
|
||||
| `output` | 对象 | 删除详情,包括时间戳、链接 ID 和成功状态 |
|
||||
|
||||
### `jira_add_watcher`
|
||||
|
||||
@@ -495,9 +466,8 @@ Jira 的主要功能包括:
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作的时间戳 |
|
||||
| `issueKey` | string | 问题键 |
|
||||
| `watcherAccountId` | string | 添加的观察者账户 ID |
|
||||
| `success` | 布尔值 | 操作成功状态 |
|
||||
| `output` | 对象 | 观察者详情,包括时间戳、问题键、观察者账户 ID 和成功状态 |
|
||||
|
||||
### `jira_remove_watcher`
|
||||
|
||||
@@ -516,9 +486,8 @@ Jira 的主要功能包括:
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `ts` | string | 操作的时间戳 |
|
||||
| `issueKey` | string | 问题键 |
|
||||
| `watcherAccountId` | string | 移除的观察者账户 ID |
|
||||
| `success` | 布尔值 | 操作成功状态 |
|
||||
| `output` | 对象 | 移除详情,包括时间戳、问题键、观察者账户 ID 和成功状态 |
|
||||
|
||||
## 注意事项
|
||||
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
---
|
||||
title: ServiceNow
|
||||
description: 创建、读取、更新、删除及批量导入 ServiceNow 记录
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="servicenow"
|
||||
color="#032D42"
|
||||
/>
|
||||
|
||||
## 使用说明
|
||||
|
||||
将 ServiceNow 集成到您的工作流程中。可在任意 ServiceNow 表(如事件、任务、用户等)中创建、读取、更新和删除记录。支持批量导入操作,便于数据迁移和 ETL。
|
||||
|
||||
## 工具
|
||||
|
||||
### `servicenow_create_record`
|
||||
|
||||
在 ServiceNow 表中创建新记录
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `instanceUrl` | string | 是 | ServiceNow 实例 URL(例如:https://instance.service-now.com) |
|
||||
| `credential` | string | 否 | ServiceNow OAuth 凭证 ID |
|
||||
| `tableName` | string | 是 | 表名(例如:incident、task、sys_user) |
|
||||
| `fields` | json | 是 | 要设置在记录上的字段(JSON 对象) |
|
||||
|
||||
#### 输出
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `record` | json | 创建的 ServiceNow 记录,包含 sys_id 及其他字段 |
|
||||
| `metadata` | json | 操作元数据 |
|
||||
|
||||
### `servicenow_read_record`
|
||||
|
||||
从 ServiceNow 表中读取记录
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `instanceUrl` | string | 否 | ServiceNow 实例 URL(如未提供,将通过 OAuth 自动检测) |
|
||||
| `credential` | string | 否 | ServiceNow OAuth 凭证 ID |
|
||||
| `tableName` | string | 是 | 表名 |
|
||||
| `sysId` | string | 否 | 指定记录 sys_id |
|
||||
| `number` | string | 否 | 记录编号(例如:INC0010001) |
|
||||
| `query` | string | 否 | 编码查询字符串(例如:"active=true^priority=1") |
|
||||
| `limit` | number | 否 | 返回的最大记录数 |
|
||||
| `fields` | string | 否 | 要返回的字段列表(以逗号分隔) |
|
||||
|
||||
#### 输出
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `records` | array | ServiceNow 记录数组 |
|
||||
| `metadata` | json | 操作元数据 |
|
||||
|
||||
### `servicenow_update_record`
|
||||
|
||||
更新 ServiceNow 表中的现有记录
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 是否必填 | 描述 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `instanceUrl` | string | 否 | ServiceNow 实例 URL(如果未提供,将通过 OAuth 自动检测) |
|
||||
| `credential` | string | 否 | ServiceNow OAuth 凭证 ID |
|
||||
| `tableName` | string | 是 | 表名 |
|
||||
| `sysId` | string | 是 | 要更新的记录 sys_id |
|
||||
| `fields` | json | 是 | 要更新的字段(JSON 对象) |
|
||||
|
||||
#### 输出
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `record` | json | 已更新的 ServiceNow 记录 |
|
||||
| `metadata` | json | 操作元数据 |
|
||||
|
||||
### `servicenow_delete_record`
|
||||
|
||||
从 ServiceNow 表中删除记录
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 是否必填 | 描述 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `instanceUrl` | string | 否 | ServiceNow 实例 URL(如果未提供,将通过 OAuth 自动检测) |
|
||||
| `credential` | string | 否 | ServiceNow OAuth 凭证 ID |
|
||||
| `tableName` | string | 是 | 表名 |
|
||||
| `sysId` | string | 是 | 要删除的记录 sys_id |
|
||||
|
||||
#### 输出
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | 删除是否成功 |
|
||||
| `metadata` | json | 操作元数据 |
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 分类:`tools`
|
||||
- 类型:`servicenow`
|
||||
@@ -52,7 +52,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
### `slack_message`
|
||||
|
||||
向 Slack 频道或直接消息发送消息。支持 Slack mrkdwn 格式。
|
||||
通过 Slack API 向 Slack 频道或用户发送消息。支持 Slack mrkdwn 格式化。
|
||||
|
||||
#### 输入
|
||||
|
||||
@@ -60,11 +60,10 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `authMethod` | string | 否 | 认证方法:oauth 或 bot_token |
|
||||
| `botToken` | string | 否 | 自定义 Bot 的令牌 |
|
||||
| `channel` | string | 否 | 目标 Slack 频道(例如,#general) |
|
||||
| `userId` | string | 否 | 目标 Slack 用户 ID,用于直接消息(例如,U1234567890) |
|
||||
| `channel` | string | 是 | 目标 Slack 频道(例如,#general) |
|
||||
| `text` | string | 是 | 要发送的消息文本(支持 Slack mrkdwn 格式) |
|
||||
| `thread_ts` | string | 否 | 回复的线程时间戳(创建线程回复) |
|
||||
| `files` | file[] | 否 | 附加到消息的文件 |
|
||||
| `thread_ts` | string | 否 | 要回复的线程时间戳(创建线程回复) |
|
||||
| `files` | file[] | 否 | 要附加到消息的文件 |
|
||||
|
||||
#### 输出
|
||||
|
||||
@@ -104,12 +103,11 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 是否必需 | 描述 |
|
||||
| 参数 | 类型 | 必需 | 描述 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `authMethod` | string | 否 | 认证方法:oauth 或 bot_token |
|
||||
| `botToken` | string | 否 | 自定义 Bot 的令牌 |
|
||||
| `channel` | string | 否 | 要读取消息的 Slack 频道(例如,#general) |
|
||||
| `userId` | string | 否 | DM 对话的用户 ID(例如,U1234567890) |
|
||||
| `botToken` | string | 否 | 自定义 Bot 的 Bot token |
|
||||
| `channel` | string | 是 | 要读取消息的 Slack 频道(例如:#general) |
|
||||
| `limit` | number | 否 | 要检索的消息数量(默认:10,最大:100) |
|
||||
| `oldest` | string | 否 | 时间范围的开始(时间戳) |
|
||||
| `latest` | string | 否 | 时间范围的结束(时间戳) |
|
||||
|
||||
@@ -27,14 +27,12 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
- 检索任何会议的详情或邀请
|
||||
- 直接从您的自动化中更新或删除现有会议
|
||||
|
||||
要连接到 Zoom,请拖放 Zoom 模块并点击 `Connect`,使用您的 Zoom 账户进行认证。连接后,您可以使用 Zoom 工具创建、列出、更新和删除 Zoom 会议。您可以随时通过点击“设置 > 集成”中的 `Disconnect` 断开您的 Zoom 账户连接,您的 Zoom 账户访问权限将立即被撤销。
|
||||
|
||||
这些功能使您能够简化远程协作、自动化定期视频会议,并在工作流中管理您的组织的 Zoom 环境。
|
||||
这些功能使您能够简化远程协作、自动化定期视频会议,并将您的组织 Zoom 环境管理整合到工作流程中。
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
## 使用说明
|
||||
|
||||
将 Zoom 集成到工作流中。创建、列出、更新和删除 Zoom 会议。获取会议详情、邀请、录制和参与者信息。以编程方式管理云录制。
|
||||
将 Zoom 集成到工作流程中。创建、列出、更新和删除 Zoom 会议。获取会议详情、邀请、录制内容和参与者信息。以编程方式管理云录制内容。
|
||||
|
||||
## 工具
|
||||
|
||||
@@ -48,18 +46,18 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `userId` | string | 是 | 用户 ID 或电子邮件地址。使用 "me" 表示已认证用户。 |
|
||||
| `topic` | string | 是 | 会议主题 |
|
||||
| `type` | number | 否 | 会议类型:1=即时会议,2=预定会议,3=无固定时间的循环会议,8=有固定时间的循环会议 |
|
||||
| `startTime` | string | 否 | 会议开始时间,采用 ISO 8601 格式 \(例如:2025-06-03T10:00:00Z\) |
|
||||
| `type` | number | 否 | 会议类型:1=即时,2=计划,3=无固定时间的定期会议,8=有固定时间的定期会议 |
|
||||
| `startTime` | string | 否 | ISO 8601 格式的会议开始时间 \(例如:2025-06-03T10:00:00Z\) |
|
||||
| `duration` | number | 否 | 会议时长(分钟) |
|
||||
| `timezone` | string | 否 | 会议的时区 \(例如:America/Los_Angeles\) |
|
||||
| `password` | string | 否 | 会议密码 |
|
||||
| `agenda` | string | 否 | 会议议程 |
|
||||
| `hostVideo` | boolean | 否 | 主持人视频开启时开始会议 |
|
||||
| `participantVideo` | boolean | 否 | 参与者视频开启时开始会议 |
|
||||
| `joinBeforeHost` | boolean | 否 | 允许参与者在主持人之前加入会议 |
|
||||
| `hostVideo` | boolean | 否 | 主持人视频开启时开始 |
|
||||
| `participantVideo` | boolean | 否 | 参与者视频开启时开始 |
|
||||
| `joinBeforeHost` | boolean | 否 | 允许参与者在主持人之前加入 |
|
||||
| `muteUponEntry` | boolean | 否 | 参与者进入时静音 |
|
||||
| `waitingRoom` | boolean | 否 | 启用等候室 |
|
||||
| `autoRecording` | string | 否 | 自动录制设置:本地、云端或无 |
|
||||
| `autoRecording` | string | 否 | 自动录制设置:本地、云或无 |
|
||||
|
||||
#### 输出
|
||||
|
||||
@@ -96,14 +94,14 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
| 参数 | 类型 | 必需 | 描述 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `meetingId` | string | 是 | 会议 ID |
|
||||
| `occurrenceId` | string | 否 | 定期会议的发生 ID |
|
||||
| `showPreviousOccurrences` | boolean | 否 | 显示定期会议的先前发生记录 |
|
||||
| `occurrenceId` | string | 否 | 循环会议的发生 ID |
|
||||
| `showPreviousOccurrences` | boolean | 否 | 显示循环会议的先前发生记录 |
|
||||
|
||||
#### 输出
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `meeting` | object | 会议详细信息 |
|
||||
| `meeting` | object | 会议详情 |
|
||||
|
||||
### `zoom_update_meeting`
|
||||
|
||||
@@ -115,7 +113,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `meetingId` | string | 是 | 要更新的会议 ID |
|
||||
| `topic` | string | 否 | 会议主题 |
|
||||
| `type` | number | 否 | 会议类型:1=即时,2=计划,3=无固定时间的定期会议,8=有固定时间的定期会议 |
|
||||
| `type` | number | 否 | 会议类型:1=即时会议,2=预定会议,3=无固定时间的循环会议,8=固定时间的循环会议 |
|
||||
| `startTime` | string | 否 | ISO 8601 格式的会议开始时间 \(例如:2025-06-03T10:00:00Z\) |
|
||||
| `duration` | number | 否 | 会议时长(分钟) |
|
||||
| `timezone` | string | 否 | 会议的时区 \(例如:America/Los_Angeles\) |
|
||||
@@ -126,7 +124,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
| `joinBeforeHost` | boolean | 否 | 允许参与者在主持人之前加入 |
|
||||
| `muteUponEntry` | boolean | 否 | 参与者进入时静音 |
|
||||
| `waitingRoom` | boolean | 否 | 启用等候室 |
|
||||
| `autoRecording` | string | 否 | 自动录制设置:本地、云或无 |
|
||||
| `autoRecording` | string | 否 | 自动录制设置:本地、云端或无 |
|
||||
|
||||
#### 输出
|
||||
|
||||
@@ -143,7 +141,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
| 参数 | 类型 | 必需 | 描述 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `meetingId` | string | 是 | 要删除的会议 ID |
|
||||
| `occurrenceId` | string | 否 | 删除定期会议的特定场次的场次 ID |
|
||||
| `occurrenceId` | string | 否 | 删除循环会议中特定场次的场次 ID |
|
||||
| `scheduleForReminder` | boolean | 否 | 向注册者发送取消提醒邮件 |
|
||||
| `cancelMeetingReminder` | boolean | 否 | 向注册者和替代主持人发送取消邮件 |
|
||||
|
||||
@@ -177,10 +175,10 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
| 参数 | 类型 | 必需 | 描述 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `userId` | string | 是 | 用户 ID 或电子邮件地址。使用 "me" 表示已认证用户。 |
|
||||
| `from` | string | 否 | 开始日期,格式为 yyyy-mm-dd \(最近 6 个月内\) |
|
||||
| `userId` | string | 是 | 用户 ID 或电子邮件地址。对于已认证用户,请使用 "me"。 |
|
||||
| `from` | string | 否 | 开始日期,格式为 yyyy-mm-dd(最近 6 个月内) |
|
||||
| `to` | string | 否 | 结束日期,格式为 yyyy-mm-dd |
|
||||
| `pageSize` | number | 否 | 每页记录数 \(最大 300\) |
|
||||
| `pageSize` | number | 否 | 每页记录数(最大 300) |
|
||||
| `nextPageToken` | string | 否 | 分页令牌,用于获取下一页结果 |
|
||||
| `trash` | boolean | 否 | 设置为 true 以列出回收站中的录制 |
|
||||
|
||||
@@ -193,39 +191,39 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
### `zoom_get_meeting_recordings`
|
||||
|
||||
获取特定 Zoom 会议的所有录制
|
||||
获取特定 Zoom 会议的所有录制内容
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 必需 | 描述 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `meetingId` | string | 是 | 会议 ID 或会议 UUID |
|
||||
| `includeFolderItems` | boolean | 否 | 包括文件夹中的项目 |
|
||||
| `includeFolderItems` | boolean | 否 | 包括文件夹内的项目 |
|
||||
| `ttl` | number | 否 | 下载 URL 的有效时间(秒)\(最大值 604800\) |
|
||||
|
||||
#### 输出
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `recording` | object | 包含所有文件的会议录制 |
|
||||
| `recording` | object | 包含所有文件的会议录制内容 |
|
||||
|
||||
### `zoom_delete_recording`
|
||||
|
||||
删除 Zoom 会议的云录制
|
||||
删除 Zoom 会议的云录制内容
|
||||
|
||||
#### 输入
|
||||
|
||||
| 参数 | 类型 | 必需 | 描述 |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `meetingId` | string | 是 | 会议 ID 或会议 UUID |
|
||||
| `recordingId` | string | 否 | 要删除的特定录制文件 ID。如果未提供,则删除所有录制。 |
|
||||
| `recordingId` | string | 否 | 要删除的特定录制文件 ID。如果未提供,则删除所有录制内容。 |
|
||||
| `action` | string | 否 | 删除操作:"trash" \(移至回收站\) 或 "delete" \(永久删除\) |
|
||||
|
||||
#### 输出
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `success` | boolean | 录制是否成功删除 |
|
||||
| `success` | boolean | 录制内容是否成功删除 |
|
||||
|
||||
### `zoom_list_past_participants`
|
||||
|
||||
@@ -243,10 +241,10 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `participants` | array | 会议参与者列表 |
|
||||
| `pageInfo` | object | 分页信息 |
|
||||
| `participants` | 数组 | 会议参与者列表 |
|
||||
| `pageInfo` | 对象 | 分页信息 |
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 类别: `tools`
|
||||
- 类型: `zoom`
|
||||
- 类别:`tools`
|
||||
- 类型:`zoom`
|
||||
|
||||
@@ -889,9 +889,9 @@ checksums:
|
||||
content/10: 71c6cf129630acff9d8df39d0a5c5407
|
||||
content/11: 9c8aa3f09c9b2bd50ea4cdff3598ea4e
|
||||
content/12: 8ee83eff32425b2c52929284e8485c20
|
||||
content/13: c1ec0b00cb68561551e48616731ea43a
|
||||
content/13: 6cda87dc9837779f4572ed70b87a5654
|
||||
content/14: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/15: 117e42c934a7f2a76b0399235841260e
|
||||
content/15: 2f696275726cdeefd7d7280b5bb43b21
|
||||
content/16: bcadfc362b69078beee0088e5936c98b
|
||||
content/17: bb43e4f36fdc1eb6211f46ddeed9e0aa
|
||||
content/18: 05540cb3028d4d781521c14e5f9e3835
|
||||
@@ -903,7 +903,7 @@ checksums:
|
||||
content/24: 228a8ece96627883153b826a1cbaa06c
|
||||
content/25: 53abe061a259c296c82676b4770ddd1b
|
||||
content/26: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/27: 03e8b10ec08b354de98e360b66b779e3
|
||||
content/27: 170ccdc4ce7ee086e9c6b5073efca582
|
||||
content/28: bcadfc362b69078beee0088e5936c98b
|
||||
content/29: b82def7d82657f941fbe60df3924eeeb
|
||||
content/30: 1ca7ee3856805fa1718031c5f75b6ffb
|
||||
@@ -2511,133 +2511,133 @@ checksums:
|
||||
content/12: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/13: 8c25606fde43bf4ff519760364fda052
|
||||
content/14: bcadfc362b69078beee0088e5936c98b
|
||||
content/15: 4ec31e928a8498d050922adb2f977c98
|
||||
content/15: 9ee0b1e8873ef165299443a76823e7bc
|
||||
content/16: be5c68d578443b68c062029104bd6ddb
|
||||
content/17: 3b38aa70e04f841184b7d958b087af8c
|
||||
content/18: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/19: 5269008adfae57a3a8fb5d8bf5922498
|
||||
content/20: bcadfc362b69078beee0088e5936c98b
|
||||
content/21: 5317f2e9eb34d7297064b381aef6912c
|
||||
content/21: c66b2996f62f0f7150fce59eed9ad7a8
|
||||
content/22: ef92d95455e378abe4d27a1cdc5e1aed
|
||||
content/23: febd6019055f3754953fd93395d0dbf2
|
||||
content/24: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/25: 7ef3f388e5ee9346bac54c771d825f40
|
||||
content/26: bcadfc362b69078beee0088e5936c98b
|
||||
content/27: e0fa91c45aa780fc03e91df77417f893
|
||||
content/27: 7bccc537f32fabcbb4cd0a85bef612de
|
||||
content/28: b463f54cd5fe2458b5842549fbb5e1ce
|
||||
content/29: 55f8c724e1a2463bc29a32518a512c73
|
||||
content/30: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/31: 770b1f7b3105937d452bc2815ebb6e05
|
||||
content/32: bcadfc362b69078beee0088e5936c98b
|
||||
content/33: 0b92b54ce40dc29bb6faccf82eace18b
|
||||
content/33: b7be768fe967164e71af56dd5cd13f86
|
||||
content/34: f426b59ee38021a4254fe566995c416c
|
||||
content/35: 2455b2f418cc79f4a67558678ae444bc
|
||||
content/36: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/37: 320e2962c7e2d24690c75b2f69e5be9d
|
||||
content/38: bcadfc362b69078beee0088e5936c98b
|
||||
content/39: 76f532ccbddb41115e56a7d56d97aa96
|
||||
content/39: 25501290045d87dad2c5819200528091
|
||||
content/40: 52233c13208d6e10497340a37b11ef3a
|
||||
content/41: 9c6bf4c4180c96e31668941aa36f2cde
|
||||
content/42: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/43: a792b3be1ab2bc6bc570f5dafca56aaa
|
||||
content/44: bcadfc362b69078beee0088e5936c98b
|
||||
content/45: 838a4016055b35389dae383f8b4ec2ac
|
||||
content/45: 120bc8dacae493c314aed0f4a4094c7f
|
||||
content/46: fe4880697d8adcd75c3a2c7e5b0fec86
|
||||
content/47: fdd9ab6e60b2c42a18a41ef869fb925b
|
||||
content/48: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/49: 0ccf3f3f59955dd78558a0e3589579a6
|
||||
content/50: bcadfc362b69078beee0088e5936c98b
|
||||
content/51: 8defd6d29c0ddbd9a811caa8f2cf3f39
|
||||
content/51: 35ce33f78ffa1130c2719885759406a5
|
||||
content/52: f04e8809e7d4f701cf24b339d844bea5
|
||||
content/53: 811c364b512dd61a2f40fb8418b6b0cd
|
||||
content/54: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/55: 2ce7e48a76065784447b75b6bc0fbff9
|
||||
content/56: bcadfc362b69078beee0088e5936c98b
|
||||
content/57: 47be5344f0c8a9ede380f37f769b5b3f
|
||||
content/57: 437fae06c917576e309864634ac006d5
|
||||
content/58: 8d41bb08f7d4000b665e6786583aa2b5
|
||||
content/59: 48adb1980e062be3783331522082edaf
|
||||
content/60: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/61: b89e1b7a15f022c0c13adadb25e3f49d
|
||||
content/62: bcadfc362b69078beee0088e5936c98b
|
||||
content/63: 44b209460093fb955b8f6b4e575dde17
|
||||
content/63: 74e0d576bddb246c672c98a8e9f4fd32
|
||||
content/64: c02f43d19361be7571e8141a61e83980
|
||||
content/65: 21a0f57793fb19dd8761b644e22ee731
|
||||
content/66: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/67: 6aa3e815a37986856a2781ccfdb7b4de
|
||||
content/68: bcadfc362b69078beee0088e5936c98b
|
||||
content/69: 9032d6a71c23f90a39232d653c3daf36
|
||||
content/69: b76190aa5e84cb17cdcf2e061edf706b
|
||||
content/70: 1845561cd920176e2dbfed65eaccca9e
|
||||
content/71: 2960e1e609b8c512f5cf1ef715c2a684
|
||||
content/72: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/73: e3586b13bcb7c91515437d76a0027201
|
||||
content/74: bcadfc362b69078beee0088e5936c98b
|
||||
content/75: f14261cdc2105f3c2380a90629edc172
|
||||
content/75: 520c03c754968a673ed5def1706f919d
|
||||
content/76: 14a4d1b2c2f1257eadb4b26b85672fab
|
||||
content/77: 45bd9b1b321f85dc31c720051c12f681
|
||||
content/78: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/79: 176f866fa8ad12f179eb5466bc968914
|
||||
content/80: bcadfc362b69078beee0088e5936c98b
|
||||
content/81: 8f1d8635d9e542fbce4ee4167d8a2bb1
|
||||
content/81: 1fc32b27418b8efe2abba1beb6d31868
|
||||
content/82: 5c0b4adc7825b3ed5831bf6c4d83a6a2
|
||||
content/83: cace0c917728a3a5bc93d26dd65669f7
|
||||
content/84: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/85: 490b3a3ef8840f1e01a58c6875af027b
|
||||
content/86: bcadfc362b69078beee0088e5936c98b
|
||||
content/87: 501cead9242b6febffe4659a63cef613
|
||||
content/87: f4c4cbcb48dbbd87b27fdf76105aaa8c
|
||||
content/88: efa34ea34fd3d30088470cf6f4476106
|
||||
content/89: 47fc6e4fd184baaf72d61223ec944148
|
||||
content/90: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/91: 43349c22c04743b654d4aee849cc81ff
|
||||
content/92: bcadfc362b69078beee0088e5936c98b
|
||||
content/93: a53a400b7cddac3a34435d23332db795
|
||||
content/93: 82634f835e924e6b2242df6127f0969a
|
||||
content/94: e3a5c53a79de7fe47abd7f7a9f86fb65
|
||||
content/95: 2053815e47b54488983f0571f49cd11a
|
||||
content/96: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/97: 846be448c3cedb87e4bd923c63e04512
|
||||
content/98: bcadfc362b69078beee0088e5936c98b
|
||||
content/99: d227cd028bf20eab7f826827efa9ea90
|
||||
content/99: 11608f282141ee6c31f933f6c2fcaa0d
|
||||
content/100: 17d1e59a4290138d979568f39e6fea9b
|
||||
content/101: cf9c3c1b441bde10b35d04d776e9f5ce
|
||||
content/102: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/103: 599eb9d3e6b88cba7a15d007ce1111f4
|
||||
content/104: bcadfc362b69078beee0088e5936c98b
|
||||
content/105: 09d70143b5598699ae1ed593baa4ac61
|
||||
content/105: ad1bd5c40adabc9f2a97682be1671d67
|
||||
content/106: afa20ccc5f708cf36a0cb6ede6ec0c4f
|
||||
content/107: 8a64259005d325f6849527186097f390
|
||||
content/108: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/109: c2b8a2c90d216d94f24850c8124849ef
|
||||
content/110: bcadfc362b69078beee0088e5936c98b
|
||||
content/111: 6bab23a5c82acbd6cd79fbdfc9bbcbde
|
||||
content/111: b5e005f9e95aead5c9596968b21821e1
|
||||
content/112: 05141d844a911fb66fd7bdc2b98e8160
|
||||
content/113: f04861fc73d9abc76dd8e140703baa14
|
||||
content/114: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/115: 09a884f6fcddd37ad4968718fb48db9e
|
||||
content/116: bcadfc362b69078beee0088e5936c98b
|
||||
content/117: a1645115447094e7520eb3d45244a3c8
|
||||
content/117: 62695bf37dd3fe43470266996e09ef86
|
||||
content/118: 96b30990733f35886cb04bc6bae18613
|
||||
content/119: b22baefc2beca09303baa0778b27a4d6
|
||||
content/120: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/121: 9b936f3424feac6551b709d0c5d5713c
|
||||
content/122: bcadfc362b69078beee0088e5936c98b
|
||||
content/123: 6cd2f15ea11b6f07e6ac7e170a90a91f
|
||||
content/123: 1a249d65588fd4a33bb50af768870bbc
|
||||
content/124: 573661fdf0cd751a2433052dff8dcdfe
|
||||
content/125: 81d70f8bd307578a5814374f31a7d6c2
|
||||
content/126: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/127: d04dcb9d64bc76606af4f9309eeacb75
|
||||
content/128: bcadfc362b69078beee0088e5936c98b
|
||||
content/129: b6b0fd5e140401e9f4c4c8a0e5ab0da1
|
||||
content/129: 7a78c9363ed6fba5d7277a24186c7296
|
||||
content/130: ce10caa6dcfe95e58c32db33157d989a
|
||||
content/131: 49df30ca91d4139a38b591311b7a83a8
|
||||
content/132: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/133: b61b7e3b5e5b7c05fcb65478d744abb5
|
||||
content/134: bcadfc362b69078beee0088e5936c98b
|
||||
content/135: 310e65d225fb68cf48f1d44d1047ea12
|
||||
content/135: 9ec868a621316d03619fc37582084053
|
||||
content/136: 805790ac8b4ae77c30fbfc9f6023bac8
|
||||
content/137: 1a4e93e8a49abd71333809a3bc0856c9
|
||||
content/138: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/139: 33fde4c3da4584b51f06183b7b192a78
|
||||
content/140: bcadfc362b69078beee0088e5936c98b
|
||||
content/141: b7451190f100388d999c183958d787a7
|
||||
content/141: a4e11bf0073b1f1e45c07e3c1c7dd969
|
||||
content/142: b3f310d5ef115bea5a8b75bf25d7ea9a
|
||||
content/143: 4930918f803340baa861bed9cdf789de
|
||||
8f76e389f6226f608571622b015ca6a1:
|
||||
@@ -47626,73 +47626,72 @@ checksums:
|
||||
content/4: 0f0165c7e21355d8f8e332c2252100db
|
||||
content/5: 11289606ffb19f4564a7f0a867a39a55
|
||||
content/6: 05eb6fe6951b12bcddd3ae36aacc7bb3
|
||||
content/7: e474de0de136881473833dd6502b6d06
|
||||
content/8: 715b7f8ee32c3d0dcd20cc0a57a9367b
|
||||
content/9: 821e6394b0a953e2b0842b04ae8f3105
|
||||
content/10: e5f8dc06b6db9aeef348d8af9617c787
|
||||
content/11: 9c8aa3f09c9b2bd50ea4cdff3598ea4e
|
||||
content/12: 0ec27ddd5601764fadfc363811376d88
|
||||
content/13: 18e3253cead6514fe5e939d98b64d8fb
|
||||
content/14: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/15: 676120936020f1a25faea0d5608b7958
|
||||
content/16: bcadfc362b69078beee0088e5936c98b
|
||||
content/17: ecad0614a5ec681a43fea86034a30905
|
||||
content/18: 8eb606aad3db305e12679efb6fe7363e
|
||||
content/19: 7d9ab020b8312987af94a42c7797a6bc
|
||||
content/20: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/21: 6854dad4e5e5419803d3b0a387bf36c1
|
||||
content/22: bcadfc362b69078beee0088e5936c98b
|
||||
content/23: 051e39427d40ab7c4b5ebbbf65c7910f
|
||||
content/24: 9c42c50fa5ce2db382867e2da5bca90d
|
||||
content/25: 7bc9a20018bc365ecf55a54a53ad1013
|
||||
content/26: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/27: 299dce7368070dd19957ba06efef836b
|
||||
content/28: bcadfc362b69078beee0088e5936c98b
|
||||
content/29: d9a5be31d4296b81660b38dcb4c695cc
|
||||
content/30: d732fd0df847a742d6dabfc5110ba31d
|
||||
content/31: 9782a621e6d591e72b7ce5e27face7af
|
||||
content/32: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/33: 2d7dd3ac552ff837d614c510033646ba
|
||||
content/34: bcadfc362b69078beee0088e5936c98b
|
||||
content/35: 37d84e8cc60979a8d3f1e48483d23113
|
||||
content/36: 90bc3ad5e30e5d579f48787e7d8181ae
|
||||
content/37: 7db1faa939033f49aad8ef462e630c26
|
||||
content/38: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/39: 5e289bbda737273fa75101e276358ec8
|
||||
content/40: bcadfc362b69078beee0088e5936c98b
|
||||
content/41: 805d1a06016797ba04f3cb840ac59e44
|
||||
content/42: 88260e555a61ba6886e56f3bc06512dc
|
||||
content/43: 80c4006b7d25c461c18e9a8a35cfac72
|
||||
content/44: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/45: 645735bd919e64e034c03b98cc75d5be
|
||||
content/46: bcadfc362b69078beee0088e5936c98b
|
||||
content/47: 842462f8cd7a897eda330bba54d297df
|
||||
content/48: 96d58ab5053c5f5db3f15f82442eb3dd
|
||||
content/49: 148c8f5f3872aa6e9944e221c35bc9a0
|
||||
content/50: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/51: 7a449b1878a28cff4b713a104581ec63
|
||||
content/52: bcadfc362b69078beee0088e5936c98b
|
||||
content/53: 502548c4b9d6be040f73fc431c3c8fd6
|
||||
content/54: 1304212656a10261692509a67cfae220
|
||||
content/55: 042eb9071c13eb10ee5fd0bfd4e00c8a
|
||||
content/56: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/57: 4263559dc306f5edcdfa1bf758adffdf
|
||||
content/58: bcadfc362b69078beee0088e5936c98b
|
||||
content/59: 984b85e501fbd9993b7fe38898e5d445
|
||||
content/60: 12f6776606adce02255b1db24cd58d29
|
||||
content/61: fc53b00cfedd65f5fd906daebd9c04df
|
||||
content/62: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/63: 242a47e9aaf5c07859e2a472bed6d3ac
|
||||
content/64: bcadfc362b69078beee0088e5936c98b
|
||||
content/65: 2823402034702ff5ca56b9cad3572c4d
|
||||
content/66: 8ddcef9d1d32bff76ac8e6c5a0e0dca5
|
||||
content/67: 94a960dd84bd71825b58d2219b98dd74
|
||||
content/68: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/69: e7777f90d25d134aa0a3cae9cdfc6563
|
||||
content/70: bcadfc362b69078beee0088e5936c98b
|
||||
content/71: affc25ff3d47510647c984a1d59b0a0e
|
||||
content/72: b3f310d5ef115bea5a8b75bf25d7ea9a
|
||||
content/73: 2d9d3b6969330e7b2d8e1169cfcf0031
|
||||
content/7: 715b7f8ee32c3d0dcd20cc0a57a9367b
|
||||
content/8: 821e6394b0a953e2b0842b04ae8f3105
|
||||
content/9: e5f8dc06b6db9aeef348d8af9617c787
|
||||
content/10: 9c8aa3f09c9b2bd50ea4cdff3598ea4e
|
||||
content/11: 0ec27ddd5601764fadfc363811376d88
|
||||
content/12: 18e3253cead6514fe5e939d98b64d8fb
|
||||
content/13: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/14: 676120936020f1a25faea0d5608b7958
|
||||
content/15: bcadfc362b69078beee0088e5936c98b
|
||||
content/16: ecad0614a5ec681a43fea86034a30905
|
||||
content/17: 8eb606aad3db305e12679efb6fe7363e
|
||||
content/18: 7d9ab020b8312987af94a42c7797a6bc
|
||||
content/19: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/20: 6854dad4e5e5419803d3b0a387bf36c1
|
||||
content/21: bcadfc362b69078beee0088e5936c98b
|
||||
content/22: 051e39427d40ab7c4b5ebbbf65c7910f
|
||||
content/23: 9c42c50fa5ce2db382867e2da5bca90d
|
||||
content/24: 7bc9a20018bc365ecf55a54a53ad1013
|
||||
content/25: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/26: 299dce7368070dd19957ba06efef836b
|
||||
content/27: bcadfc362b69078beee0088e5936c98b
|
||||
content/28: d9a5be31d4296b81660b38dcb4c695cc
|
||||
content/29: d732fd0df847a742d6dabfc5110ba31d
|
||||
content/30: 9782a621e6d591e72b7ce5e27face7af
|
||||
content/31: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/32: 2d7dd3ac552ff837d614c510033646ba
|
||||
content/33: bcadfc362b69078beee0088e5936c98b
|
||||
content/34: 37d84e8cc60979a8d3f1e48483d23113
|
||||
content/35: 90bc3ad5e30e5d579f48787e7d8181ae
|
||||
content/36: 7db1faa939033f49aad8ef462e630c26
|
||||
content/37: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/38: 5e289bbda737273fa75101e276358ec8
|
||||
content/39: bcadfc362b69078beee0088e5936c98b
|
||||
content/40: 805d1a06016797ba04f3cb840ac59e44
|
||||
content/41: 88260e555a61ba6886e56f3bc06512dc
|
||||
content/42: 80c4006b7d25c461c18e9a8a35cfac72
|
||||
content/43: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/44: 645735bd919e64e034c03b98cc75d5be
|
||||
content/45: bcadfc362b69078beee0088e5936c98b
|
||||
content/46: 842462f8cd7a897eda330bba54d297df
|
||||
content/47: 96d58ab5053c5f5db3f15f82442eb3dd
|
||||
content/48: 148c8f5f3872aa6e9944e221c35bc9a0
|
||||
content/49: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/50: 7a449b1878a28cff4b713a104581ec63
|
||||
content/51: bcadfc362b69078beee0088e5936c98b
|
||||
content/52: 502548c4b9d6be040f73fc431c3c8fd6
|
||||
content/53: 1304212656a10261692509a67cfae220
|
||||
content/54: 042eb9071c13eb10ee5fd0bfd4e00c8a
|
||||
content/55: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/56: 4263559dc306f5edcdfa1bf758adffdf
|
||||
content/57: bcadfc362b69078beee0088e5936c98b
|
||||
content/58: 984b85e501fbd9993b7fe38898e5d445
|
||||
content/59: 12f6776606adce02255b1db24cd58d29
|
||||
content/60: fc53b00cfedd65f5fd906daebd9c04df
|
||||
content/61: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/62: 242a47e9aaf5c07859e2a472bed6d3ac
|
||||
content/63: bcadfc362b69078beee0088e5936c98b
|
||||
content/64: 2823402034702ff5ca56b9cad3572c4d
|
||||
content/65: 8ddcef9d1d32bff76ac8e6c5a0e0dca5
|
||||
content/66: 94a960dd84bd71825b58d2219b98dd74
|
||||
content/67: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/68: e7777f90d25d134aa0a3cae9cdfc6563
|
||||
content/69: bcadfc362b69078beee0088e5936c98b
|
||||
content/70: affc25ff3d47510647c984a1d59b0a0e
|
||||
content/71: b3f310d5ef115bea5a8b75bf25d7ea9a
|
||||
content/72: 2d9d3b6969330e7b2d8e1169cfcf0031
|
||||
1db887f91df2e066fc769749f3b2a930:
|
||||
meta/title: b4c01a60ed020f21556b4a8ef3f24cae
|
||||
meta/description: b2f402630c2605cff14c3d7ad2c52d16
|
||||
@@ -49822,37 +49821,3 @@ checksums:
|
||||
content/472: dbc5fceeefb3ab5fa505394becafef4e
|
||||
content/473: b3f310d5ef115bea5a8b75bf25d7ea9a
|
||||
content/474: 27c398e669b297cea076e4ce4cc0c5eb
|
||||
9a28da736b42bf8de55126d4c06b6150:
|
||||
meta/title: 418d5c8a18ad73520b38765741601f32
|
||||
meta/description: 2b5a9723c7a45d2be5001d5d056b7c7b
|
||||
content/0: 1b031fb0c62c46b177aeed5c3d3f8f80
|
||||
content/1: e72670f88454b5b1c955b029de5fa8b5
|
||||
content/2: 821e6394b0a953e2b0842b04ae8f3105
|
||||
content/3: 7fa671d05a60d4f25b4980405c2c7278
|
||||
content/4: 9c8aa3f09c9b2bd50ea4cdff3598ea4e
|
||||
content/5: 263633aee6db9332de806ae50d87de05
|
||||
content/6: 5a7e2171e5f73fec5eae21a50e5de661
|
||||
content/7: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/8: 10d2d4eccb4b8923f048980dc16e43e1
|
||||
content/9: bcadfc362b69078beee0088e5936c98b
|
||||
content/10: d81ef802f80143282cf4e534561a9570
|
||||
content/11: 02233e6212003c1d121424cfd8b86b62
|
||||
content/12: efe2c6dd368708de68a1addbfdb11b0c
|
||||
content/13: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/14: 0f3295854b7de5dbfab1ebd2a130b498
|
||||
content/15: bcadfc362b69078beee0088e5936c98b
|
||||
content/16: 953f353184dc27db1f20156db2a9ad90
|
||||
content/17: 2011e87d0555cd0ab133ef2d35e7a37b
|
||||
content/18: dbf08acb413d845ec419e45b1f986bdb
|
||||
content/19: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/20: 3a8417b390ec7d3d55b1920c721e9006
|
||||
content/21: bcadfc362b69078beee0088e5936c98b
|
||||
content/22: c06a5bb458242baa23d34957034c2fe7
|
||||
content/23: ff043e912417bc29ac7c64520160c07d
|
||||
content/24: 9c2175ab469cb6ff9e62bc8bdcf7621d
|
||||
content/25: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/26: 67e6ba04cf67f92e714ed94e7483dec5
|
||||
content/27: bcadfc362b69078beee0088e5936c98b
|
||||
content/28: fd0f38eb3fe5cf95be366a4ff6b4fb90
|
||||
content/29: b3f310d5ef115bea5a8b75bf25d7ea9a
|
||||
content/30: 4a7b2c644e487f3d12b6a6b54f8c6773
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import type { InferPageType } from 'fumadocs-core/source'
|
||||
import type { PageData, source } from '@/lib/source'
|
||||
import type { source } from '@/lib/source'
|
||||
|
||||
export async function getLLMText(page: InferPageType<typeof source>) {
|
||||
const data = page.data as PageData
|
||||
const processed = await data.getText('processed')
|
||||
return `# ${data.title} (${page.url})
|
||||
const processed = await page.data.getText('processed')
|
||||
return `# ${page.data.title} (${page.url})
|
||||
|
||||
${processed}`
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { type InferPageType, loader } from 'fumadocs-core/source'
|
||||
import type { DocData, DocMethods } from 'fumadocs-mdx/runtime/types'
|
||||
import { loader } from 'fumadocs-core/source'
|
||||
import { docs } from '@/.source/server'
|
||||
import { i18n } from './i18n'
|
||||
|
||||
@@ -8,13 +7,3 @@ export const source = loader({
|
||||
source: docs.toFumadocsSource(),
|
||||
i18n,
|
||||
})
|
||||
|
||||
/** Full page data type including MDX content and metadata */
|
||||
export type PageData = DocData &
|
||||
DocMethods & {
|
||||
title: string
|
||||
description?: string
|
||||
full?: boolean
|
||||
}
|
||||
|
||||
export type Page = InferPageType<typeof source>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"dev": "next dev --port 7322",
|
||||
"dev": "next dev --port 3001",
|
||||
"build": "fumadocs-mdx && NODE_OPTIONS='--max-old-space-size=8192' next build",
|
||||
"start": "next start",
|
||||
"postinstall": "fumadocs-mdx",
|
||||
@@ -19,7 +19,7 @@
|
||||
"fumadocs-mdx": "14.1.0",
|
||||
"fumadocs-ui": "16.2.3",
|
||||
"lucide-react": "^0.511.0",
|
||||
"next": "16.1.0-canary.21",
|
||||
"next": "16.0.9",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "19.2.1",
|
||||
"react-dom": "19.2.1",
|
||||
|
||||
@@ -4,7 +4,6 @@ export const FOOTER_BLOCKS = [
|
||||
'Condition',
|
||||
'Evaluator',
|
||||
'Function',
|
||||
'Guardrails',
|
||||
'Human In The Loop',
|
||||
'Loop',
|
||||
'Parallel',
|
||||
@@ -31,6 +30,7 @@ export const FOOTER_TOOLS = [
|
||||
'GitHub',
|
||||
'Gmail',
|
||||
'Google Drive',
|
||||
'Guardrails',
|
||||
'HubSpot',
|
||||
'HuggingFace',
|
||||
'Hunter',
|
||||
|
||||
@@ -64,7 +64,6 @@ export default async function Page({ params }: { params: Promise<{ slug: string
|
||||
sizes='(max-width: 768px) 100vw, 450px'
|
||||
priority
|
||||
itemProp='image'
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -145,7 +144,6 @@ export default async function Page({ params }: { params: Promise<{ slug: string
|
||||
className='h-[160px] w-full object-cover'
|
||||
sizes='(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw'
|
||||
loading='lazy'
|
||||
unoptimized
|
||||
/>
|
||||
<div className='p-3'>
|
||||
<div className='mb-1 text-gray-600 text-xs'>
|
||||
|
||||
@@ -38,7 +38,6 @@ export default async function AuthorPage({ params }: { params: Promise<{ id: str
|
||||
width={40}
|
||||
height={40}
|
||||
className='rounded-full'
|
||||
unoptimized
|
||||
/>
|
||||
) : null}
|
||||
<h1 className='font-medium text-[32px] leading-tight'>{author.name}</h1>
|
||||
@@ -53,7 +52,6 @@ export default async function AuthorPage({ params }: { params: Promise<{ id: str
|
||||
width={600}
|
||||
height={315}
|
||||
className='h-[160px] w-full object-cover transition-transform group-hover:scale-[1.02]'
|
||||
unoptimized
|
||||
/>
|
||||
<div className='p-3'>
|
||||
<div className='mb-1 text-gray-600 text-xs'>
|
||||
|
||||
@@ -76,7 +76,6 @@ export default async function StudioIndex({
|
||||
className='h-48 w-full object-cover'
|
||||
sizes='(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw'
|
||||
loading='lazy'
|
||||
unoptimized
|
||||
/>
|
||||
<div className='flex flex-1 flex-col p-4'>
|
||||
<div className='mb-2 text-gray-600 text-xs'>
|
||||
|
||||
@@ -159,7 +159,7 @@ describe('OAuth Utils', () => {
|
||||
|
||||
const result = await refreshTokenIfNeeded('request-id', mockCredential, 'credential-id')
|
||||
|
||||
expect(mockRefreshOAuthToken).toHaveBeenCalledWith('google', 'refresh-token', undefined)
|
||||
expect(mockRefreshOAuthToken).toHaveBeenCalledWith('google', 'refresh-token')
|
||||
expect(mockDb.update).toHaveBeenCalled()
|
||||
expect(mockDb.set).toHaveBeenCalled()
|
||||
expect(result).toEqual({ accessToken: 'new-token', refreshed: true })
|
||||
@@ -239,7 +239,7 @@ describe('OAuth Utils', () => {
|
||||
|
||||
const token = await refreshAccessTokenIfNeeded('credential-id', 'test-user-id', 'request-id')
|
||||
|
||||
expect(mockRefreshOAuthToken).toHaveBeenCalledWith('google', 'refresh-token', undefined)
|
||||
expect(mockRefreshOAuthToken).toHaveBeenCalledWith('google', 'refresh-token')
|
||||
expect(mockDb.update).toHaveBeenCalled()
|
||||
expect(mockDb.set).toHaveBeenCalled()
|
||||
expect(token).toBe('new-token')
|
||||
|
||||
@@ -18,7 +18,6 @@ interface AccountInsertData {
|
||||
updatedAt: Date
|
||||
refreshToken?: string
|
||||
idToken?: string
|
||||
accessTokenExpiresAt?: Date
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,7 +103,6 @@ export async function getOAuthToken(userId: string, providerId: string): Promise
|
||||
accessToken: account.accessToken,
|
||||
refreshToken: account.refreshToken,
|
||||
accessTokenExpiresAt: account.accessTokenExpiresAt,
|
||||
idToken: account.idToken,
|
||||
})
|
||||
.from(account)
|
||||
.where(and(eq(account.userId, userId), eq(account.providerId, providerId)))
|
||||
@@ -132,14 +130,7 @@ export async function getOAuthToken(userId: string, providerId: string): Promise
|
||||
|
||||
try {
|
||||
// Use the existing refreshOAuthToken function
|
||||
// For ServiceNow, pass the instance URL (stored in idToken) for the token endpoint
|
||||
const instanceUrl =
|
||||
providerId === 'servicenow' ? (credential.idToken ?? undefined) : undefined
|
||||
const refreshResult = await refreshOAuthToken(
|
||||
providerId,
|
||||
credential.refreshToken!,
|
||||
instanceUrl
|
||||
)
|
||||
const refreshResult = await refreshOAuthToken(providerId, credential.refreshToken!)
|
||||
|
||||
if (!refreshResult) {
|
||||
logger.error(`Failed to refresh token for user ${userId}, provider ${providerId}`, {
|
||||
@@ -222,13 +213,9 @@ export async function refreshAccessTokenIfNeeded(
|
||||
if (shouldRefresh) {
|
||||
logger.info(`[${requestId}] Token expired, attempting to refresh for credential`)
|
||||
try {
|
||||
// For ServiceNow, pass the instance URL (stored in idToken) for the token endpoint
|
||||
const instanceUrl =
|
||||
credential.providerId === 'servicenow' ? (credential.idToken ?? undefined) : undefined
|
||||
const refreshedToken = await refreshOAuthToken(
|
||||
credential.providerId,
|
||||
credential.refreshToken!,
|
||||
instanceUrl
|
||||
credential.refreshToken!
|
||||
)
|
||||
|
||||
if (!refreshedToken) {
|
||||
@@ -300,14 +287,7 @@ export async function refreshTokenIfNeeded(
|
||||
}
|
||||
|
||||
try {
|
||||
// For ServiceNow, pass the instance URL (stored in idToken) for the token endpoint
|
||||
const instanceUrl =
|
||||
credential.providerId === 'servicenow' ? (credential.idToken ?? undefined) : undefined
|
||||
const refreshResult = await refreshOAuthToken(
|
||||
credential.providerId,
|
||||
credential.refreshToken!,
|
||||
instanceUrl
|
||||
)
|
||||
const refreshResult = await refreshOAuthToken(credential.providerId, credential.refreshToken!)
|
||||
|
||||
if (!refreshResult) {
|
||||
logger.error(`[${requestId}] Failed to refresh token for credential`)
|
||||
|
||||
@@ -1,166 +0,0 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { env } from '@/lib/core/config/env'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
|
||||
const logger = createLogger('ServiceNowCallback')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const baseUrl = getBaseUrl()
|
||||
|
||||
try {
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.redirect(`${baseUrl}/workspace?error=unauthorized`)
|
||||
}
|
||||
|
||||
const { searchParams } = request.nextUrl
|
||||
const code = searchParams.get('code')
|
||||
const state = searchParams.get('state')
|
||||
const error = searchParams.get('error')
|
||||
const errorDescription = searchParams.get('error_description')
|
||||
|
||||
// Handle OAuth errors from ServiceNow
|
||||
if (error) {
|
||||
logger.error('ServiceNow OAuth error:', { error, errorDescription })
|
||||
return NextResponse.redirect(
|
||||
`${baseUrl}/workspace?error=servicenow_auth_error&message=${encodeURIComponent(errorDescription || error)}`
|
||||
)
|
||||
}
|
||||
|
||||
const storedState = request.cookies.get('servicenow_oauth_state')?.value
|
||||
const storedInstanceUrl = request.cookies.get('servicenow_instance_url')?.value
|
||||
|
||||
const clientId = env.SERVICENOW_CLIENT_ID
|
||||
const clientSecret = env.SERVICENOW_CLIENT_SECRET
|
||||
|
||||
if (!clientId || !clientSecret) {
|
||||
logger.error('ServiceNow credentials not configured')
|
||||
return NextResponse.redirect(`${baseUrl}/workspace?error=servicenow_config_error`)
|
||||
}
|
||||
|
||||
// Validate state parameter
|
||||
if (!state || state !== storedState) {
|
||||
logger.error('State mismatch in ServiceNow OAuth callback')
|
||||
return NextResponse.redirect(`${baseUrl}/workspace?error=servicenow_state_mismatch`)
|
||||
}
|
||||
|
||||
// Validate authorization code
|
||||
if (!code) {
|
||||
logger.error('No code received from ServiceNow')
|
||||
return NextResponse.redirect(`${baseUrl}/workspace?error=servicenow_no_code`)
|
||||
}
|
||||
|
||||
// Validate instance URL
|
||||
if (!storedInstanceUrl) {
|
||||
logger.error('No instance URL stored')
|
||||
return NextResponse.redirect(`${baseUrl}/workspace?error=servicenow_no_instance`)
|
||||
}
|
||||
|
||||
const redirectUri = `${baseUrl}/api/auth/oauth2/callback/servicenow`
|
||||
|
||||
// Exchange authorization code for access token
|
||||
const tokenResponse = await fetch(`${storedInstanceUrl}/oauth_token.do`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
code: code,
|
||||
redirect_uri: redirectUri,
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
}).toString(),
|
||||
})
|
||||
|
||||
if (!tokenResponse.ok) {
|
||||
const errorText = await tokenResponse.text()
|
||||
logger.error('Failed to exchange code for token:', {
|
||||
status: tokenResponse.status,
|
||||
body: errorText,
|
||||
})
|
||||
return NextResponse.redirect(`${baseUrl}/workspace?error=servicenow_token_error`)
|
||||
}
|
||||
|
||||
const tokenData = await tokenResponse.json()
|
||||
const accessToken = tokenData.access_token
|
||||
const refreshToken = tokenData.refresh_token
|
||||
const expiresIn = tokenData.expires_in
|
||||
// ServiceNow always grants 'useraccount' scope but returns empty string
|
||||
const scope = tokenData.scope || 'useraccount'
|
||||
|
||||
logger.info('ServiceNow token exchange successful:', {
|
||||
hasAccessToken: !!accessToken,
|
||||
hasRefreshToken: !!refreshToken,
|
||||
expiresIn,
|
||||
})
|
||||
|
||||
if (!accessToken) {
|
||||
logger.error('No access token in response')
|
||||
return NextResponse.redirect(`${baseUrl}/workspace?error=servicenow_no_token`)
|
||||
}
|
||||
|
||||
// Redirect to store endpoint with token data in cookies
|
||||
const storeUrl = new URL(`${baseUrl}/api/auth/oauth2/servicenow/store`)
|
||||
|
||||
const response = NextResponse.redirect(storeUrl)
|
||||
|
||||
// Store token data in secure cookies for the store endpoint
|
||||
response.cookies.set('servicenow_pending_token', accessToken, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 60, // 1 minute
|
||||
path: '/',
|
||||
})
|
||||
|
||||
if (refreshToken) {
|
||||
response.cookies.set('servicenow_pending_refresh_token', refreshToken, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 60,
|
||||
path: '/',
|
||||
})
|
||||
}
|
||||
|
||||
response.cookies.set('servicenow_pending_instance', storedInstanceUrl, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 60,
|
||||
path: '/',
|
||||
})
|
||||
|
||||
response.cookies.set('servicenow_pending_scope', scope || '', {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 60,
|
||||
path: '/',
|
||||
})
|
||||
|
||||
if (expiresIn) {
|
||||
response.cookies.set('servicenow_pending_expires_in', expiresIn.toString(), {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 60,
|
||||
path: '/',
|
||||
})
|
||||
}
|
||||
|
||||
// Clean up OAuth state cookies
|
||||
response.cookies.delete('servicenow_oauth_state')
|
||||
response.cookies.delete('servicenow_instance_url')
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
logger.error('Error in ServiceNow OAuth callback:', error)
|
||||
return NextResponse.redirect(`${baseUrl}/workspace?error=servicenow_callback_error`)
|
||||
}
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
import { db } from '@sim/db'
|
||||
import { account } from '@sim/db/schema'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { safeAccountInsert } from '@/app/api/auth/oauth/utils'
|
||||
|
||||
const logger = createLogger('ServiceNowStore')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const baseUrl = getBaseUrl()
|
||||
|
||||
try {
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
logger.warn('Unauthorized attempt to store ServiceNow token')
|
||||
return NextResponse.redirect(`${baseUrl}/workspace?error=unauthorized`)
|
||||
}
|
||||
|
||||
// Retrieve token data from cookies
|
||||
const accessToken = request.cookies.get('servicenow_pending_token')?.value
|
||||
const refreshToken = request.cookies.get('servicenow_pending_refresh_token')?.value
|
||||
const instanceUrl = request.cookies.get('servicenow_pending_instance')?.value
|
||||
const scope = request.cookies.get('servicenow_pending_scope')?.value
|
||||
const expiresInStr = request.cookies.get('servicenow_pending_expires_in')?.value
|
||||
|
||||
if (!accessToken || !instanceUrl) {
|
||||
logger.error('Missing token or instance URL in cookies')
|
||||
return NextResponse.redirect(`${baseUrl}/workspace?error=servicenow_missing_data`)
|
||||
}
|
||||
|
||||
// Validate the token by fetching user info from ServiceNow
|
||||
const userResponse = await fetch(
|
||||
`${instanceUrl}/api/now/table/sys_user?sysparm_query=user_name=${encodeURIComponent('javascript:gs.getUserName()')}&sysparm_limit=1`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// Alternative: Use the instance info endpoint instead
|
||||
let accountIdentifier = instanceUrl
|
||||
let userInfo: Record<string, unknown> | null = null
|
||||
|
||||
// Try to get current user info
|
||||
try {
|
||||
const whoamiResponse = await fetch(`${instanceUrl}/api/now/ui/user/current_user`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (whoamiResponse.ok) {
|
||||
const whoamiData = await whoamiResponse.json()
|
||||
userInfo = whoamiData.result
|
||||
if (userInfo?.user_sys_id) {
|
||||
accountIdentifier = userInfo.user_sys_id as string
|
||||
} else if (userInfo?.user_name) {
|
||||
accountIdentifier = userInfo.user_name as string
|
||||
}
|
||||
logger.info('Retrieved ServiceNow user info', { accountIdentifier })
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn('Could not retrieve ServiceNow user info, using instance URL as identifier')
|
||||
}
|
||||
|
||||
// Calculate expiration time
|
||||
const now = new Date()
|
||||
const expiresIn = expiresInStr ? Number.parseInt(expiresInStr, 10) : 3600 // Default to 1 hour
|
||||
const accessTokenExpiresAt = new Date(now.getTime() + expiresIn * 1000)
|
||||
|
||||
// Check for existing ServiceNow account for this user
|
||||
const existing = await db.query.account.findFirst({
|
||||
where: and(eq(account.userId, session.user.id), eq(account.providerId, 'servicenow')),
|
||||
})
|
||||
|
||||
// ServiceNow always grants 'useraccount' scope but returns empty string
|
||||
const effectiveScope = scope?.trim() ? scope : 'useraccount'
|
||||
|
||||
const accountData = {
|
||||
accessToken: accessToken,
|
||||
refreshToken: refreshToken || null,
|
||||
accountId: accountIdentifier,
|
||||
scope: effectiveScope,
|
||||
updatedAt: now,
|
||||
accessTokenExpiresAt: accessTokenExpiresAt,
|
||||
idToken: instanceUrl, // Store instance URL in idToken for API calls
|
||||
}
|
||||
|
||||
if (existing) {
|
||||
await db.update(account).set(accountData).where(eq(account.id, existing.id))
|
||||
logger.info('Updated existing ServiceNow account', { accountId: existing.id })
|
||||
} else {
|
||||
await safeAccountInsert(
|
||||
{
|
||||
id: `servicenow_${session.user.id}_${Date.now()}`,
|
||||
userId: session.user.id,
|
||||
providerId: 'servicenow',
|
||||
accountId: accountData.accountId,
|
||||
accessToken: accountData.accessToken,
|
||||
refreshToken: accountData.refreshToken || undefined,
|
||||
accessTokenExpiresAt: accountData.accessTokenExpiresAt,
|
||||
scope: accountData.scope,
|
||||
idToken: accountData.idToken,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
{ provider: 'ServiceNow', identifier: instanceUrl }
|
||||
)
|
||||
logger.info('Created new ServiceNow account')
|
||||
}
|
||||
|
||||
// Get return URL from cookie
|
||||
const returnUrl = request.cookies.get('servicenow_return_url')?.value
|
||||
|
||||
const redirectUrl = returnUrl || `${baseUrl}/workspace`
|
||||
const finalUrl = new URL(redirectUrl)
|
||||
finalUrl.searchParams.set('servicenow_connected', 'true')
|
||||
|
||||
const response = NextResponse.redirect(finalUrl.toString())
|
||||
|
||||
// Clean up all ServiceNow cookies
|
||||
response.cookies.delete('servicenow_pending_token')
|
||||
response.cookies.delete('servicenow_pending_refresh_token')
|
||||
response.cookies.delete('servicenow_pending_instance')
|
||||
response.cookies.delete('servicenow_pending_scope')
|
||||
response.cookies.delete('servicenow_pending_expires_in')
|
||||
response.cookies.delete('servicenow_return_url')
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
logger.error('Error storing ServiceNow token:', error)
|
||||
return NextResponse.redirect(`${baseUrl}/workspace?error=servicenow_store_error`)
|
||||
}
|
||||
}
|
||||
@@ -1,264 +0,0 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { env } from '@/lib/core/config/env'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
|
||||
const logger = createLogger('ServiceNowAuthorize')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
/**
|
||||
* ServiceNow OAuth scopes
|
||||
* useraccount - Default scope for user account access
|
||||
* Note: ServiceNow always returns 'useraccount' in OAuth responses regardless of requested scopes.
|
||||
* Table API permissions are configured at the OAuth application level in ServiceNow.
|
||||
*/
|
||||
const SERVICENOW_SCOPES = 'useraccount'
|
||||
|
||||
/**
|
||||
* Validates a ServiceNow instance URL format
|
||||
*/
|
||||
function isValidInstanceUrl(url: string): boolean {
|
||||
try {
|
||||
const parsed = new URL(url)
|
||||
return (
|
||||
parsed.protocol === 'https:' &&
|
||||
(parsed.hostname.endsWith('.service-now.com') || parsed.hostname.endsWith('.servicenow.com'))
|
||||
)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const clientId = env.SERVICENOW_CLIENT_ID
|
||||
|
||||
if (!clientId) {
|
||||
logger.error('SERVICENOW_CLIENT_ID not configured')
|
||||
return NextResponse.json({ error: 'ServiceNow client ID not configured' }, { status: 500 })
|
||||
}
|
||||
|
||||
const instanceUrl = request.nextUrl.searchParams.get('instanceUrl')
|
||||
const returnUrl = request.nextUrl.searchParams.get('returnUrl')
|
||||
|
||||
if (!instanceUrl) {
|
||||
const returnUrlParam = returnUrl ? encodeURIComponent(returnUrl) : ''
|
||||
return new NextResponse(
|
||||
`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Connect ServiceNow Instance</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
background: linear-gradient(135deg, #81B5A1 0%, #5A8A75 100%);
|
||||
}
|
||||
.container {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
|
||||
text-align: center;
|
||||
max-width: 450px;
|
||||
width: 90%;
|
||||
}
|
||||
h2 {
|
||||
color: #111827;
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
p {
|
||||
color: #6b7280;
|
||||
margin: 0 0 1.5rem 0;
|
||||
}
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-color: #81B5A1;
|
||||
box-shadow: 0 0 0 3px rgba(129, 181, 161, 0.2);
|
||||
}
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
background: #81B5A1;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
}
|
||||
button:hover {
|
||||
background: #6A9A87;
|
||||
}
|
||||
.help {
|
||||
font-size: 0.875rem;
|
||||
color: #9ca3af;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.error {
|
||||
color: #dc2626;
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 1rem;
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h2>Connect Your ServiceNow Instance</h2>
|
||||
<p>Enter your ServiceNow instance URL to continue</p>
|
||||
<div id="error" class="error"></div>
|
||||
<form onsubmit="handleSubmit(event)">
|
||||
<input
|
||||
type="text"
|
||||
id="instanceUrl"
|
||||
placeholder="https://mycompany.service-now.com"
|
||||
required
|
||||
/>
|
||||
<button type="submit">Connect Instance</button>
|
||||
</form>
|
||||
<p class="help">Your instance URL looks like: https://yourcompany.service-now.com</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const returnUrl = '${returnUrlParam}';
|
||||
function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
const errorEl = document.getElementById('error');
|
||||
let instanceUrl = document.getElementById('instanceUrl').value.trim();
|
||||
|
||||
// Ensure https:// prefix
|
||||
if (!instanceUrl.startsWith('https://') && !instanceUrl.startsWith('http://')) {
|
||||
instanceUrl = 'https://' + instanceUrl;
|
||||
}
|
||||
|
||||
// Validate the URL format
|
||||
try {
|
||||
const parsed = new URL(instanceUrl);
|
||||
if (!parsed.hostname.endsWith('.service-now.com') && !parsed.hostname.endsWith('.servicenow.com')) {
|
||||
errorEl.textContent = 'Please enter a valid ServiceNow instance URL (e.g., https://yourcompany.service-now.com)';
|
||||
errorEl.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
// Clean the URL (remove trailing slashes, paths)
|
||||
instanceUrl = parsed.origin;
|
||||
} catch {
|
||||
errorEl.textContent = 'Please enter a valid URL';
|
||||
errorEl.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
let url = window.location.pathname + '?instanceUrl=' + encodeURIComponent(instanceUrl);
|
||||
if (returnUrl) {
|
||||
url += '&returnUrl=' + returnUrl;
|
||||
}
|
||||
window.location.href = url;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>`,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'text/html; charset=utf-8',
|
||||
'Cache-Control': 'no-store, no-cache, must-revalidate',
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Validate instance URL
|
||||
if (!isValidInstanceUrl(instanceUrl)) {
|
||||
logger.error('Invalid ServiceNow instance URL:', { instanceUrl })
|
||||
return NextResponse.json(
|
||||
{
|
||||
error:
|
||||
'Invalid ServiceNow instance URL. Must be a valid .service-now.com or .servicenow.com domain.',
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Clean the instance URL
|
||||
const parsedUrl = new URL(instanceUrl)
|
||||
const cleanInstanceUrl = parsedUrl.origin
|
||||
|
||||
const baseUrl = getBaseUrl()
|
||||
const redirectUri = `${baseUrl}/api/auth/oauth2/callback/servicenow`
|
||||
|
||||
const state = crypto.randomUUID()
|
||||
|
||||
// ServiceNow OAuth authorization URL
|
||||
const oauthUrl =
|
||||
`${cleanInstanceUrl}/oauth_auth.do?` +
|
||||
new URLSearchParams({
|
||||
response_type: 'code',
|
||||
client_id: clientId,
|
||||
redirect_uri: redirectUri,
|
||||
state: state,
|
||||
scope: SERVICENOW_SCOPES,
|
||||
}).toString()
|
||||
|
||||
logger.info('Initiating ServiceNow OAuth:', {
|
||||
instanceUrl: cleanInstanceUrl,
|
||||
requestedScopes: SERVICENOW_SCOPES,
|
||||
redirectUri,
|
||||
returnUrl: returnUrl || 'not specified',
|
||||
})
|
||||
|
||||
const response = NextResponse.redirect(oauthUrl)
|
||||
|
||||
// Store state and instance URL in cookies for validation in callback
|
||||
response.cookies.set('servicenow_oauth_state', state, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 60 * 10, // 10 minutes
|
||||
path: '/',
|
||||
})
|
||||
|
||||
response.cookies.set('servicenow_instance_url', cleanInstanceUrl, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 60 * 10,
|
||||
path: '/',
|
||||
})
|
||||
|
||||
if (returnUrl) {
|
||||
response.cookies.set('servicenow_return_url', returnUrl, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 60 * 10,
|
||||
path: '/',
|
||||
})
|
||||
}
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
logger.error('Error initiating ServiceNow authorization:', error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -303,14 +303,6 @@ export async function POST(req: NextRequest) {
|
||||
apiVersion: 'preview',
|
||||
endpoint: env.AZURE_OPENAI_ENDPOINT,
|
||||
}
|
||||
} else if (providerEnv === 'vertex') {
|
||||
providerConfig = {
|
||||
provider: 'vertex',
|
||||
model: modelToUse,
|
||||
apiKey: env.COPILOT_API_KEY,
|
||||
vertexProject: env.VERTEX_PROJECT,
|
||||
vertexLocation: env.VERTEX_LOCATION,
|
||||
}
|
||||
} else {
|
||||
providerConfig = {
|
||||
provider: providerEnv,
|
||||
|
||||
@@ -66,14 +66,6 @@ export async function POST(req: NextRequest) {
|
||||
apiVersion: env.AZURE_OPENAI_API_VERSION,
|
||||
endpoint: env.AZURE_OPENAI_ENDPOINT,
|
||||
}
|
||||
} else if (providerEnv === 'vertex') {
|
||||
providerConfig = {
|
||||
provider: 'vertex',
|
||||
model: modelToUse,
|
||||
apiKey: env.COPILOT_API_KEY,
|
||||
vertexProject: env.VERTEX_PROJECT,
|
||||
vertexLocation: env.VERTEX_LOCATION,
|
||||
}
|
||||
} else {
|
||||
providerConfig = {
|
||||
provider: providerEnv,
|
||||
|
||||
@@ -7,80 +7,10 @@ import { NextRequest } from 'next/server'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createMockRequest } from '@/app/api/__test-utils__/utils'
|
||||
|
||||
vi.mock('@/lib/execution/isolated-vm', () => ({
|
||||
executeInIsolatedVM: vi.fn().mockImplementation(async (req) => {
|
||||
const { code, params, envVars, contextVariables } = req
|
||||
const stdoutChunks: string[] = []
|
||||
|
||||
const mockConsole = {
|
||||
log: (...args: unknown[]) => {
|
||||
stdoutChunks.push(
|
||||
`${args.map((a) => (typeof a === 'object' ? JSON.stringify(a) : String(a))).join(' ')}\n`
|
||||
)
|
||||
},
|
||||
error: (...args: unknown[]) => {
|
||||
stdoutChunks.push(
|
||||
'ERROR: ' +
|
||||
args.map((a) => (typeof a === 'object' ? JSON.stringify(a) : String(a))).join(' ') +
|
||||
'\n'
|
||||
)
|
||||
},
|
||||
warn: (...args: unknown[]) => mockConsole.log('WARN:', ...args),
|
||||
info: (...args: unknown[]) => mockConsole.log(...args),
|
||||
}
|
||||
|
||||
try {
|
||||
const escapePattern = /this\.constructor\.constructor|\.constructor\s*\(/
|
||||
if (escapePattern.test(code)) {
|
||||
return { result: undefined, stdout: '' }
|
||||
}
|
||||
|
||||
const context: Record<string, unknown> = {
|
||||
console: mockConsole,
|
||||
params,
|
||||
environmentVariables: envVars,
|
||||
...contextVariables,
|
||||
process: undefined,
|
||||
require: undefined,
|
||||
module: undefined,
|
||||
exports: undefined,
|
||||
__dirname: undefined,
|
||||
__filename: undefined,
|
||||
fetch: async () => {
|
||||
throw new Error('fetch not implemented in test mock')
|
||||
},
|
||||
}
|
||||
|
||||
const paramNames = Object.keys(context)
|
||||
const paramValues = Object.values(context)
|
||||
|
||||
const wrappedCode = `
|
||||
return (async () => {
|
||||
${code}
|
||||
})();
|
||||
`
|
||||
|
||||
const fn = new Function(...paramNames, wrappedCode)
|
||||
const result = await fn(...paramValues)
|
||||
|
||||
return {
|
||||
result,
|
||||
stdout: stdoutChunks.join(''),
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error
|
||||
return {
|
||||
result: null,
|
||||
stdout: stdoutChunks.join(''),
|
||||
error: {
|
||||
message: err.message || String(error),
|
||||
name: err.name || 'Error',
|
||||
stack: err.stack,
|
||||
},
|
||||
}
|
||||
}
|
||||
}),
|
||||
}))
|
||||
const mockCreateContext = vi.fn()
|
||||
const mockRunInContext = vi.fn()
|
||||
const mockScript = vi.fn()
|
||||
const mockExecuteInE2B = vi.fn()
|
||||
|
||||
vi.mock('@/lib/logs/console/logger', () => ({
|
||||
createLogger: vi.fn(() => ({
|
||||
@@ -91,20 +21,35 @@ vi.mock('@/lib/logs/console/logger', () => ({
|
||||
})),
|
||||
}))
|
||||
|
||||
vi.mock('vm', () => ({
|
||||
createContext: vi.fn(),
|
||||
Script: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/execution/e2b', () => ({
|
||||
executeInE2B: vi.fn(),
|
||||
}))
|
||||
|
||||
import { createContext, Script } from 'vm'
|
||||
import { validateProxyUrl } from '@/lib/core/security/input-validation'
|
||||
import { executeInE2B } from '@/lib/execution/e2b'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { POST } from './route'
|
||||
|
||||
const mockedCreateContext = vi.mocked(createContext)
|
||||
const mockedScript = vi.mocked(Script)
|
||||
const mockedExecuteInE2B = vi.mocked(executeInE2B)
|
||||
const mockedCreateLogger = vi.mocked(createLogger)
|
||||
|
||||
describe('Function Execute API Route', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
mockedCreateContext.mockReturnValue({})
|
||||
mockRunInContext.mockResolvedValue('vm success')
|
||||
mockedScript.mockImplementation((): any => ({
|
||||
runInContext: mockRunInContext,
|
||||
}))
|
||||
mockedExecuteInE2B.mockResolvedValue({
|
||||
result: 'e2b success',
|
||||
stdout: 'e2b output',
|
||||
@@ -117,77 +62,19 @@ describe('Function Execute API Route', () => {
|
||||
})
|
||||
|
||||
describe('Security Tests', () => {
|
||||
it.concurrent('should use isolated-vm for secure sandboxed execution', async () => {
|
||||
it.concurrent('should create secure fetch in VM context', async () => {
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'return "test"',
|
||||
})
|
||||
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
await POST(req)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(data.success).toBe(true)
|
||||
expect(data.output.result).toBe('test')
|
||||
})
|
||||
expect(mockedCreateContext).toHaveBeenCalled()
|
||||
const contextArgs = mockedCreateContext.mock.calls[0][0]
|
||||
expect(contextArgs).toHaveProperty('fetch')
|
||||
expect(typeof (contextArgs as any).fetch).toBe('function')
|
||||
|
||||
it.concurrent('should prevent VM escape via constructor chain', async () => {
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'return this.constructor.constructor("return process")().env',
|
||||
})
|
||||
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
if (response.status === 500) {
|
||||
expect(data.success).toBe(false)
|
||||
} else {
|
||||
const result = data.output?.result
|
||||
expect(result === undefined || result === null).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
it.concurrent('should prevent access to require via constructor chain', async () => {
|
||||
const req = createMockRequest('POST', {
|
||||
code: `
|
||||
const proc = this.constructor.constructor("return process")();
|
||||
const fs = proc.mainModule.require("fs");
|
||||
return fs.readFileSync("/etc/passwd", "utf8");
|
||||
`,
|
||||
})
|
||||
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
if (response.status === 200) {
|
||||
const result = data.output?.result
|
||||
if (result !== undefined && result !== null && typeof result === 'string') {
|
||||
expect(result).not.toContain('root:')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it.concurrent('should not expose process object', async () => {
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'return typeof process',
|
||||
})
|
||||
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(data.output.result).toBe('undefined')
|
||||
})
|
||||
|
||||
it.concurrent('should not expose require function', async () => {
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'return typeof require',
|
||||
})
|
||||
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(data.output.result).toBe('undefined')
|
||||
expect((contextArgs as any).fetch?.name).toBe('secureFetch')
|
||||
})
|
||||
|
||||
it.concurrent('should block SSRF attacks through secure fetch wrapper', async () => {
|
||||
@@ -226,20 +113,6 @@ describe('Function Execute API Route', () => {
|
||||
expect(data.output).toHaveProperty('executionTime')
|
||||
})
|
||||
|
||||
it.concurrent('should return computed result for multi-line code', async () => {
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'const a = 1;\nconst b = 2;\nconst c = 3;\nconst d = 4;\nreturn a + b + c + d;',
|
||||
timeout: 5000,
|
||||
})
|
||||
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(data.success).toBe(true)
|
||||
expect(data.output.result).toBe(10)
|
||||
})
|
||||
|
||||
it.concurrent('should handle missing code parameter', async () => {
|
||||
const req = createMockRequest('POST', {
|
||||
timeout: 5000,
|
||||
@@ -439,6 +312,20 @@ describe('Function Execute API Route', () => {
|
||||
|
||||
describe('Enhanced Error Handling', () => {
|
||||
it('should provide detailed syntax error with line content', async () => {
|
||||
const syntaxError = new Error('Invalid or unexpected token')
|
||||
syntaxError.name = 'SyntaxError'
|
||||
syntaxError.stack = `user-function.js:5
|
||||
description: "This has a missing closing quote
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
SyntaxError: Invalid or unexpected token
|
||||
at new Script (node:vm:117:7)
|
||||
at POST (/path/to/route.ts:123:24)`
|
||||
|
||||
mockedScript.mockImplementationOnce(() => {
|
||||
throw syntaxError
|
||||
})
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'const obj = {\n name: "test",\n description: "This has a missing closing quote\n};\nreturn obj;',
|
||||
timeout: 5000,
|
||||
@@ -449,10 +336,28 @@ describe('Function Execute API Route', () => {
|
||||
|
||||
expect(response.status).toBe(500)
|
||||
expect(data.success).toBe(false)
|
||||
expect(data.error).toBeTruthy()
|
||||
expect(data.error).toContain('Syntax Error')
|
||||
expect(data.error).toContain('Line 3')
|
||||
expect(data.error).toContain('description: "This has a missing closing quote')
|
||||
expect(data.error).toContain('Invalid or unexpected token')
|
||||
expect(data.error).toContain('(Check for missing quotes, brackets, or semicolons)')
|
||||
|
||||
expect(data.debug).toBeDefined()
|
||||
expect(data.debug.line).toBe(3)
|
||||
expect(data.debug.errorType).toBe('SyntaxError')
|
||||
expect(data.debug.lineContent).toBe('description: "This has a missing closing quote')
|
||||
})
|
||||
|
||||
it('should provide detailed runtime error with line and column', async () => {
|
||||
const runtimeError = new Error("Cannot read properties of null (reading 'someMethod')")
|
||||
runtimeError.name = 'TypeError'
|
||||
runtimeError.stack = `TypeError: Cannot read properties of null (reading 'someMethod')
|
||||
at user-function.js:4:16
|
||||
at user-function.js:9:3
|
||||
at Script.runInContext (node:vm:147:14)`
|
||||
|
||||
mockRunInContext.mockRejectedValueOnce(runtimeError)
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'const obj = null;\nreturn obj.someMethod();',
|
||||
timeout: 5000,
|
||||
@@ -464,10 +369,26 @@ describe('Function Execute API Route', () => {
|
||||
expect(response.status).toBe(500)
|
||||
expect(data.success).toBe(false)
|
||||
expect(data.error).toContain('Type Error')
|
||||
expect(data.error).toContain('Line 2')
|
||||
expect(data.error).toContain('return obj.someMethod();')
|
||||
expect(data.error).toContain('Cannot read properties of null')
|
||||
|
||||
expect(data.debug).toBeDefined()
|
||||
expect(data.debug.line).toBe(2)
|
||||
expect(data.debug.column).toBe(16)
|
||||
expect(data.debug.errorType).toBe('TypeError')
|
||||
expect(data.debug.lineContent).toBe('return obj.someMethod();')
|
||||
})
|
||||
|
||||
it('should handle ReferenceError with enhanced details', async () => {
|
||||
const referenceError = new Error('undefinedVariable is not defined')
|
||||
referenceError.name = 'ReferenceError'
|
||||
referenceError.stack = `ReferenceError: undefinedVariable is not defined
|
||||
at user-function.js:4:8
|
||||
at Script.runInContext (node:vm:147:14)`
|
||||
|
||||
mockRunInContext.mockRejectedValueOnce(referenceError)
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'const x = 42;\nreturn undefinedVariable + x;',
|
||||
timeout: 5000,
|
||||
@@ -479,12 +400,21 @@ describe('Function Execute API Route', () => {
|
||||
expect(response.status).toBe(500)
|
||||
expect(data.success).toBe(false)
|
||||
expect(data.error).toContain('Reference Error')
|
||||
expect(data.error).toContain('Line 2')
|
||||
expect(data.error).toContain('return undefinedVariable + x;')
|
||||
expect(data.error).toContain('undefinedVariable is not defined')
|
||||
})
|
||||
|
||||
it('should handle thrown errors gracefully', async () => {
|
||||
it('should handle errors without line content gracefully', async () => {
|
||||
const genericError = new Error('Generic error without stack trace')
|
||||
genericError.name = 'Error'
|
||||
|
||||
mockedScript.mockImplementationOnce(() => {
|
||||
throw genericError
|
||||
})
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'throw new Error("Custom error message");',
|
||||
code: 'return "test";',
|
||||
timeout: 5000,
|
||||
})
|
||||
|
||||
@@ -493,10 +423,51 @@ describe('Function Execute API Route', () => {
|
||||
|
||||
expect(response.status).toBe(500)
|
||||
expect(data.success).toBe(false)
|
||||
expect(data.error).toContain('Custom error message')
|
||||
expect(data.error).toBe('Generic error without stack trace')
|
||||
|
||||
expect(data.debug).toBeDefined()
|
||||
expect(data.debug.errorType).toBe('Error')
|
||||
expect(data.debug.line).toBeUndefined()
|
||||
expect(data.debug.lineContent).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should extract line numbers from different stack trace formats', async () => {
|
||||
const testError = new Error('Test error')
|
||||
testError.name = 'Error'
|
||||
testError.stack = `Error: Test error
|
||||
at user-function.js:7:25
|
||||
at async function
|
||||
at Script.runInContext (node:vm:147:14)`
|
||||
|
||||
mockedScript.mockImplementationOnce(() => {
|
||||
throw testError
|
||||
})
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'const a = 1;\nconst b = 2;\nconst c = 3;\nconst d = 4;\nreturn a + b + c + d;',
|
||||
timeout: 5000,
|
||||
})
|
||||
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(500)
|
||||
expect(data.success).toBe(false)
|
||||
|
||||
expect(data.debug.line).toBe(5)
|
||||
expect(data.debug.column).toBe(25)
|
||||
expect(data.debug.lineContent).toBe('return a + b + c + d;')
|
||||
})
|
||||
|
||||
it.concurrent('should provide helpful suggestions for common syntax errors', async () => {
|
||||
const syntaxError = new Error('Unexpected end of input')
|
||||
syntaxError.name = 'SyntaxError'
|
||||
syntaxError.stack = 'user-function.js:4\nSyntaxError: Unexpected end of input'
|
||||
|
||||
mockedScript.mockImplementationOnce(() => {
|
||||
throw syntaxError
|
||||
})
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'const obj = {\n name: "test"\n// Missing closing brace',
|
||||
timeout: 5000,
|
||||
@@ -507,7 +478,9 @@ describe('Function Execute API Route', () => {
|
||||
|
||||
expect(response.status).toBe(500)
|
||||
expect(data.success).toBe(false)
|
||||
expect(data.error).toBeTruthy()
|
||||
expect(data.error).toContain('Syntax Error')
|
||||
expect(data.error).toContain('Unexpected end of input')
|
||||
expect(data.error).toContain('(Check for missing closing brackets or braces)')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import { createContext, Script } from 'vm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { isE2bEnabled } from '@/lib/core/config/feature-flags'
|
||||
import { validateProxyUrl } from '@/lib/core/security/input-validation'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { executeInE2B } from '@/lib/execution/e2b'
|
||||
import { executeInIsolatedVM } from '@/lib/execution/isolated-vm'
|
||||
import { CodeLanguage, DEFAULT_CODE_LANGUAGE, isValidCodeLanguage } from '@/lib/execution/languages'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import {
|
||||
createEnvVarPattern,
|
||||
createWorkflowVariablePattern,
|
||||
} from '@/executor/utils/reference-validation'
|
||||
export const dynamic = 'force-dynamic'
|
||||
export const runtime = 'nodejs'
|
||||
|
||||
@@ -16,6 +13,30 @@ export const MAX_DURATION = 210
|
||||
|
||||
const logger = createLogger('FunctionExecuteAPI')
|
||||
|
||||
function createSecureFetch(requestId: string) {
|
||||
const originalFetch = (globalThis as any).fetch || require('node-fetch').default
|
||||
|
||||
return async function secureFetch(input: any, init?: any) {
|
||||
const url = typeof input === 'string' ? input : input?.url || input
|
||||
|
||||
if (!url || typeof url !== 'string') {
|
||||
throw new Error('Invalid URL provided to fetch')
|
||||
}
|
||||
|
||||
const validation = validateProxyUrl(url)
|
||||
if (!validation.isValid) {
|
||||
logger.warn(`[${requestId}] Blocked fetch request due to SSRF validation`, {
|
||||
url: url.substring(0, 100),
|
||||
error: validation.error,
|
||||
})
|
||||
throw new Error(`Security Error: ${validation.error}`)
|
||||
}
|
||||
|
||||
return originalFetch(input, init)
|
||||
}
|
||||
}
|
||||
|
||||
// Constants for E2B code wrapping line counts
|
||||
const E2B_JS_WRAPPER_LINES = 3 // Lines before user code: ';(async () => {', ' try {', ' const __sim_result = await (async () => {'
|
||||
const E2B_PYTHON_WRAPPER_LINES = 1 // Lines before user code: 'def __sim_main__():'
|
||||
|
||||
@@ -325,9 +346,9 @@ function createUserFriendlyErrorMessage(
|
||||
): string {
|
||||
let errorMessage = enhanced.message
|
||||
|
||||
// Add line information if available
|
||||
// Add line and column information if available
|
||||
if (enhanced.line !== undefined) {
|
||||
let lineInfo = `Line ${enhanced.line}`
|
||||
let lineInfo = `Line ${enhanced.line}${enhanced.column !== undefined ? `:${enhanced.column}` : ''}`
|
||||
|
||||
// Add the actual line content if available
|
||||
if (enhanced.lineContent) {
|
||||
@@ -341,7 +362,8 @@ function createUserFriendlyErrorMessage(
|
||||
const stackMatch = enhanced.stack.match(/user-function\.js:(\d+)(?::(\d+))?/)
|
||||
if (stackMatch) {
|
||||
const line = Number.parseInt(stackMatch[1], 10)
|
||||
let lineInfo = `Line ${line}`
|
||||
const column = stackMatch[2] ? Number.parseInt(stackMatch[2], 10) : undefined
|
||||
let lineInfo = `Line ${line}${column ? `:${column}` : ''}`
|
||||
|
||||
// Try to get line content if we have userCode
|
||||
if (userCode) {
|
||||
@@ -378,6 +400,27 @@ function createUserFriendlyErrorMessage(
|
||||
}
|
||||
}
|
||||
|
||||
// For syntax errors, provide additional context
|
||||
if (enhanced.name === 'SyntaxError') {
|
||||
if (errorMessage.includes('Invalid or unexpected token')) {
|
||||
errorMessage += ' (Check for missing quotes, brackets, or semicolons)'
|
||||
} else if (errorMessage.includes('Unexpected end of input')) {
|
||||
errorMessage += ' (Check for missing closing brackets or braces)'
|
||||
} else if (errorMessage.includes('Unexpected token')) {
|
||||
// Check if this might be due to incomplete code
|
||||
if (
|
||||
enhanced.lineContent &&
|
||||
((enhanced.lineContent.includes('(') && !enhanced.lineContent.includes(')')) ||
|
||||
(enhanced.lineContent.includes('[') && !enhanced.lineContent.includes(']')) ||
|
||||
(enhanced.lineContent.includes('{') && !enhanced.lineContent.includes('}')))
|
||||
) {
|
||||
errorMessage += ' (Check for missing closing parentheses, brackets, or braces)'
|
||||
} else {
|
||||
errorMessage += ' (Check your syntax)'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errorMessage
|
||||
}
|
||||
|
||||
@@ -391,27 +434,19 @@ function resolveWorkflowVariables(
|
||||
): string {
|
||||
let resolvedCode = code
|
||||
|
||||
const regex = createWorkflowVariablePattern()
|
||||
let match: RegExpExecArray | null
|
||||
const replacements: Array<{
|
||||
match: string
|
||||
index: number
|
||||
variableName: string
|
||||
variableValue: unknown
|
||||
}> = []
|
||||
|
||||
while ((match = regex.exec(code)) !== null) {
|
||||
const variableName = match[1].trim()
|
||||
const variableMatches = resolvedCode.match(/<variable\.([^>]+)>/g) || []
|
||||
for (const match of variableMatches) {
|
||||
const variableName = match.slice('<variable.'.length, -1).trim()
|
||||
|
||||
// Find the variable by name (workflowVariables is indexed by ID, values are variable objects)
|
||||
const foundVariable = Object.entries(workflowVariables).find(
|
||||
([_, variable]) => (variable.name || '').replace(/\s+/g, '') === variableName
|
||||
)
|
||||
|
||||
let variableValue: unknown = ''
|
||||
if (foundVariable) {
|
||||
const variable = foundVariable[1]
|
||||
variableValue = variable.value
|
||||
// Get the typed value - handle different variable types
|
||||
let variableValue = variable.value
|
||||
|
||||
if (variable.value !== undefined && variable.value !== null) {
|
||||
try {
|
||||
@@ -433,30 +468,22 @@ function resolveWorkflowVariables(
|
||||
// Keep original value if JSON parsing fails
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
} catch (error) {
|
||||
// Fallback to original value on error
|
||||
variableValue = variable.value
|
||||
}
|
||||
}
|
||||
|
||||
// Create a safe variable reference
|
||||
const safeVarName = `__variable_${variableName.replace(/[^a-zA-Z0-9_]/g, '_')}`
|
||||
contextVariables[safeVarName] = variableValue
|
||||
|
||||
// Replace the variable reference with the safe variable name
|
||||
resolvedCode = resolvedCode.replace(new RegExp(escapeRegExp(match), 'g'), safeVarName)
|
||||
} else {
|
||||
// Variable not found - replace with empty string to avoid syntax errors
|
||||
resolvedCode = resolvedCode.replace(new RegExp(escapeRegExp(match), 'g'), '')
|
||||
}
|
||||
|
||||
replacements.push({
|
||||
match: match[0],
|
||||
index: match.index,
|
||||
variableName,
|
||||
variableValue,
|
||||
})
|
||||
}
|
||||
|
||||
// Process replacements in reverse order to maintain correct indices
|
||||
for (let i = replacements.length - 1; i >= 0; i--) {
|
||||
const { match: matchStr, index, variableName, variableValue } = replacements[i]
|
||||
|
||||
// Use variable reference approach
|
||||
const safeVarName = `__variable_${variableName.replace(/[^a-zA-Z0-9_]/g, '_')}`
|
||||
contextVariables[safeVarName] = variableValue
|
||||
resolvedCode =
|
||||
resolvedCode.slice(0, index) + safeVarName + resolvedCode.slice(index + matchStr.length)
|
||||
}
|
||||
|
||||
return resolvedCode
|
||||
@@ -473,29 +500,18 @@ function resolveEnvironmentVariables(
|
||||
): string {
|
||||
let resolvedCode = code
|
||||
|
||||
const regex = createEnvVarPattern()
|
||||
let match: RegExpExecArray | null
|
||||
const replacements: Array<{ match: string; index: number; varName: string; varValue: string }> =
|
||||
[]
|
||||
|
||||
while ((match = regex.exec(code)) !== null) {
|
||||
const varName = match[1].trim()
|
||||
const envVarMatches = resolvedCode.match(/\{\{([^}]+)\}\}/g) || []
|
||||
for (const match of envVarMatches) {
|
||||
const varName = match.slice(2, -2).trim()
|
||||
// Priority: 1. Environment variables from workflow, 2. Params
|
||||
const varValue = envVars[varName] || params[varName] || ''
|
||||
replacements.push({
|
||||
match: match[0],
|
||||
index: match.index,
|
||||
varName,
|
||||
varValue: String(varValue),
|
||||
})
|
||||
}
|
||||
|
||||
for (let i = replacements.length - 1; i >= 0; i--) {
|
||||
const { match: matchStr, index, varName, varValue } = replacements[i]
|
||||
|
||||
// Instead of injecting large JSON directly, create a variable reference
|
||||
const safeVarName = `__var_${varName.replace(/[^a-zA-Z0-9_]/g, '_')}`
|
||||
contextVariables[safeVarName] = varValue
|
||||
resolvedCode =
|
||||
resolvedCode.slice(0, index) + safeVarName + resolvedCode.slice(index + matchStr.length)
|
||||
|
||||
// Replace the template with a variable reference
|
||||
resolvedCode = resolvedCode.replace(new RegExp(escapeRegExp(match), 'g'), safeVarName)
|
||||
}
|
||||
|
||||
return resolvedCode
|
||||
@@ -883,7 +899,28 @@ export async function POST(req: NextRequest) {
|
||||
})
|
||||
}
|
||||
|
||||
const executionMethod = 'isolated-vm'
|
||||
const executionMethod = 'vm'
|
||||
const context = createContext({
|
||||
params: executionParams,
|
||||
environmentVariables: envVars,
|
||||
...contextVariables,
|
||||
fetch: createSecureFetch(requestId),
|
||||
console: {
|
||||
log: (...args: any[]) => {
|
||||
const logMessage = `${args
|
||||
.map((arg) => (typeof arg === 'object' ? JSON.stringify(arg) : String(arg)))
|
||||
.join(' ')}\n`
|
||||
stdout += logMessage
|
||||
},
|
||||
error: (...args: any[]) => {
|
||||
const errorMessage = `${args
|
||||
.map((arg) => (typeof arg === 'object' ? JSON.stringify(arg) : String(arg)))
|
||||
.join(' ')}\n`
|
||||
logger.error(`[${requestId}] Code Console Error: ${errorMessage}`)
|
||||
stdout += `ERROR: ${errorMessage}`
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const wrapperLines = ['(async () => {', ' try {']
|
||||
if (isCustomTool) {
|
||||
@@ -893,99 +930,36 @@ export async function POST(req: NextRequest) {
|
||||
})
|
||||
}
|
||||
userCodeStartLine = wrapperLines.length + 1
|
||||
const fullScript = [
|
||||
...wrapperLines,
|
||||
` ${resolvedCode.split('\n').join('\n ')}`,
|
||||
' } catch (error) {',
|
||||
' console.error(error);',
|
||||
' throw error;',
|
||||
' }',
|
||||
'})()',
|
||||
].join('\n')
|
||||
|
||||
let codeToExecute = resolvedCode
|
||||
let prependedLineCount = 0
|
||||
if (isCustomTool) {
|
||||
const paramKeys = Object.keys(executionParams)
|
||||
const paramDestructuring = paramKeys.map((key) => `const ${key} = params.${key};`).join('\n')
|
||||
codeToExecute = `${paramDestructuring}\n${resolvedCode}`
|
||||
prependedLineCount = paramKeys.length
|
||||
}
|
||||
const script = new Script(fullScript, {
|
||||
filename: 'user-function.js',
|
||||
lineOffset: 0,
|
||||
columnOffset: 0,
|
||||
})
|
||||
|
||||
const isolatedResult = await executeInIsolatedVM({
|
||||
code: codeToExecute,
|
||||
params: executionParams,
|
||||
envVars,
|
||||
contextVariables,
|
||||
timeoutMs: timeout,
|
||||
requestId,
|
||||
const result = await script.runInContext(context, {
|
||||
timeout,
|
||||
displayErrors: true,
|
||||
breakOnSigint: true,
|
||||
})
|
||||
|
||||
const executionTime = Date.now() - startTime
|
||||
|
||||
if (isolatedResult.error) {
|
||||
logger.error(`[${requestId}] Function execution failed in isolated-vm`, {
|
||||
error: isolatedResult.error,
|
||||
executionTime,
|
||||
})
|
||||
|
||||
const ivmError = isolatedResult.error
|
||||
// Adjust line number for prepended param destructuring in custom tools
|
||||
let adjustedLine = ivmError.line
|
||||
let adjustedLineContent = ivmError.lineContent
|
||||
if (prependedLineCount > 0 && ivmError.line !== undefined) {
|
||||
adjustedLine = Math.max(1, ivmError.line - prependedLineCount)
|
||||
// Get line content from original user code, not the prepended code
|
||||
const codeLines = resolvedCode.split('\n')
|
||||
if (adjustedLine <= codeLines.length) {
|
||||
adjustedLineContent = codeLines[adjustedLine - 1]?.trim()
|
||||
}
|
||||
}
|
||||
const enhancedError: EnhancedError = {
|
||||
message: ivmError.message,
|
||||
name: ivmError.name,
|
||||
stack: ivmError.stack,
|
||||
originalError: ivmError,
|
||||
line: adjustedLine,
|
||||
column: ivmError.column,
|
||||
lineContent: adjustedLineContent,
|
||||
}
|
||||
|
||||
const userFriendlyErrorMessage = createUserFriendlyErrorMessage(
|
||||
enhancedError,
|
||||
requestId,
|
||||
resolvedCode
|
||||
)
|
||||
|
||||
logger.error(`[${requestId}] Enhanced error details`, {
|
||||
originalMessage: ivmError.message,
|
||||
enhancedMessage: userFriendlyErrorMessage,
|
||||
line: enhancedError.line,
|
||||
column: enhancedError.column,
|
||||
lineContent: enhancedError.lineContent,
|
||||
errorType: enhancedError.name,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: userFriendlyErrorMessage,
|
||||
output: {
|
||||
result: null,
|
||||
stdout: cleanStdout(isolatedResult.stdout),
|
||||
executionTime,
|
||||
},
|
||||
debug: {
|
||||
line: enhancedError.line,
|
||||
column: enhancedError.column,
|
||||
errorType: enhancedError.name,
|
||||
lineContent: enhancedError.lineContent,
|
||||
stack: enhancedError.stack,
|
||||
},
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
stdout = isolatedResult.stdout
|
||||
logger.info(`[${requestId}] Function executed successfully using ${executionMethod}`, {
|
||||
executionTime,
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: { result: isolatedResult.result, stdout: cleanStdout(stdout), executionTime },
|
||||
output: { result, stdout: cleanStdout(stdout), executionTime },
|
||||
})
|
||||
} catch (error: any) {
|
||||
const executionTime = Date.now() - startTime
|
||||
@@ -1002,6 +976,7 @@ export async function POST(req: NextRequest) {
|
||||
resolvedCode
|
||||
)
|
||||
|
||||
// Log enhanced error details for debugging
|
||||
logger.error(`[${requestId}] Enhanced error details`, {
|
||||
originalMessage: error.message,
|
||||
enhancedMessage: userFriendlyErrorMessage,
|
||||
@@ -1020,6 +995,7 @@ export async function POST(req: NextRequest) {
|
||||
stdout: cleanStdout(stdout),
|
||||
executionTime,
|
||||
},
|
||||
// Include debug information in development or for debugging
|
||||
debug: {
|
||||
line: enhancedError.line,
|
||||
column: enhancedError.column,
|
||||
|
||||
@@ -6,22 +6,7 @@ import {
|
||||
workflowDeploymentVersion,
|
||||
workflowExecutionLogs,
|
||||
} from '@sim/db/schema'
|
||||
import {
|
||||
and,
|
||||
desc,
|
||||
eq,
|
||||
gt,
|
||||
gte,
|
||||
inArray,
|
||||
isNotNull,
|
||||
isNull,
|
||||
lt,
|
||||
lte,
|
||||
ne,
|
||||
or,
|
||||
type SQL,
|
||||
sql,
|
||||
} from 'drizzle-orm'
|
||||
import { and, desc, eq, gte, inArray, isNotNull, isNull, lte, or, type SQL, sql } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { getSession } from '@/lib/auth'
|
||||
@@ -37,19 +22,14 @@ const QueryParamsSchema = z.object({
|
||||
limit: z.coerce.number().optional().default(100),
|
||||
offset: z.coerce.number().optional().default(0),
|
||||
level: z.string().optional(),
|
||||
workflowIds: z.string().optional(),
|
||||
folderIds: z.string().optional(),
|
||||
triggers: z.string().optional(),
|
||||
workflowIds: z.string().optional(), // Comma-separated list of workflow IDs
|
||||
folderIds: z.string().optional(), // Comma-separated list of folder IDs
|
||||
triggers: z.string().optional(), // Comma-separated list of trigger types
|
||||
startDate: z.string().optional(),
|
||||
endDate: z.string().optional(),
|
||||
search: z.string().optional(),
|
||||
workflowName: z.string().optional(),
|
||||
folderName: z.string().optional(),
|
||||
executionId: z.string().optional(),
|
||||
costOperator: z.enum(['=', '>', '<', '>=', '<=', '!=']).optional(),
|
||||
costValue: z.coerce.number().optional(),
|
||||
durationOperator: z.enum(['=', '>', '<', '>=', '<=', '!=']).optional(),
|
||||
durationValue: z.coerce.number().optional(),
|
||||
workspaceId: z.string(),
|
||||
})
|
||||
|
||||
@@ -69,6 +49,7 @@ export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const params = QueryParamsSchema.parse(Object.fromEntries(searchParams.entries()))
|
||||
|
||||
// Conditionally select columns based on detail level to optimize performance
|
||||
const selectColumns =
|
||||
params.details === 'full'
|
||||
? {
|
||||
@@ -82,9 +63,9 @@ export async function GET(request: NextRequest) {
|
||||
startedAt: workflowExecutionLogs.startedAt,
|
||||
endedAt: workflowExecutionLogs.endedAt,
|
||||
totalDurationMs: workflowExecutionLogs.totalDurationMs,
|
||||
executionData: workflowExecutionLogs.executionData,
|
||||
executionData: workflowExecutionLogs.executionData, // Large field - only in full mode
|
||||
cost: workflowExecutionLogs.cost,
|
||||
files: workflowExecutionLogs.files,
|
||||
files: workflowExecutionLogs.files, // Large field - only in full mode
|
||||
createdAt: workflowExecutionLogs.createdAt,
|
||||
workflowName: workflow.name,
|
||||
workflowDescription: workflow.description,
|
||||
@@ -101,6 +82,7 @@ export async function GET(request: NextRequest) {
|
||||
deploymentVersionName: workflowDeploymentVersion.name,
|
||||
}
|
||||
: {
|
||||
// Basic mode - exclude large fields for better performance
|
||||
id: workflowExecutionLogs.id,
|
||||
workflowId: workflowExecutionLogs.workflowId,
|
||||
executionId: workflowExecutionLogs.executionId,
|
||||
@@ -111,9 +93,9 @@ export async function GET(request: NextRequest) {
|
||||
startedAt: workflowExecutionLogs.startedAt,
|
||||
endedAt: workflowExecutionLogs.endedAt,
|
||||
totalDurationMs: workflowExecutionLogs.totalDurationMs,
|
||||
executionData: sql<null>`NULL`,
|
||||
executionData: sql<null>`NULL`, // Exclude large execution data in basic mode
|
||||
cost: workflowExecutionLogs.cost,
|
||||
files: sql<null>`NULL`,
|
||||
files: sql<null>`NULL`, // Exclude files in basic mode
|
||||
createdAt: workflowExecutionLogs.createdAt,
|
||||
workflowName: workflow.name,
|
||||
workflowDescription: workflow.description,
|
||||
@@ -127,7 +109,7 @@ export async function GET(request: NextRequest) {
|
||||
pausedTotalPauseCount: pausedExecutions.totalPauseCount,
|
||||
pausedResumedCount: pausedExecutions.resumedCount,
|
||||
deploymentVersion: workflowDeploymentVersion.version,
|
||||
deploymentVersionName: sql<null>`NULL`,
|
||||
deploymentVersionName: sql<null>`NULL`, // Only needed in full mode for details panel
|
||||
}
|
||||
|
||||
const baseQuery = db
|
||||
@@ -157,28 +139,34 @@ export async function GET(request: NextRequest) {
|
||||
)
|
||||
)
|
||||
|
||||
// Build additional conditions for the query
|
||||
let conditions: SQL | undefined
|
||||
|
||||
// Filter by level with support for derived statuses (running, pending)
|
||||
if (params.level && params.level !== 'all') {
|
||||
const levels = params.level.split(',').filter(Boolean)
|
||||
const levelConditions: SQL[] = []
|
||||
|
||||
for (const level of levels) {
|
||||
if (level === 'error') {
|
||||
// Direct database field
|
||||
levelConditions.push(eq(workflowExecutionLogs.level, 'error'))
|
||||
} else if (level === 'info') {
|
||||
// Completed info logs only (not running, not pending)
|
||||
const condition = and(
|
||||
eq(workflowExecutionLogs.level, 'info'),
|
||||
isNotNull(workflowExecutionLogs.endedAt)
|
||||
)
|
||||
if (condition) levelConditions.push(condition)
|
||||
} else if (level === 'running') {
|
||||
// Running logs: info level with no endedAt
|
||||
const condition = and(
|
||||
eq(workflowExecutionLogs.level, 'info'),
|
||||
isNull(workflowExecutionLogs.endedAt)
|
||||
)
|
||||
if (condition) levelConditions.push(condition)
|
||||
} else if (level === 'pending') {
|
||||
// Pending logs: info level with pause status indicators
|
||||
const condition = and(
|
||||
eq(workflowExecutionLogs.level, 'info'),
|
||||
or(
|
||||
@@ -201,6 +189,7 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by specific workflow IDs
|
||||
if (params.workflowIds) {
|
||||
const workflowIds = params.workflowIds.split(',').filter(Boolean)
|
||||
if (workflowIds.length > 0) {
|
||||
@@ -208,6 +197,7 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by folder IDs
|
||||
if (params.folderIds) {
|
||||
const folderIds = params.folderIds.split(',').filter(Boolean)
|
||||
if (folderIds.length > 0) {
|
||||
@@ -215,6 +205,7 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by triggers
|
||||
if (params.triggers) {
|
||||
const triggers = params.triggers.split(',').filter(Boolean)
|
||||
if (triggers.length > 0 && !triggers.includes('all')) {
|
||||
@@ -222,6 +213,7 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by date range
|
||||
if (params.startDate) {
|
||||
conditions = and(
|
||||
conditions,
|
||||
@@ -232,79 +224,33 @@ export async function GET(request: NextRequest) {
|
||||
conditions = and(conditions, lte(workflowExecutionLogs.startedAt, new Date(params.endDate)))
|
||||
}
|
||||
|
||||
// Filter by search query
|
||||
if (params.search) {
|
||||
const searchTerm = `%${params.search}%`
|
||||
// With message removed, restrict search to executionId only
|
||||
conditions = and(conditions, sql`${workflowExecutionLogs.executionId} ILIKE ${searchTerm}`)
|
||||
}
|
||||
|
||||
// Filter by workflow name (from advanced search input)
|
||||
if (params.workflowName) {
|
||||
const nameTerm = `%${params.workflowName}%`
|
||||
conditions = and(conditions, sql`${workflow.name} ILIKE ${nameTerm}`)
|
||||
}
|
||||
|
||||
// Filter by folder name (best-effort text match when present on workflows)
|
||||
if (params.folderName) {
|
||||
const folderTerm = `%${params.folderName}%`
|
||||
conditions = and(conditions, sql`${workflow.name} ILIKE ${folderTerm}`)
|
||||
}
|
||||
|
||||
if (params.executionId) {
|
||||
conditions = and(conditions, eq(workflowExecutionLogs.executionId, params.executionId))
|
||||
}
|
||||
|
||||
if (params.costOperator && params.costValue !== undefined) {
|
||||
const costField = sql`(${workflowExecutionLogs.cost}->>'total')::numeric`
|
||||
switch (params.costOperator) {
|
||||
case '=':
|
||||
conditions = and(conditions, sql`${costField} = ${params.costValue}`)
|
||||
break
|
||||
case '>':
|
||||
conditions = and(conditions, sql`${costField} > ${params.costValue}`)
|
||||
break
|
||||
case '<':
|
||||
conditions = and(conditions, sql`${costField} < ${params.costValue}`)
|
||||
break
|
||||
case '>=':
|
||||
conditions = and(conditions, sql`${costField} >= ${params.costValue}`)
|
||||
break
|
||||
case '<=':
|
||||
conditions = and(conditions, sql`${costField} <= ${params.costValue}`)
|
||||
break
|
||||
case '!=':
|
||||
conditions = and(conditions, sql`${costField} != ${params.costValue}`)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (params.durationOperator && params.durationValue !== undefined) {
|
||||
const durationField = workflowExecutionLogs.totalDurationMs
|
||||
switch (params.durationOperator) {
|
||||
case '=':
|
||||
conditions = and(conditions, eq(durationField, params.durationValue))
|
||||
break
|
||||
case '>':
|
||||
conditions = and(conditions, gt(durationField, params.durationValue))
|
||||
break
|
||||
case '<':
|
||||
conditions = and(conditions, lt(durationField, params.durationValue))
|
||||
break
|
||||
case '>=':
|
||||
conditions = and(conditions, gte(durationField, params.durationValue))
|
||||
break
|
||||
case '<=':
|
||||
conditions = and(conditions, lte(durationField, params.durationValue))
|
||||
break
|
||||
case '!=':
|
||||
conditions = and(conditions, ne(durationField, params.durationValue))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the query using the optimized join
|
||||
const logs = await baseQuery
|
||||
.where(conditions)
|
||||
.orderBy(desc(workflowExecutionLogs.startedAt))
|
||||
.limit(params.limit)
|
||||
.offset(params.offset)
|
||||
|
||||
// Get total count for pagination using the same join structure
|
||||
const countQuery = db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(workflowExecutionLogs)
|
||||
@@ -333,10 +279,13 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
const count = countResult[0]?.count || 0
|
||||
|
||||
// Block executions are now extracted from trace spans instead of separate table
|
||||
const blockExecutionsByExecution: Record<string, any[]> = {}
|
||||
|
||||
// Create clean trace spans from block executions
|
||||
const createTraceSpans = (blockExecutions: any[]) => {
|
||||
return blockExecutions.map((block, index) => {
|
||||
// For error blocks, include error information in the output
|
||||
let output = block.outputData
|
||||
if (block.status === 'error' && block.errorMessage) {
|
||||
output = {
|
||||
@@ -365,6 +314,7 @@ export async function GET(request: NextRequest) {
|
||||
})
|
||||
}
|
||||
|
||||
// Extract cost information from block executions
|
||||
const extractCostSummary = (blockExecutions: any[]) => {
|
||||
let totalCost = 0
|
||||
let totalInputCost = 0
|
||||
@@ -383,6 +333,7 @@ export async function GET(request: NextRequest) {
|
||||
totalPromptTokens += block.cost.tokens?.prompt || 0
|
||||
totalCompletionTokens += block.cost.tokens?.completion || 0
|
||||
|
||||
// Track per-model costs
|
||||
if (block.cost.model) {
|
||||
if (!models.has(block.cost.model)) {
|
||||
models.set(block.cost.model, {
|
||||
@@ -412,29 +363,34 @@ export async function GET(request: NextRequest) {
|
||||
prompt: totalPromptTokens,
|
||||
completion: totalCompletionTokens,
|
||||
},
|
||||
models: Object.fromEntries(models),
|
||||
models: Object.fromEntries(models), // Convert Map to object for JSON serialization
|
||||
}
|
||||
}
|
||||
|
||||
// Transform to clean log format with workflow data included
|
||||
const enhancedLogs = logs.map((log) => {
|
||||
const blockExecutions = blockExecutionsByExecution[log.executionId] || []
|
||||
|
||||
// Only process trace spans and detailed cost in full mode
|
||||
let traceSpans = []
|
||||
let finalOutput: any
|
||||
let costSummary = (log.cost as any) || { total: 0 }
|
||||
|
||||
if (params.details === 'full' && log.executionData) {
|
||||
// Use stored trace spans if available, otherwise create from block executions
|
||||
const storedTraceSpans = (log.executionData as any)?.traceSpans
|
||||
traceSpans =
|
||||
storedTraceSpans && Array.isArray(storedTraceSpans) && storedTraceSpans.length > 0
|
||||
? storedTraceSpans
|
||||
: createTraceSpans(blockExecutions)
|
||||
|
||||
// Prefer stored cost JSON; otherwise synthesize from blocks
|
||||
costSummary =
|
||||
log.cost && Object.keys(log.cost as any).length > 0
|
||||
? (log.cost as any)
|
||||
: extractCostSummary(blockExecutions)
|
||||
|
||||
// Include finalOutput if present on executionData
|
||||
try {
|
||||
const fo = (log.executionData as any)?.finalOutput
|
||||
if (fo !== undefined) finalOutput = fo
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
import { db } from '@sim/db'
|
||||
import { permissions, workflowMcpServer, workspace } from '@sim/db/schema'
|
||||
import { and, eq, sql } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
|
||||
const logger = createLogger('McpDiscoverAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
/**
|
||||
* GET - Discover all published MCP servers available to the authenticated user
|
||||
*
|
||||
* This endpoint allows external MCP clients to discover available servers
|
||||
* using just their API key, without needing to know workspace IDs.
|
||||
*
|
||||
* Authentication: API Key (X-API-Key header) or Session
|
||||
*
|
||||
* Returns all published MCP servers from workspaces the user has access to.
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// Authenticate the request
|
||||
const auth = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Authentication required. Provide X-API-Key header with your Sim API key.',
|
||||
},
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const userId = auth.userId
|
||||
|
||||
// Get all workspaces the user has access to via permissions table
|
||||
const userWorkspacePermissions = await db
|
||||
.select({ entityId: permissions.entityId })
|
||||
.from(permissions)
|
||||
.where(and(eq(permissions.userId, userId), eq(permissions.entityType, 'workspace')))
|
||||
|
||||
const workspaceIds = userWorkspacePermissions.map((w) => w.entityId)
|
||||
|
||||
if (workspaceIds.length === 0) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
servers: [],
|
||||
message: 'No workspaces found for this user',
|
||||
})
|
||||
}
|
||||
|
||||
// Get all published MCP servers from user's workspaces with tool count
|
||||
const servers = await db
|
||||
.select({
|
||||
id: workflowMcpServer.id,
|
||||
name: workflowMcpServer.name,
|
||||
description: workflowMcpServer.description,
|
||||
workspaceId: workflowMcpServer.workspaceId,
|
||||
workspaceName: workspace.name,
|
||||
isPublished: workflowMcpServer.isPublished,
|
||||
publishedAt: workflowMcpServer.publishedAt,
|
||||
toolCount: sql<number>`(
|
||||
SELECT COUNT(*)::int
|
||||
FROM "workflow_mcp_tool"
|
||||
WHERE "workflow_mcp_tool"."server_id" = "workflow_mcp_server"."id"
|
||||
)`.as('tool_count'),
|
||||
})
|
||||
.from(workflowMcpServer)
|
||||
.leftJoin(workspace, eq(workflowMcpServer.workspaceId, workspace.id))
|
||||
.where(
|
||||
and(
|
||||
eq(workflowMcpServer.isPublished, true),
|
||||
sql`${workflowMcpServer.workspaceId} IN ${workspaceIds}`
|
||||
)
|
||||
)
|
||||
.orderBy(workflowMcpServer.name)
|
||||
|
||||
const baseUrl = getBaseUrl()
|
||||
|
||||
// Format response with connection URLs
|
||||
const formattedServers = servers.map((server) => ({
|
||||
id: server.id,
|
||||
name: server.name,
|
||||
description: server.description,
|
||||
workspace: {
|
||||
id: server.workspaceId,
|
||||
name: server.workspaceName,
|
||||
},
|
||||
toolCount: server.toolCount || 0,
|
||||
publishedAt: server.publishedAt,
|
||||
urls: {
|
||||
http: `${baseUrl}/api/mcp/serve/${server.id}`,
|
||||
sse: `${baseUrl}/api/mcp/serve/${server.id}/sse`,
|
||||
},
|
||||
}))
|
||||
|
||||
logger.info(`User ${userId} discovered ${formattedServers.length} MCP servers`)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
servers: formattedServers,
|
||||
authentication: {
|
||||
method: 'API Key',
|
||||
header: 'X-API-Key',
|
||||
description: 'Include your Sim API key in the X-API-Key header for all MCP requests',
|
||||
},
|
||||
usage: {
|
||||
listTools: {
|
||||
method: 'POST',
|
||||
body: '{"jsonrpc":"2.0","id":1,"method":"tools/list"}',
|
||||
},
|
||||
callTool: {
|
||||
method: 'POST',
|
||||
body: '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"TOOL_NAME","arguments":{}}}',
|
||||
},
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error discovering MCP servers:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to discover MCP servers' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,360 +0,0 @@
|
||||
import { db } from '@sim/db'
|
||||
import { workflow, workflowMcpServer, workflowMcpTool } from '@sim/db/schema'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
|
||||
const logger = createLogger('WorkflowMcpServeAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
interface RouteParams {
|
||||
serverId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* MCP JSON-RPC Request
|
||||
*/
|
||||
interface JsonRpcRequest {
|
||||
jsonrpc: '2.0'
|
||||
id: string | number
|
||||
method: string
|
||||
params?: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* MCP JSON-RPC Response
|
||||
*/
|
||||
interface JsonRpcResponse {
|
||||
jsonrpc: '2.0'
|
||||
id: string | number
|
||||
result?: unknown
|
||||
error?: {
|
||||
code: number
|
||||
message: string
|
||||
data?: unknown
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create JSON-RPC success response
|
||||
*/
|
||||
function createJsonRpcResponse(id: string | number, result: unknown): JsonRpcResponse {
|
||||
return {
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
result,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create JSON-RPC error response
|
||||
*/
|
||||
function createJsonRpcError(
|
||||
id: string | number,
|
||||
code: number,
|
||||
message: string,
|
||||
data?: unknown
|
||||
): JsonRpcResponse {
|
||||
return {
|
||||
jsonrpc: '2.0',
|
||||
id,
|
||||
error: { code, message, data },
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that the server exists and is published
|
||||
*/
|
||||
async function validateServer(serverId: string) {
|
||||
const [server] = await db
|
||||
.select({
|
||||
id: workflowMcpServer.id,
|
||||
name: workflowMcpServer.name,
|
||||
workspaceId: workflowMcpServer.workspaceId,
|
||||
isPublished: workflowMcpServer.isPublished,
|
||||
})
|
||||
.from(workflowMcpServer)
|
||||
.where(eq(workflowMcpServer.id, serverId))
|
||||
.limit(1)
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
/**
|
||||
* GET - Server info and capabilities (MCP initialize)
|
||||
*/
|
||||
export async function GET(request: NextRequest, { params }: { params: Promise<RouteParams> }) {
|
||||
const { serverId } = await params
|
||||
|
||||
try {
|
||||
const server = await validateServer(serverId)
|
||||
|
||||
if (!server) {
|
||||
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
if (!server.isPublished) {
|
||||
return NextResponse.json({ error: 'Server is not published' }, { status: 403 })
|
||||
}
|
||||
|
||||
// Return server capabilities
|
||||
return NextResponse.json({
|
||||
name: server.name,
|
||||
version: '1.0.0',
|
||||
protocolVersion: '2024-11-05',
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
instructions: `This MCP server exposes workflow tools from Sim Studio. Each tool executes a deployed workflow.`,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error getting MCP server info:', error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST - Handle MCP JSON-RPC requests
|
||||
*/
|
||||
export async function POST(request: NextRequest, { params }: { params: Promise<RouteParams> }) {
|
||||
const { serverId } = await params
|
||||
|
||||
try {
|
||||
// Validate server
|
||||
const server = await validateServer(serverId)
|
||||
|
||||
if (!server) {
|
||||
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
if (!server.isPublished) {
|
||||
return NextResponse.json({ error: 'Server is not published' }, { status: 403 })
|
||||
}
|
||||
|
||||
// Authenticate the request
|
||||
const auth = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
// Parse JSON-RPC request
|
||||
const body = await request.json()
|
||||
const rpcRequest = body as JsonRpcRequest
|
||||
|
||||
if (rpcRequest.jsonrpc !== '2.0' || !rpcRequest.method) {
|
||||
return NextResponse.json(createJsonRpcError(rpcRequest?.id || 0, -32600, 'Invalid Request'), {
|
||||
status: 400,
|
||||
})
|
||||
}
|
||||
|
||||
// Handle different MCP methods
|
||||
switch (rpcRequest.method) {
|
||||
case 'initialize':
|
||||
return NextResponse.json(
|
||||
createJsonRpcResponse(rpcRequest.id, {
|
||||
protocolVersion: '2024-11-05',
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
serverInfo: {
|
||||
name: server.name,
|
||||
version: '1.0.0',
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
case 'tools/list':
|
||||
return handleToolsList(rpcRequest, serverId)
|
||||
|
||||
case 'tools/call': {
|
||||
// Get the API key from the request to forward to the workflow execute call
|
||||
const apiKey =
|
||||
request.headers.get('X-API-Key') ||
|
||||
request.headers.get('Authorization')?.replace('Bearer ', '')
|
||||
return handleToolsCall(rpcRequest, serverId, auth.userId, server.workspaceId, apiKey)
|
||||
}
|
||||
|
||||
case 'ping':
|
||||
return NextResponse.json(createJsonRpcResponse(rpcRequest.id, {}))
|
||||
|
||||
default:
|
||||
return NextResponse.json(
|
||||
createJsonRpcError(rpcRequest.id, -32601, `Method not found: ${rpcRequest.method}`),
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error handling MCP request:', error)
|
||||
return NextResponse.json(createJsonRpcError(0, -32603, 'Internal error'), { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle tools/list method
|
||||
*/
|
||||
async function handleToolsList(
|
||||
rpcRequest: JsonRpcRequest,
|
||||
serverId: string
|
||||
): Promise<NextResponse> {
|
||||
try {
|
||||
const tools = await db
|
||||
.select({
|
||||
id: workflowMcpTool.id,
|
||||
toolName: workflowMcpTool.toolName,
|
||||
toolDescription: workflowMcpTool.toolDescription,
|
||||
parameterSchema: workflowMcpTool.parameterSchema,
|
||||
isEnabled: workflowMcpTool.isEnabled,
|
||||
workflowId: workflowMcpTool.workflowId,
|
||||
})
|
||||
.from(workflowMcpTool)
|
||||
.where(eq(workflowMcpTool.serverId, serverId))
|
||||
|
||||
const mcpTools = tools
|
||||
.filter((tool) => tool.isEnabled)
|
||||
.map((tool) => ({
|
||||
name: tool.toolName,
|
||||
description: tool.toolDescription || `Execute workflow tool: ${tool.toolName}`,
|
||||
inputSchema: tool.parameterSchema || {
|
||||
type: 'object',
|
||||
properties: {
|
||||
input: {
|
||||
type: 'object',
|
||||
description: 'Input data for the workflow',
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
return NextResponse.json(createJsonRpcResponse(rpcRequest.id, { tools: mcpTools }))
|
||||
} catch (error) {
|
||||
logger.error('Error listing tools:', error)
|
||||
return NextResponse.json(createJsonRpcError(rpcRequest.id, -32603, 'Failed to list tools'), {
|
||||
status: 500,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle tools/call method
|
||||
*/
|
||||
async function handleToolsCall(
|
||||
rpcRequest: JsonRpcRequest,
|
||||
serverId: string,
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
apiKey?: string | null
|
||||
): Promise<NextResponse> {
|
||||
try {
|
||||
const params = rpcRequest.params as
|
||||
| { name: string; arguments?: Record<string, unknown> }
|
||||
| undefined
|
||||
|
||||
if (!params?.name) {
|
||||
return NextResponse.json(
|
||||
createJsonRpcError(rpcRequest.id, -32602, 'Invalid params: tool name required'),
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Find the tool
|
||||
const [tool] = await db
|
||||
.select({
|
||||
id: workflowMcpTool.id,
|
||||
toolName: workflowMcpTool.toolName,
|
||||
workflowId: workflowMcpTool.workflowId,
|
||||
isEnabled: workflowMcpTool.isEnabled,
|
||||
})
|
||||
.from(workflowMcpTool)
|
||||
.where(eq(workflowMcpTool.serverId, serverId))
|
||||
.then((tools) => tools.filter((t) => t.toolName === params.name))
|
||||
|
||||
if (!tool) {
|
||||
return NextResponse.json(
|
||||
createJsonRpcError(rpcRequest.id, -32602, `Tool not found: ${params.name}`),
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
if (!tool.isEnabled) {
|
||||
return NextResponse.json(
|
||||
createJsonRpcError(rpcRequest.id, -32602, `Tool is disabled: ${params.name}`),
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Verify workflow is still deployed
|
||||
const [workflowRecord] = await db
|
||||
.select({ id: workflow.id, isDeployed: workflow.isDeployed })
|
||||
.from(workflow)
|
||||
.where(eq(workflow.id, tool.workflowId))
|
||||
.limit(1)
|
||||
|
||||
if (!workflowRecord || !workflowRecord.isDeployed) {
|
||||
return NextResponse.json(
|
||||
createJsonRpcError(rpcRequest.id, -32603, 'Workflow is not deployed'),
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Execute the workflow
|
||||
const baseUrl = getBaseUrl()
|
||||
const executeUrl = `${baseUrl}/api/workflows/${tool.workflowId}/execute`
|
||||
|
||||
logger.info(`Executing workflow ${tool.workflowId} via MCP tool ${params.name}`)
|
||||
|
||||
// Build headers for the internal execute call
|
||||
const executeHeaders: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
// Forward the API key for authentication
|
||||
if (apiKey) {
|
||||
executeHeaders['X-API-Key'] = apiKey
|
||||
}
|
||||
|
||||
const executeResponse = await fetch(executeUrl, {
|
||||
method: 'POST',
|
||||
headers: executeHeaders,
|
||||
body: JSON.stringify({
|
||||
input: params.arguments || {},
|
||||
triggerType: 'mcp',
|
||||
}),
|
||||
})
|
||||
|
||||
const executeResult = await executeResponse.json()
|
||||
|
||||
if (!executeResponse.ok) {
|
||||
return NextResponse.json(
|
||||
createJsonRpcError(
|
||||
rpcRequest.id,
|
||||
-32603,
|
||||
executeResult.error || 'Workflow execution failed'
|
||||
),
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
// Format response for MCP
|
||||
const content = [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(executeResult.output || executeResult, null, 2),
|
||||
},
|
||||
]
|
||||
|
||||
return NextResponse.json(
|
||||
createJsonRpcResponse(rpcRequest.id, {
|
||||
content,
|
||||
isError: !executeResult.success,
|
||||
})
|
||||
)
|
||||
} catch (error) {
|
||||
logger.error('Error calling tool:', error)
|
||||
return NextResponse.json(createJsonRpcError(rpcRequest.id, -32603, 'Tool execution failed'), {
|
||||
status: 500,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
/**
|
||||
* MCP SSE/HTTP Endpoint
|
||||
*
|
||||
* Implements MCP protocol using the official @modelcontextprotocol/sdk
|
||||
* with a Next.js-compatible transport adapter.
|
||||
*/
|
||||
|
||||
import { db } from '@sim/db'
|
||||
import { workflowMcpServer } from '@sim/db/schema'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { SSE_HEADERS } from '@/lib/core/utils/sse'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { createMcpSseStream, handleMcpRequest } from '@/lib/mcp/workflow-mcp-server'
|
||||
|
||||
const logger = createLogger('WorkflowMcpSSE')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
export const runtime = 'nodejs'
|
||||
|
||||
interface RouteParams {
|
||||
serverId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that the server exists and is published
|
||||
*/
|
||||
async function validateServer(serverId: string) {
|
||||
const [server] = await db
|
||||
.select({
|
||||
id: workflowMcpServer.id,
|
||||
name: workflowMcpServer.name,
|
||||
workspaceId: workflowMcpServer.workspaceId,
|
||||
isPublished: workflowMcpServer.isPublished,
|
||||
})
|
||||
.from(workflowMcpServer)
|
||||
.where(eq(workflowMcpServer.id, serverId))
|
||||
.limit(1)
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
/**
|
||||
* GET - SSE endpoint for MCP protocol
|
||||
* Establishes a Server-Sent Events connection for MCP notifications
|
||||
*/
|
||||
export async function GET(request: NextRequest, { params }: { params: Promise<RouteParams> }) {
|
||||
const { serverId } = await params
|
||||
|
||||
try {
|
||||
// Validate server exists and is published
|
||||
const server = await validateServer(serverId)
|
||||
|
||||
if (!server) {
|
||||
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
if (!server.isPublished) {
|
||||
return NextResponse.json({ error: 'Server is not published' }, { status: 403 })
|
||||
}
|
||||
|
||||
// Check authentication
|
||||
const auth = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const apiKey =
|
||||
request.headers.get('X-API-Key') ||
|
||||
request.headers.get('Authorization')?.replace('Bearer ', '')
|
||||
|
||||
// Create SSE stream using the SDK-based server
|
||||
const stream = createMcpSseStream({
|
||||
serverId,
|
||||
serverName: server.name,
|
||||
userId: auth.userId,
|
||||
workspaceId: server.workspaceId,
|
||||
apiKey,
|
||||
})
|
||||
|
||||
return new NextResponse(stream, {
|
||||
headers: {
|
||||
...SSE_HEADERS,
|
||||
'X-MCP-Server-Id': serverId,
|
||||
'X-MCP-Server-Name': server.name,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error establishing SSE connection:', error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST - Handle MCP JSON-RPC messages
|
||||
* This is the primary endpoint for MCP protocol messages using the SDK
|
||||
*/
|
||||
export async function POST(request: NextRequest, { params }: { params: Promise<RouteParams> }) {
|
||||
const { serverId } = await params
|
||||
|
||||
try {
|
||||
// Validate server
|
||||
const server = await validateServer(serverId)
|
||||
|
||||
if (!server) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
jsonrpc: '2.0',
|
||||
id: null,
|
||||
error: { code: -32000, message: 'Server not found' },
|
||||
},
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
if (!server.isPublished) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
jsonrpc: '2.0',
|
||||
id: null,
|
||||
error: { code: -32000, message: 'Server is not published' },
|
||||
},
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
|
||||
// Check authentication
|
||||
const auth = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
jsonrpc: '2.0',
|
||||
id: null,
|
||||
error: { code: -32000, message: 'Unauthorized' },
|
||||
},
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const apiKey =
|
||||
request.headers.get('X-API-Key') ||
|
||||
request.headers.get('Authorization')?.replace('Bearer ', '')
|
||||
|
||||
// Handle the request using the SDK-based server
|
||||
return handleMcpRequest(
|
||||
{
|
||||
serverId,
|
||||
serverName: server.name,
|
||||
userId: auth.userId,
|
||||
workspaceId: server.workspaceId,
|
||||
apiKey,
|
||||
},
|
||||
request
|
||||
)
|
||||
} catch (error) {
|
||||
logger.error('Error handling MCP POST request:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
jsonrpc: '2.0',
|
||||
id: null,
|
||||
error: { code: -32603, message: 'Internal error' },
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE - Handle session termination
|
||||
* MCP clients may send DELETE to end a session
|
||||
*/
|
||||
export async function DELETE(request: NextRequest, { params }: { params: Promise<RouteParams> }) {
|
||||
const { serverId } = await params
|
||||
|
||||
try {
|
||||
// Validate server exists
|
||||
const server = await validateServer(serverId)
|
||||
|
||||
if (!server) {
|
||||
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
// Check authentication
|
||||
const auth = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
logger.info(`MCP session terminated for server ${serverId}`)
|
||||
|
||||
return new NextResponse(null, { status: 204 })
|
||||
} catch (error) {
|
||||
logger.error('Error handling MCP DELETE request:', error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import type { NextRequest } from 'next/server'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { withMcpAuth } from '@/lib/mcp/middleware'
|
||||
import { mcpService } from '@/lib/mcp/service'
|
||||
import type { McpServerStatusConfig } from '@/lib/mcp/types'
|
||||
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
|
||||
|
||||
const logger = createLogger('McpServerRefreshAPI')
|
||||
@@ -51,12 +50,6 @@ export const POST = withMcpAuth<{ id: string }>('read')(
|
||||
let toolCount = 0
|
||||
let lastError: string | null = null
|
||||
|
||||
const currentStatusConfig: McpServerStatusConfig =
|
||||
(server.statusConfig as McpServerStatusConfig | null) ?? {
|
||||
consecutiveFailures: 0,
|
||||
lastSuccessfulDiscovery: null,
|
||||
}
|
||||
|
||||
try {
|
||||
const tools = await mcpService.discoverServerTools(userId, serverId, workspaceId)
|
||||
connectionStatus = 'connected'
|
||||
@@ -70,40 +63,20 @@ export const POST = withMcpAuth<{ id: string }>('read')(
|
||||
logger.warn(`[${requestId}] Failed to connect to server ${serverId}:`, error)
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
const newStatusConfig =
|
||||
connectionStatus === 'connected'
|
||||
? { consecutiveFailures: 0, lastSuccessfulDiscovery: now.toISOString() }
|
||||
: {
|
||||
consecutiveFailures: currentStatusConfig.consecutiveFailures + 1,
|
||||
lastSuccessfulDiscovery: currentStatusConfig.lastSuccessfulDiscovery,
|
||||
}
|
||||
|
||||
const [refreshedServer] = await db
|
||||
.update(mcpServers)
|
||||
.set({
|
||||
lastToolsRefresh: now,
|
||||
lastToolsRefresh: new Date(),
|
||||
connectionStatus,
|
||||
lastError,
|
||||
lastConnected: connectionStatus === 'connected' ? now : server.lastConnected,
|
||||
lastConnected: connectionStatus === 'connected' ? new Date() : server.lastConnected,
|
||||
toolCount,
|
||||
statusConfig: newStatusConfig,
|
||||
updatedAt: now,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(mcpServers.id, serverId))
|
||||
.returning()
|
||||
|
||||
if (connectionStatus === 'connected') {
|
||||
logger.info(
|
||||
`[${requestId}] Successfully refreshed MCP server: ${serverId} (${toolCount} tools)`
|
||||
)
|
||||
await mcpService.clearCache(workspaceId)
|
||||
} else {
|
||||
logger.warn(
|
||||
`[${requestId}] Refresh completed for MCP server ${serverId} but connection failed: ${lastError}`
|
||||
)
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Successfully refreshed MCP server: ${serverId}`)
|
||||
return createMcpSuccessResponse({
|
||||
status: connectionStatus,
|
||||
toolCount,
|
||||
|
||||
@@ -48,19 +48,6 @@ export const PATCH = withMcpAuth<{ id: string }>('write')(
|
||||
// Remove workspaceId from body to prevent it from being updated
|
||||
const { workspaceId: _, ...updateData } = body
|
||||
|
||||
// Get the current server to check if URL is changing
|
||||
const [currentServer] = await db
|
||||
.select({ url: mcpServers.url })
|
||||
.from(mcpServers)
|
||||
.where(
|
||||
and(
|
||||
eq(mcpServers.id, serverId),
|
||||
eq(mcpServers.workspaceId, workspaceId),
|
||||
isNull(mcpServers.deletedAt)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
const [updatedServer] = await db
|
||||
.update(mcpServers)
|
||||
.set({
|
||||
@@ -84,12 +71,8 @@ export const PATCH = withMcpAuth<{ id: string }>('write')(
|
||||
)
|
||||
}
|
||||
|
||||
// Only clear cache if URL changed (requires re-discovery)
|
||||
const urlChanged = body.url && currentServer?.url !== body.url
|
||||
if (urlChanged) {
|
||||
await mcpService.clearCache(workspaceId)
|
||||
logger.info(`[${requestId}] Cleared cache due to URL change`)
|
||||
}
|
||||
// Clear MCP service cache after update
|
||||
mcpService.clearCache(workspaceId)
|
||||
|
||||
logger.info(`[${requestId}] Successfully updated MCP server: ${serverId}`)
|
||||
return createMcpSuccessResponse({ server: updatedServer })
|
||||
|
||||
@@ -117,14 +117,12 @@ export const POST = withMcpAuth('write')(
|
||||
timeout: body.timeout || 30000,
|
||||
retries: body.retries || 3,
|
||||
enabled: body.enabled !== false,
|
||||
connectionStatus: 'connected',
|
||||
lastConnected: new Date(),
|
||||
updatedAt: new Date(),
|
||||
deletedAt: null,
|
||||
})
|
||||
.where(eq(mcpServers.id, serverId))
|
||||
|
||||
await mcpService.clearCache(workspaceId)
|
||||
mcpService.clearCache(workspaceId)
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Successfully updated MCP server: ${body.name} (ID: ${serverId})`
|
||||
@@ -147,14 +145,12 @@ export const POST = withMcpAuth('write')(
|
||||
timeout: body.timeout || 30000,
|
||||
retries: body.retries || 3,
|
||||
enabled: body.enabled !== false,
|
||||
connectionStatus: 'connected',
|
||||
lastConnected: new Date(),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.returning()
|
||||
|
||||
await mcpService.clearCache(workspaceId)
|
||||
mcpService.clearCache(workspaceId)
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Successfully registered MCP server: ${body.name} (ID: ${serverId})`
|
||||
@@ -216,7 +212,7 @@ export const DELETE = withMcpAuth('admin')(
|
||||
)
|
||||
}
|
||||
|
||||
await mcpService.clearCache(workspaceId)
|
||||
mcpService.clearCache(workspaceId)
|
||||
|
||||
logger.info(`[${requestId}] Successfully deleted MCP server: ${serverId}`)
|
||||
return createMcpSuccessResponse({ message: `Server ${serverId} deleted successfully` })
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
import { db } from '@sim/db'
|
||||
import { workflow, workflowBlocks } from '@sim/db/schema'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { withMcpAuth } from '@/lib/mcp/middleware'
|
||||
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
|
||||
|
||||
const logger = createLogger('McpStoredToolsAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
interface StoredMcpTool {
|
||||
workflowId: string
|
||||
workflowName: string
|
||||
serverId: string
|
||||
serverUrl?: string
|
||||
toolName: string
|
||||
schema?: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* GET - Get all stored MCP tools from workflows in the workspace
|
||||
*
|
||||
* Scans all workflows in the workspace and extracts MCP tools that have been
|
||||
* added to agent blocks. Returns the stored state of each tool for comparison
|
||||
* against current server state.
|
||||
*/
|
||||
export const GET = withMcpAuth('read')(
|
||||
async (request: NextRequest, { userId, workspaceId, requestId }) => {
|
||||
try {
|
||||
logger.info(`[${requestId}] Fetching stored MCP tools for workspace ${workspaceId}`)
|
||||
|
||||
// Get all workflows in workspace
|
||||
const workflows = await db
|
||||
.select({
|
||||
id: workflow.id,
|
||||
name: workflow.name,
|
||||
})
|
||||
.from(workflow)
|
||||
.where(eq(workflow.workspaceId, workspaceId))
|
||||
|
||||
const workflowMap = new Map(workflows.map((w) => [w.id, w.name]))
|
||||
const workflowIds = workflows.map((w) => w.id)
|
||||
|
||||
if (workflowIds.length === 0) {
|
||||
return createMcpSuccessResponse({ tools: [] })
|
||||
}
|
||||
|
||||
// Get all agent blocks from these workflows
|
||||
const agentBlocks = await db
|
||||
.select({
|
||||
workflowId: workflowBlocks.workflowId,
|
||||
subBlocks: workflowBlocks.subBlocks,
|
||||
})
|
||||
.from(workflowBlocks)
|
||||
.where(eq(workflowBlocks.type, 'agent'))
|
||||
|
||||
const storedTools: StoredMcpTool[] = []
|
||||
|
||||
for (const block of agentBlocks) {
|
||||
if (!workflowMap.has(block.workflowId)) continue
|
||||
|
||||
const subBlocks = block.subBlocks as Record<string, unknown> | null
|
||||
if (!subBlocks) continue
|
||||
|
||||
const toolsSubBlock = subBlocks.tools as Record<string, unknown> | undefined
|
||||
const toolsValue = toolsSubBlock?.value
|
||||
|
||||
if (!toolsValue || !Array.isArray(toolsValue)) continue
|
||||
|
||||
for (const tool of toolsValue) {
|
||||
if (tool.type !== 'mcp') continue
|
||||
|
||||
const params = tool.params as Record<string, unknown> | undefined
|
||||
if (!params?.serverId || !params?.toolName) continue
|
||||
|
||||
storedTools.push({
|
||||
workflowId: block.workflowId,
|
||||
workflowName: workflowMap.get(block.workflowId) || 'Untitled',
|
||||
serverId: params.serverId as string,
|
||||
serverUrl: params.serverUrl as string | undefined,
|
||||
toolName: params.toolName as string,
|
||||
schema: tool.schema as Record<string, unknown> | undefined,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Found ${storedTools.length} stored MCP tools across ${workflows.length} workflows`
|
||||
)
|
||||
|
||||
return createMcpSuccessResponse({ tools: storedTools })
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error fetching stored MCP tools:`, error)
|
||||
return createMcpErrorResponse(
|
||||
error instanceof Error ? error : new Error('Failed to fetch stored MCP tools'),
|
||||
'Failed to fetch stored MCP tools',
|
||||
500
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -1,150 +0,0 @@
|
||||
import { db } from '@sim/db'
|
||||
import { workflowMcpServer, workflowMcpTool } from '@sim/db/schema'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { withMcpAuth } from '@/lib/mcp/middleware'
|
||||
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
|
||||
|
||||
const logger = createLogger('WorkflowMcpServerPublishAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
interface RouteParams {
|
||||
id: string
|
||||
}
|
||||
|
||||
/**
|
||||
* POST - Publish a workflow MCP server (make it accessible via OAuth)
|
||||
*/
|
||||
export const POST = withMcpAuth<RouteParams>('admin')(
|
||||
async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
|
||||
try {
|
||||
const { id: serverId } = await params
|
||||
|
||||
logger.info(`[${requestId}] Publishing workflow MCP server: ${serverId}`)
|
||||
|
||||
const [existingServer] = await db
|
||||
.select({ id: workflowMcpServer.id, isPublished: workflowMcpServer.isPublished })
|
||||
.from(workflowMcpServer)
|
||||
.where(
|
||||
and(eq(workflowMcpServer.id, serverId), eq(workflowMcpServer.workspaceId, workspaceId))
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (!existingServer) {
|
||||
return createMcpErrorResponse(new Error('Server not found'), 'Server not found', 404)
|
||||
}
|
||||
|
||||
if (existingServer.isPublished) {
|
||||
return createMcpErrorResponse(
|
||||
new Error('Server is already published'),
|
||||
'Server is already published',
|
||||
400
|
||||
)
|
||||
}
|
||||
|
||||
// Check if server has at least one tool
|
||||
const tools = await db
|
||||
.select({ id: workflowMcpTool.id })
|
||||
.from(workflowMcpTool)
|
||||
.where(eq(workflowMcpTool.serverId, serverId))
|
||||
.limit(1)
|
||||
|
||||
if (tools.length === 0) {
|
||||
return createMcpErrorResponse(
|
||||
new Error(
|
||||
'Cannot publish server without any tools. Add at least one workflow as a tool first.'
|
||||
),
|
||||
'Server has no tools',
|
||||
400
|
||||
)
|
||||
}
|
||||
|
||||
const [updatedServer] = await db
|
||||
.update(workflowMcpServer)
|
||||
.set({
|
||||
isPublished: true,
|
||||
publishedAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(workflowMcpServer.id, serverId))
|
||||
.returning()
|
||||
|
||||
const baseUrl = getBaseUrl()
|
||||
const mcpServerUrl = `${baseUrl}/api/mcp/serve/${serverId}/sse`
|
||||
|
||||
logger.info(`[${requestId}] Successfully published workflow MCP server: ${serverId}`)
|
||||
|
||||
return createMcpSuccessResponse({
|
||||
server: updatedServer,
|
||||
mcpServerUrl,
|
||||
message: 'Server published successfully. External MCP clients can now connect using OAuth.',
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error publishing workflow MCP server:`, error)
|
||||
return createMcpErrorResponse(
|
||||
error instanceof Error ? error : new Error('Failed to publish workflow MCP server'),
|
||||
'Failed to publish workflow MCP server',
|
||||
500
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* DELETE - Unpublish a workflow MCP server
|
||||
*/
|
||||
export const DELETE = withMcpAuth<RouteParams>('admin')(
|
||||
async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
|
||||
try {
|
||||
const { id: serverId } = await params
|
||||
|
||||
logger.info(`[${requestId}] Unpublishing workflow MCP server: ${serverId}`)
|
||||
|
||||
const [existingServer] = await db
|
||||
.select({ id: workflowMcpServer.id, isPublished: workflowMcpServer.isPublished })
|
||||
.from(workflowMcpServer)
|
||||
.where(
|
||||
and(eq(workflowMcpServer.id, serverId), eq(workflowMcpServer.workspaceId, workspaceId))
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (!existingServer) {
|
||||
return createMcpErrorResponse(new Error('Server not found'), 'Server not found', 404)
|
||||
}
|
||||
|
||||
if (!existingServer.isPublished) {
|
||||
return createMcpErrorResponse(
|
||||
new Error('Server is not published'),
|
||||
'Server is not published',
|
||||
400
|
||||
)
|
||||
}
|
||||
|
||||
const [updatedServer] = await db
|
||||
.update(workflowMcpServer)
|
||||
.set({
|
||||
isPublished: false,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(workflowMcpServer.id, serverId))
|
||||
.returning()
|
||||
|
||||
logger.info(`[${requestId}] Successfully unpublished workflow MCP server: ${serverId}`)
|
||||
|
||||
return createMcpSuccessResponse({
|
||||
server: updatedServer,
|
||||
message: 'Server unpublished successfully. External MCP clients can no longer connect.',
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error unpublishing workflow MCP server:`, error)
|
||||
return createMcpErrorResponse(
|
||||
error instanceof Error ? error : new Error('Failed to unpublish workflow MCP server'),
|
||||
'Failed to unpublish workflow MCP server',
|
||||
500
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -1,157 +0,0 @@
|
||||
import { db } from '@sim/db'
|
||||
import { workflowMcpServer, workflowMcpTool } from '@sim/db/schema'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
|
||||
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
|
||||
|
||||
const logger = createLogger('WorkflowMcpServerAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
interface RouteParams {
|
||||
id: string
|
||||
}
|
||||
|
||||
/**
|
||||
* GET - Get a specific workflow MCP server with its tools
|
||||
*/
|
||||
export const GET = withMcpAuth<RouteParams>('read')(
|
||||
async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
|
||||
try {
|
||||
const { id: serverId } = await params
|
||||
|
||||
logger.info(`[${requestId}] Getting workflow MCP server: ${serverId}`)
|
||||
|
||||
const [server] = await db
|
||||
.select({
|
||||
id: workflowMcpServer.id,
|
||||
workspaceId: workflowMcpServer.workspaceId,
|
||||
createdBy: workflowMcpServer.createdBy,
|
||||
name: workflowMcpServer.name,
|
||||
description: workflowMcpServer.description,
|
||||
isPublished: workflowMcpServer.isPublished,
|
||||
publishedAt: workflowMcpServer.publishedAt,
|
||||
createdAt: workflowMcpServer.createdAt,
|
||||
updatedAt: workflowMcpServer.updatedAt,
|
||||
})
|
||||
.from(workflowMcpServer)
|
||||
.where(
|
||||
and(eq(workflowMcpServer.id, serverId), eq(workflowMcpServer.workspaceId, workspaceId))
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (!server) {
|
||||
return createMcpErrorResponse(new Error('Server not found'), 'Server not found', 404)
|
||||
}
|
||||
|
||||
const tools = await db
|
||||
.select()
|
||||
.from(workflowMcpTool)
|
||||
.where(eq(workflowMcpTool.serverId, serverId))
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Found workflow MCP server: ${server.name} with ${tools.length} tools`
|
||||
)
|
||||
|
||||
return createMcpSuccessResponse({ server, tools })
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error getting workflow MCP server:`, error)
|
||||
return createMcpErrorResponse(
|
||||
error instanceof Error ? error : new Error('Failed to get workflow MCP server'),
|
||||
'Failed to get workflow MCP server',
|
||||
500
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* PATCH - Update a workflow MCP server
|
||||
*/
|
||||
export const PATCH = withMcpAuth<RouteParams>('write')(
|
||||
async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
|
||||
try {
|
||||
const { id: serverId } = await params
|
||||
const body = getParsedBody(request) || (await request.json())
|
||||
|
||||
logger.info(`[${requestId}] Updating workflow MCP server: ${serverId}`)
|
||||
|
||||
const [existingServer] = await db
|
||||
.select({ id: workflowMcpServer.id })
|
||||
.from(workflowMcpServer)
|
||||
.where(
|
||||
and(eq(workflowMcpServer.id, serverId), eq(workflowMcpServer.workspaceId, workspaceId))
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (!existingServer) {
|
||||
return createMcpErrorResponse(new Error('Server not found'), 'Server not found', 404)
|
||||
}
|
||||
|
||||
const updateData: Record<string, unknown> = {
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
|
||||
if (body.name !== undefined) {
|
||||
updateData.name = body.name.trim()
|
||||
}
|
||||
if (body.description !== undefined) {
|
||||
updateData.description = body.description?.trim() || null
|
||||
}
|
||||
|
||||
const [updatedServer] = await db
|
||||
.update(workflowMcpServer)
|
||||
.set(updateData)
|
||||
.where(eq(workflowMcpServer.id, serverId))
|
||||
.returning()
|
||||
|
||||
logger.info(`[${requestId}] Successfully updated workflow MCP server: ${serverId}`)
|
||||
|
||||
return createMcpSuccessResponse({ server: updatedServer })
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error updating workflow MCP server:`, error)
|
||||
return createMcpErrorResponse(
|
||||
error instanceof Error ? error : new Error('Failed to update workflow MCP server'),
|
||||
'Failed to update workflow MCP server',
|
||||
500
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* DELETE - Delete a workflow MCP server and all its tools
|
||||
*/
|
||||
export const DELETE = withMcpAuth<RouteParams>('admin')(
|
||||
async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
|
||||
try {
|
||||
const { id: serverId } = await params
|
||||
|
||||
logger.info(`[${requestId}] Deleting workflow MCP server: ${serverId}`)
|
||||
|
||||
const [deletedServer] = await db
|
||||
.delete(workflowMcpServer)
|
||||
.where(
|
||||
and(eq(workflowMcpServer.id, serverId), eq(workflowMcpServer.workspaceId, workspaceId))
|
||||
)
|
||||
.returning()
|
||||
|
||||
if (!deletedServer) {
|
||||
return createMcpErrorResponse(new Error('Server not found'), 'Server not found', 404)
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Successfully deleted workflow MCP server: ${serverId}`)
|
||||
|
||||
return createMcpSuccessResponse({ message: `Server ${serverId} deleted successfully` })
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error deleting workflow MCP server:`, error)
|
||||
return createMcpErrorResponse(
|
||||
error instanceof Error ? error : new Error('Failed to delete workflow MCP server'),
|
||||
'Failed to delete workflow MCP server',
|
||||
500
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -1,178 +0,0 @@
|
||||
import { db } from '@sim/db'
|
||||
import { workflowMcpServer, workflowMcpTool } from '@sim/db/schema'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
|
||||
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
|
||||
|
||||
const logger = createLogger('WorkflowMcpToolAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
interface RouteParams {
|
||||
id: string
|
||||
toolId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* GET - Get a specific tool
|
||||
*/
|
||||
export const GET = withMcpAuth<RouteParams>('read')(
|
||||
async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
|
||||
try {
|
||||
const { id: serverId, toolId } = await params
|
||||
|
||||
logger.info(`[${requestId}] Getting tool ${toolId} from server ${serverId}`)
|
||||
|
||||
// Verify server exists and belongs to workspace
|
||||
const [server] = await db
|
||||
.select({ id: workflowMcpServer.id })
|
||||
.from(workflowMcpServer)
|
||||
.where(
|
||||
and(eq(workflowMcpServer.id, serverId), eq(workflowMcpServer.workspaceId, workspaceId))
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (!server) {
|
||||
return createMcpErrorResponse(new Error('Server not found'), 'Server not found', 404)
|
||||
}
|
||||
|
||||
const [tool] = await db
|
||||
.select()
|
||||
.from(workflowMcpTool)
|
||||
.where(and(eq(workflowMcpTool.id, toolId), eq(workflowMcpTool.serverId, serverId)))
|
||||
.limit(1)
|
||||
|
||||
if (!tool) {
|
||||
return createMcpErrorResponse(new Error('Tool not found'), 'Tool not found', 404)
|
||||
}
|
||||
|
||||
return createMcpSuccessResponse({ tool })
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error getting tool:`, error)
|
||||
return createMcpErrorResponse(
|
||||
error instanceof Error ? error : new Error('Failed to get tool'),
|
||||
'Failed to get tool',
|
||||
500
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* PATCH - Update a tool's configuration
|
||||
*/
|
||||
export const PATCH = withMcpAuth<RouteParams>('write')(
|
||||
async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
|
||||
try {
|
||||
const { id: serverId, toolId } = await params
|
||||
const body = getParsedBody(request) || (await request.json())
|
||||
|
||||
logger.info(`[${requestId}] Updating tool ${toolId} in server ${serverId}`)
|
||||
|
||||
// Verify server exists and belongs to workspace
|
||||
const [server] = await db
|
||||
.select({ id: workflowMcpServer.id })
|
||||
.from(workflowMcpServer)
|
||||
.where(
|
||||
and(eq(workflowMcpServer.id, serverId), eq(workflowMcpServer.workspaceId, workspaceId))
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (!server) {
|
||||
return createMcpErrorResponse(new Error('Server not found'), 'Server not found', 404)
|
||||
}
|
||||
|
||||
const [existingTool] = await db
|
||||
.select({ id: workflowMcpTool.id })
|
||||
.from(workflowMcpTool)
|
||||
.where(and(eq(workflowMcpTool.id, toolId), eq(workflowMcpTool.serverId, serverId)))
|
||||
.limit(1)
|
||||
|
||||
if (!existingTool) {
|
||||
return createMcpErrorResponse(new Error('Tool not found'), 'Tool not found', 404)
|
||||
}
|
||||
|
||||
const updateData: Record<string, unknown> = {
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
|
||||
if (body.toolName !== undefined) {
|
||||
updateData.toolName = body.toolName.trim()
|
||||
}
|
||||
if (body.toolDescription !== undefined) {
|
||||
updateData.toolDescription = body.toolDescription?.trim() || null
|
||||
}
|
||||
if (body.parameterSchema !== undefined) {
|
||||
updateData.parameterSchema = body.parameterSchema
|
||||
}
|
||||
if (body.isEnabled !== undefined) {
|
||||
updateData.isEnabled = body.isEnabled
|
||||
}
|
||||
|
||||
const [updatedTool] = await db
|
||||
.update(workflowMcpTool)
|
||||
.set(updateData)
|
||||
.where(eq(workflowMcpTool.id, toolId))
|
||||
.returning()
|
||||
|
||||
logger.info(`[${requestId}] Successfully updated tool ${toolId}`)
|
||||
|
||||
return createMcpSuccessResponse({ tool: updatedTool })
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error updating tool:`, error)
|
||||
return createMcpErrorResponse(
|
||||
error instanceof Error ? error : new Error('Failed to update tool'),
|
||||
'Failed to update tool',
|
||||
500
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* DELETE - Remove a tool from an MCP server
|
||||
*/
|
||||
export const DELETE = withMcpAuth<RouteParams>('write')(
|
||||
async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
|
||||
try {
|
||||
const { id: serverId, toolId } = await params
|
||||
|
||||
logger.info(`[${requestId}] Deleting tool ${toolId} from server ${serverId}`)
|
||||
|
||||
// Verify server exists and belongs to workspace
|
||||
const [server] = await db
|
||||
.select({ id: workflowMcpServer.id })
|
||||
.from(workflowMcpServer)
|
||||
.where(
|
||||
and(eq(workflowMcpServer.id, serverId), eq(workflowMcpServer.workspaceId, workspaceId))
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (!server) {
|
||||
return createMcpErrorResponse(new Error('Server not found'), 'Server not found', 404)
|
||||
}
|
||||
|
||||
const [deletedTool] = await db
|
||||
.delete(workflowMcpTool)
|
||||
.where(and(eq(workflowMcpTool.id, toolId), eq(workflowMcpTool.serverId, serverId)))
|
||||
.returning()
|
||||
|
||||
if (!deletedTool) {
|
||||
return createMcpErrorResponse(new Error('Tool not found'), 'Tool not found', 404)
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Successfully deleted tool ${toolId}`)
|
||||
|
||||
return createMcpSuccessResponse({ message: `Tool ${toolId} deleted successfully` })
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error deleting tool:`, error)
|
||||
return createMcpErrorResponse(
|
||||
error instanceof Error ? error : new Error('Failed to delete tool'),
|
||||
'Failed to delete tool',
|
||||
500
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -1,226 +0,0 @@
|
||||
import { db } from '@sim/db'
|
||||
import { workflow, workflowMcpServer, workflowMcpTool } from '@sim/db/schema'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
|
||||
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
|
||||
import { sanitizeToolName } from '@/lib/mcp/workflow-tool-schema'
|
||||
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
|
||||
import { hasValidStartBlockInState } from '@/lib/workflows/triggers/trigger-utils'
|
||||
|
||||
const logger = createLogger('WorkflowMcpToolsAPI')
|
||||
|
||||
/**
|
||||
* Check if a workflow has a valid start block by loading from database
|
||||
*/
|
||||
async function hasValidStartBlock(workflowId: string): Promise<boolean> {
|
||||
try {
|
||||
const normalizedData = await loadWorkflowFromNormalizedTables(workflowId)
|
||||
return hasValidStartBlockInState(normalizedData)
|
||||
} catch (error) {
|
||||
logger.warn('Error checking for start block:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
interface RouteParams {
|
||||
id: string
|
||||
}
|
||||
|
||||
/**
|
||||
* GET - List all tools for a workflow MCP server
|
||||
*/
|
||||
export const GET = withMcpAuth<RouteParams>('read')(
|
||||
async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
|
||||
try {
|
||||
const { id: serverId } = await params
|
||||
|
||||
logger.info(`[${requestId}] Listing tools for workflow MCP server: ${serverId}`)
|
||||
|
||||
// Verify server exists and belongs to workspace
|
||||
const [server] = await db
|
||||
.select({ id: workflowMcpServer.id })
|
||||
.from(workflowMcpServer)
|
||||
.where(
|
||||
and(eq(workflowMcpServer.id, serverId), eq(workflowMcpServer.workspaceId, workspaceId))
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (!server) {
|
||||
return createMcpErrorResponse(new Error('Server not found'), 'Server not found', 404)
|
||||
}
|
||||
|
||||
// Get tools with workflow details
|
||||
const tools = await db
|
||||
.select({
|
||||
id: workflowMcpTool.id,
|
||||
serverId: workflowMcpTool.serverId,
|
||||
workflowId: workflowMcpTool.workflowId,
|
||||
toolName: workflowMcpTool.toolName,
|
||||
toolDescription: workflowMcpTool.toolDescription,
|
||||
parameterSchema: workflowMcpTool.parameterSchema,
|
||||
isEnabled: workflowMcpTool.isEnabled,
|
||||
createdAt: workflowMcpTool.createdAt,
|
||||
updatedAt: workflowMcpTool.updatedAt,
|
||||
workflowName: workflow.name,
|
||||
workflowDescription: workflow.description,
|
||||
isDeployed: workflow.isDeployed,
|
||||
})
|
||||
.from(workflowMcpTool)
|
||||
.leftJoin(workflow, eq(workflowMcpTool.workflowId, workflow.id))
|
||||
.where(eq(workflowMcpTool.serverId, serverId))
|
||||
|
||||
logger.info(`[${requestId}] Found ${tools.length} tools for server ${serverId}`)
|
||||
|
||||
return createMcpSuccessResponse({ tools })
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error listing tools:`, error)
|
||||
return createMcpErrorResponse(
|
||||
error instanceof Error ? error : new Error('Failed to list tools'),
|
||||
'Failed to list tools',
|
||||
500
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* POST - Add a workflow as a tool to an MCP server
|
||||
*/
|
||||
export const POST = withMcpAuth<RouteParams>('write')(
|
||||
async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
|
||||
try {
|
||||
const { id: serverId } = await params
|
||||
const body = getParsedBody(request) || (await request.json())
|
||||
|
||||
logger.info(`[${requestId}] Adding tool to workflow MCP server: ${serverId}`, {
|
||||
workflowId: body.workflowId,
|
||||
})
|
||||
|
||||
if (!body.workflowId) {
|
||||
return createMcpErrorResponse(
|
||||
new Error('Missing required field: workflowId'),
|
||||
'Missing required field',
|
||||
400
|
||||
)
|
||||
}
|
||||
|
||||
// Verify server exists and belongs to workspace
|
||||
const [server] = await db
|
||||
.select({ id: workflowMcpServer.id })
|
||||
.from(workflowMcpServer)
|
||||
.where(
|
||||
and(eq(workflowMcpServer.id, serverId), eq(workflowMcpServer.workspaceId, workspaceId))
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (!server) {
|
||||
return createMcpErrorResponse(new Error('Server not found'), 'Server not found', 404)
|
||||
}
|
||||
|
||||
// Verify workflow exists and is deployed
|
||||
const [workflowRecord] = await db
|
||||
.select({
|
||||
id: workflow.id,
|
||||
name: workflow.name,
|
||||
description: workflow.description,
|
||||
isDeployed: workflow.isDeployed,
|
||||
workspaceId: workflow.workspaceId,
|
||||
})
|
||||
.from(workflow)
|
||||
.where(eq(workflow.id, body.workflowId))
|
||||
.limit(1)
|
||||
|
||||
if (!workflowRecord) {
|
||||
return createMcpErrorResponse(new Error('Workflow not found'), 'Workflow not found', 404)
|
||||
}
|
||||
|
||||
// Verify workflow belongs to the same workspace
|
||||
if (workflowRecord.workspaceId !== workspaceId) {
|
||||
return createMcpErrorResponse(
|
||||
new Error('Workflow does not belong to this workspace'),
|
||||
'Access denied',
|
||||
403
|
||||
)
|
||||
}
|
||||
|
||||
if (!workflowRecord.isDeployed) {
|
||||
return createMcpErrorResponse(
|
||||
new Error('Workflow must be deployed before adding as a tool'),
|
||||
'Workflow not deployed',
|
||||
400
|
||||
)
|
||||
}
|
||||
|
||||
// Verify workflow has a valid start block
|
||||
const hasStartBlock = await hasValidStartBlock(body.workflowId)
|
||||
if (!hasStartBlock) {
|
||||
return createMcpErrorResponse(
|
||||
new Error('Workflow must have a Start block to be used as an MCP tool'),
|
||||
'No start block found',
|
||||
400
|
||||
)
|
||||
}
|
||||
|
||||
// Check if tool already exists for this workflow
|
||||
const [existingTool] = await db
|
||||
.select({ id: workflowMcpTool.id })
|
||||
.from(workflowMcpTool)
|
||||
.where(
|
||||
and(
|
||||
eq(workflowMcpTool.serverId, serverId),
|
||||
eq(workflowMcpTool.workflowId, body.workflowId)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (existingTool) {
|
||||
return createMcpErrorResponse(
|
||||
new Error('This workflow is already added as a tool to this server'),
|
||||
'Tool already exists',
|
||||
409
|
||||
)
|
||||
}
|
||||
|
||||
// Generate tool name and description
|
||||
const toolName = body.toolName?.trim() || sanitizeToolName(workflowRecord.name)
|
||||
const toolDescription =
|
||||
body.toolDescription?.trim() ||
|
||||
workflowRecord.description ||
|
||||
`Execute ${workflowRecord.name} workflow`
|
||||
|
||||
// Create the tool
|
||||
const toolId = crypto.randomUUID()
|
||||
const [tool] = await db
|
||||
.insert(workflowMcpTool)
|
||||
.values({
|
||||
id: toolId,
|
||||
serverId,
|
||||
workflowId: body.workflowId,
|
||||
toolName,
|
||||
toolDescription,
|
||||
parameterSchema: body.parameterSchema || {},
|
||||
isEnabled: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.returning()
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Successfully added tool ${toolName} (workflow: ${body.workflowId}) to server ${serverId}`
|
||||
)
|
||||
|
||||
return createMcpSuccessResponse({ tool }, 201)
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error adding tool:`, error)
|
||||
return createMcpErrorResponse(
|
||||
error instanceof Error ? error : new Error('Failed to add tool'),
|
||||
'Failed to add tool',
|
||||
500
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -1,107 +0,0 @@
|
||||
import { db } from '@sim/db'
|
||||
import { workflowMcpServer } from '@sim/db/schema'
|
||||
import { eq, sql } from 'drizzle-orm'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
|
||||
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
|
||||
|
||||
const logger = createLogger('WorkflowMcpServersAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
/**
|
||||
* GET - List all workflow MCP servers for the workspace
|
||||
*/
|
||||
export const GET = withMcpAuth('read')(
|
||||
async (request: NextRequest, { userId, workspaceId, requestId }) => {
|
||||
try {
|
||||
logger.info(`[${requestId}] Listing workflow MCP servers for workspace ${workspaceId}`)
|
||||
|
||||
const servers = await db
|
||||
.select({
|
||||
id: workflowMcpServer.id,
|
||||
workspaceId: workflowMcpServer.workspaceId,
|
||||
createdBy: workflowMcpServer.createdBy,
|
||||
name: workflowMcpServer.name,
|
||||
description: workflowMcpServer.description,
|
||||
isPublished: workflowMcpServer.isPublished,
|
||||
publishedAt: workflowMcpServer.publishedAt,
|
||||
createdAt: workflowMcpServer.createdAt,
|
||||
updatedAt: workflowMcpServer.updatedAt,
|
||||
toolCount: sql<number>`(
|
||||
SELECT COUNT(*)::int
|
||||
FROM "workflow_mcp_tool"
|
||||
WHERE "workflow_mcp_tool"."server_id" = "workflow_mcp_server"."id"
|
||||
)`.as('tool_count'),
|
||||
})
|
||||
.from(workflowMcpServer)
|
||||
.where(eq(workflowMcpServer.workspaceId, workspaceId))
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Listed ${servers.length} workflow MCP servers for workspace ${workspaceId}`
|
||||
)
|
||||
return createMcpSuccessResponse({ servers })
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error listing workflow MCP servers:`, error)
|
||||
return createMcpErrorResponse(
|
||||
error instanceof Error ? error : new Error('Failed to list workflow MCP servers'),
|
||||
'Failed to list workflow MCP servers',
|
||||
500
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* POST - Create a new workflow MCP server
|
||||
*/
|
||||
export const POST = withMcpAuth('write')(
|
||||
async (request: NextRequest, { userId, workspaceId, requestId }) => {
|
||||
try {
|
||||
const body = getParsedBody(request) || (await request.json())
|
||||
|
||||
logger.info(`[${requestId}] Creating workflow MCP server:`, {
|
||||
name: body.name,
|
||||
workspaceId,
|
||||
})
|
||||
|
||||
if (!body.name) {
|
||||
return createMcpErrorResponse(
|
||||
new Error('Missing required field: name'),
|
||||
'Missing required field',
|
||||
400
|
||||
)
|
||||
}
|
||||
|
||||
const serverId = crypto.randomUUID()
|
||||
|
||||
const [server] = await db
|
||||
.insert(workflowMcpServer)
|
||||
.values({
|
||||
id: serverId,
|
||||
workspaceId,
|
||||
createdBy: userId,
|
||||
name: body.name.trim(),
|
||||
description: body.description?.trim() || null,
|
||||
isPublished: false,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.returning()
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Successfully created workflow MCP server: ${body.name} (ID: ${serverId})`
|
||||
)
|
||||
|
||||
return createMcpSuccessResponse({ server }, 201)
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error creating workflow MCP server:`, error)
|
||||
return createMcpErrorResponse(
|
||||
error instanceof Error ? error : new Error('Failed to create workflow MCP server'),
|
||||
'Failed to create workflow MCP server',
|
||||
500
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -16,6 +16,7 @@ export async function GET() {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
// Get organizations where user is owner or admin
|
||||
const userOrganizations = await db
|
||||
.select({
|
||||
id: organization.id,
|
||||
@@ -31,15 +32,8 @@ export async function GET() {
|
||||
)
|
||||
)
|
||||
|
||||
const anyMembership = await db
|
||||
.select({ id: member.id })
|
||||
.from(member)
|
||||
.where(eq(member.userId, session.user.id))
|
||||
.limit(1)
|
||||
|
||||
return NextResponse.json({
|
||||
organizations: userOrganizations,
|
||||
isMemberOfAnyOrg: anyMembership.length > 0,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch organizations', {
|
||||
|
||||
@@ -45,6 +45,7 @@ export async function GET(request: NextRequest) {
|
||||
host: OLLAMA_HOST,
|
||||
})
|
||||
|
||||
// Return empty array instead of error to avoid breaking the UI
|
||||
return NextResponse.json({ models: [] })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,8 +35,6 @@ export async function POST(request: NextRequest) {
|
||||
apiKey,
|
||||
azureEndpoint,
|
||||
azureApiVersion,
|
||||
vertexProject,
|
||||
vertexLocation,
|
||||
responseFormat,
|
||||
workflowId,
|
||||
workspaceId,
|
||||
@@ -60,8 +58,6 @@ export async function POST(request: NextRequest) {
|
||||
hasApiKey: !!apiKey,
|
||||
hasAzureEndpoint: !!azureEndpoint,
|
||||
hasAzureApiVersion: !!azureApiVersion,
|
||||
hasVertexProject: !!vertexProject,
|
||||
hasVertexLocation: !!vertexLocation,
|
||||
hasResponseFormat: !!responseFormat,
|
||||
workflowId,
|
||||
stream: !!stream,
|
||||
@@ -108,8 +104,6 @@ export async function POST(request: NextRequest) {
|
||||
apiKey: finalApiKey,
|
||||
azureEndpoint,
|
||||
azureApiVersion,
|
||||
vertexProject,
|
||||
vertexLocation,
|
||||
responseFormat,
|
||||
workflowId,
|
||||
workspaceId,
|
||||
|
||||
@@ -20,16 +20,10 @@ export async function GET(request: NextRequest) {
|
||||
baseUrl,
|
||||
})
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
if (env.VLLM_API_KEY) {
|
||||
headers.Authorization = `Bearer ${env.VLLM_API_KEY}`
|
||||
}
|
||||
|
||||
const response = await fetch(`${baseUrl}/v1/models`, {
|
||||
headers,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
next: { revalidate: 60 },
|
||||
})
|
||||
|
||||
@@ -56,6 +50,7 @@ export async function GET(request: NextRequest) {
|
||||
baseUrl,
|
||||
})
|
||||
|
||||
// Return empty array instead of error to avoid breaking the UI
|
||||
return NextResponse.json({ models: [] })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,28 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('SlackAddReactionAPI')
|
||||
|
||||
const SlackAddReactionSchema = z.object({
|
||||
accessToken: z.string().min(1, 'Access token is required'),
|
||||
channel: z.string().min(1, 'Channel is required'),
|
||||
channel: z.string().min(1, 'Channel ID is required'),
|
||||
timestamp: z.string().min(1, 'Message timestamp is required'),
|
||||
name: z.string().min(1, 'Emoji name is required'),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
logger.warn(`[${requestId}] Unauthorized Slack add reaction attempt: ${authResult.error}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
@@ -25,9 +32,22 @@ export async function POST(request: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Authenticated Slack add reaction request via ${authResult.authType}`,
|
||||
{
|
||||
userId: authResult.userId,
|
||||
}
|
||||
)
|
||||
|
||||
const body = await request.json()
|
||||
const validatedData = SlackAddReactionSchema.parse(body)
|
||||
|
||||
logger.info(`[${requestId}] Adding Slack reaction`, {
|
||||
channel: validatedData.channel,
|
||||
timestamp: validatedData.timestamp,
|
||||
emoji: validatedData.name,
|
||||
})
|
||||
|
||||
const slackResponse = await fetch('https://slack.com/api/reactions.add', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -44,6 +64,7 @@ export async function POST(request: NextRequest) {
|
||||
const data = await slackResponse.json()
|
||||
|
||||
if (!data.ok) {
|
||||
logger.error(`[${requestId}] Slack API error:`, data)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
@@ -53,6 +74,12 @@ export async function POST(request: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Reaction added successfully`, {
|
||||
channel: validatedData.channel,
|
||||
timestamp: validatedData.timestamp,
|
||||
reaction: validatedData.name,
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
@@ -66,6 +93,7 @@ export async function POST(request: NextRequest) {
|
||||
})
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
@@ -76,6 +104,7 @@ export async function POST(request: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
logger.error(`[${requestId}] Error adding Slack reaction:`, error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
|
||||
@@ -1,20 +1,27 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('SlackDeleteMessageAPI')
|
||||
|
||||
const SlackDeleteMessageSchema = z.object({
|
||||
accessToken: z.string().min(1, 'Access token is required'),
|
||||
channel: z.string().min(1, 'Channel is required'),
|
||||
channel: z.string().min(1, 'Channel ID is required'),
|
||||
timestamp: z.string().min(1, 'Message timestamp is required'),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
logger.warn(`[${requestId}] Unauthorized Slack delete message attempt: ${authResult.error}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
@@ -24,9 +31,21 @@ export async function POST(request: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Authenticated Slack delete message request via ${authResult.authType}`,
|
||||
{
|
||||
userId: authResult.userId,
|
||||
}
|
||||
)
|
||||
|
||||
const body = await request.json()
|
||||
const validatedData = SlackDeleteMessageSchema.parse(body)
|
||||
|
||||
logger.info(`[${requestId}] Deleting Slack message`, {
|
||||
channel: validatedData.channel,
|
||||
timestamp: validatedData.timestamp,
|
||||
})
|
||||
|
||||
const slackResponse = await fetch('https://slack.com/api/chat.delete', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -42,6 +61,7 @@ export async function POST(request: NextRequest) {
|
||||
const data = await slackResponse.json()
|
||||
|
||||
if (!data.ok) {
|
||||
logger.error(`[${requestId}] Slack API error:`, data)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
@@ -51,6 +71,11 @@ export async function POST(request: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Message deleted successfully`, {
|
||||
channel: data.channel,
|
||||
timestamp: data.ts,
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
@@ -63,6 +88,7 @@ export async function POST(request: NextRequest) {
|
||||
})
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
@@ -73,6 +99,7 @@ export async function POST(request: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
logger.error(`[${requestId}] Error deleting Slack message:`, error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
|
||||
@@ -1,207 +0,0 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { openDMChannel } from '../utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('SlackReadMessagesAPI')
|
||||
|
||||
const SlackReadMessagesSchema = z
|
||||
.object({
|
||||
accessToken: z.string().min(1, 'Access token is required'),
|
||||
channel: z.string().optional().nullable(),
|
||||
userId: z.string().optional().nullable(),
|
||||
limit: z.number().optional().nullable(),
|
||||
oldest: z.string().optional().nullable(),
|
||||
latest: z.string().optional().nullable(),
|
||||
})
|
||||
.refine((data) => data.channel || data.userId, {
|
||||
message: 'Either channel or userId is required',
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
logger.warn(`[${requestId}] Unauthorized Slack read messages attempt: ${authResult.error}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: authResult.error || 'Authentication required',
|
||||
},
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Authenticated Slack read messages request via ${authResult.authType}`,
|
||||
{
|
||||
userId: authResult.userId,
|
||||
}
|
||||
)
|
||||
|
||||
const body = await request.json()
|
||||
const validatedData = SlackReadMessagesSchema.parse(body)
|
||||
|
||||
let channel = validatedData.channel
|
||||
if (!channel && validatedData.userId) {
|
||||
logger.info(`[${requestId}] Opening DM channel for user: ${validatedData.userId}`)
|
||||
channel = await openDMChannel(
|
||||
validatedData.accessToken,
|
||||
validatedData.userId,
|
||||
requestId,
|
||||
logger
|
||||
)
|
||||
}
|
||||
|
||||
const url = new URL('https://slack.com/api/conversations.history')
|
||||
url.searchParams.append('channel', channel!)
|
||||
const limit = validatedData.limit ? Number(validatedData.limit) : 10
|
||||
url.searchParams.append('limit', String(Math.min(limit, 15)))
|
||||
|
||||
if (validatedData.oldest) {
|
||||
url.searchParams.append('oldest', validatedData.oldest)
|
||||
}
|
||||
if (validatedData.latest) {
|
||||
url.searchParams.append('latest', validatedData.latest)
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Reading Slack messages`, {
|
||||
channel,
|
||||
limit,
|
||||
})
|
||||
|
||||
const slackResponse = await fetch(url.toString(), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${validatedData.accessToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
const data = await slackResponse.json()
|
||||
|
||||
if (!data.ok) {
|
||||
logger.error(`[${requestId}] Slack API error:`, data)
|
||||
|
||||
if (data.error === 'not_in_channel') {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error:
|
||||
'Bot is not in the channel. Please invite the Sim bot to your Slack channel by typing: /invite @Sim Studio',
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
if (data.error === 'channel_not_found') {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Channel not found. Please check the channel ID and try again.',
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
if (data.error === 'missing_scope') {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error:
|
||||
'Missing required permissions. Please reconnect your Slack account with the necessary scopes (channels:history, groups:history, im:history).',
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: data.error || 'Failed to fetch messages',
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const messages = (data.messages || []).map((message: any) => ({
|
||||
type: message.type || 'message',
|
||||
ts: message.ts,
|
||||
text: message.text || '',
|
||||
user: message.user,
|
||||
bot_id: message.bot_id,
|
||||
username: message.username,
|
||||
channel: message.channel,
|
||||
team: message.team,
|
||||
thread_ts: message.thread_ts,
|
||||
parent_user_id: message.parent_user_id,
|
||||
reply_count: message.reply_count,
|
||||
reply_users_count: message.reply_users_count,
|
||||
latest_reply: message.latest_reply,
|
||||
subscribed: message.subscribed,
|
||||
last_read: message.last_read,
|
||||
unread_count: message.unread_count,
|
||||
subtype: message.subtype,
|
||||
reactions: message.reactions?.map((reaction: any) => ({
|
||||
name: reaction.name,
|
||||
count: reaction.count,
|
||||
users: reaction.users || [],
|
||||
})),
|
||||
is_starred: message.is_starred,
|
||||
pinned_to: message.pinned_to,
|
||||
files: message.files?.map((file: any) => ({
|
||||
id: file.id,
|
||||
name: file.name,
|
||||
mimetype: file.mimetype,
|
||||
size: file.size,
|
||||
url_private: file.url_private,
|
||||
permalink: file.permalink,
|
||||
mode: file.mode,
|
||||
})),
|
||||
attachments: message.attachments,
|
||||
blocks: message.blocks,
|
||||
edited: message.edited
|
||||
? {
|
||||
user: message.edited.user,
|
||||
ts: message.edited.ts,
|
||||
}
|
||||
: undefined,
|
||||
permalink: message.permalink,
|
||||
}))
|
||||
|
||||
logger.info(`[${requestId}] Successfully read ${messages.length} messages`)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
messages,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors })
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Invalid request data',
|
||||
details: error.errors,
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
logger.error(`[${requestId}] Error reading Slack messages:`, error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -3,24 +3,20 @@ import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { sendSlackMessage } from '../utils'
|
||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('SlackSendMessageAPI')
|
||||
|
||||
const SlackSendMessageSchema = z
|
||||
.object({
|
||||
accessToken: z.string().min(1, 'Access token is required'),
|
||||
channel: z.string().optional().nullable(),
|
||||
userId: z.string().optional().nullable(),
|
||||
text: z.string().min(1, 'Message text is required'),
|
||||
thread_ts: z.string().optional().nullable(),
|
||||
files: z.array(z.any()).optional().nullable(),
|
||||
})
|
||||
.refine((data) => data.channel || data.userId, {
|
||||
message: 'Either channel or userId is required',
|
||||
})
|
||||
const SlackSendMessageSchema = z.object({
|
||||
accessToken: z.string().min(1, 'Access token is required'),
|
||||
channel: z.string().min(1, 'Channel is required'),
|
||||
text: z.string().min(1, 'Message text is required'),
|
||||
thread_ts: z.string().optional().nullable(),
|
||||
files: z.array(z.any()).optional().nullable(),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
@@ -46,33 +42,222 @@ export async function POST(request: NextRequest) {
|
||||
const body = await request.json()
|
||||
const validatedData = SlackSendMessageSchema.parse(body)
|
||||
|
||||
const isDM = !!validatedData.userId
|
||||
logger.info(`[${requestId}] Sending Slack message`, {
|
||||
channel: validatedData.channel,
|
||||
userId: validatedData.userId,
|
||||
isDM,
|
||||
hasFiles: !!(validatedData.files && validatedData.files.length > 0),
|
||||
fileCount: validatedData.files?.length || 0,
|
||||
})
|
||||
|
||||
const result = await sendSlackMessage(
|
||||
{
|
||||
accessToken: validatedData.accessToken,
|
||||
channel: validatedData.channel ?? undefined,
|
||||
userId: validatedData.userId ?? undefined,
|
||||
text: validatedData.text,
|
||||
threadTs: validatedData.thread_ts ?? undefined,
|
||||
files: validatedData.files ?? undefined,
|
||||
},
|
||||
requestId,
|
||||
logger
|
||||
)
|
||||
if (!validatedData.files || validatedData.files.length === 0) {
|
||||
logger.info(`[${requestId}] No files, using chat.postMessage`)
|
||||
|
||||
if (!result.success) {
|
||||
return NextResponse.json({ success: false, error: result.error }, { status: 400 })
|
||||
const response = await fetch('https://slack.com/api/chat.postMessage', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${validatedData.accessToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
channel: validatedData.channel,
|
||||
text: validatedData.text,
|
||||
...(validatedData.thread_ts && { thread_ts: validatedData.thread_ts }),
|
||||
}),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.ok) {
|
||||
logger.error(`[${requestId}] Slack API error:`, data.error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: data.error || 'Failed to send message',
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Message sent successfully`)
|
||||
const messageObj = data.message || {
|
||||
type: 'message',
|
||||
ts: data.ts,
|
||||
text: validatedData.text,
|
||||
channel: data.channel,
|
||||
}
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
message: messageObj,
|
||||
ts: data.ts,
|
||||
channel: data.channel,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, output: result.output })
|
||||
logger.info(`[${requestId}] Processing ${validatedData.files.length} file(s)`)
|
||||
|
||||
const userFiles = processFilesToUserFiles(validatedData.files, requestId, logger)
|
||||
|
||||
if (userFiles.length === 0) {
|
||||
logger.warn(`[${requestId}] No valid files to upload`)
|
||||
const response = await fetch('https://slack.com/api/chat.postMessage', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${validatedData.accessToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
channel: validatedData.channel,
|
||||
text: validatedData.text,
|
||||
...(validatedData.thread_ts && { thread_ts: validatedData.thread_ts }),
|
||||
}),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
const messageObj = data.message || {
|
||||
type: 'message',
|
||||
ts: data.ts,
|
||||
text: validatedData.text,
|
||||
channel: data.channel,
|
||||
}
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
message: messageObj,
|
||||
ts: data.ts,
|
||||
channel: data.channel,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const uploadedFileIds: string[] = []
|
||||
|
||||
for (const userFile of userFiles) {
|
||||
logger.info(`[${requestId}] Uploading file: ${userFile.name}`)
|
||||
|
||||
const buffer = await downloadFileFromStorage(userFile, requestId, logger)
|
||||
|
||||
const getUrlResponse = await fetch('https://slack.com/api/files.getUploadURLExternal', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: `Bearer ${validatedData.accessToken}`,
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
filename: userFile.name,
|
||||
length: buffer.length.toString(),
|
||||
}),
|
||||
})
|
||||
|
||||
const urlData = await getUrlResponse.json()
|
||||
|
||||
if (!urlData.ok) {
|
||||
logger.error(`[${requestId}] Failed to get upload URL:`, urlData.error)
|
||||
continue
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Got upload URL for ${userFile.name}, file_id: ${urlData.file_id}`)
|
||||
|
||||
const uploadResponse = await fetch(urlData.upload_url, {
|
||||
method: 'POST',
|
||||
body: new Uint8Array(buffer),
|
||||
})
|
||||
|
||||
if (!uploadResponse.ok) {
|
||||
logger.error(`[${requestId}] Failed to upload file data: ${uploadResponse.status}`)
|
||||
continue
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] File data uploaded successfully`)
|
||||
uploadedFileIds.push(urlData.file_id)
|
||||
}
|
||||
|
||||
if (uploadedFileIds.length === 0) {
|
||||
logger.warn(`[${requestId}] No files uploaded successfully, sending text-only message`)
|
||||
const response = await fetch('https://slack.com/api/chat.postMessage', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${validatedData.accessToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
channel: validatedData.channel,
|
||||
text: validatedData.text,
|
||||
...(validatedData.thread_ts && { thread_ts: validatedData.thread_ts }),
|
||||
}),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
const messageObj = data.message || {
|
||||
type: 'message',
|
||||
ts: data.ts,
|
||||
text: validatedData.text,
|
||||
channel: data.channel,
|
||||
}
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
message: messageObj,
|
||||
ts: data.ts,
|
||||
channel: data.channel,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const completeResponse = await fetch('https://slack.com/api/files.completeUploadExternal', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${validatedData.accessToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
files: uploadedFileIds.map((id) => ({ id })),
|
||||
channel_id: validatedData.channel,
|
||||
initial_comment: validatedData.text,
|
||||
}),
|
||||
})
|
||||
|
||||
const completeData = await completeResponse.json()
|
||||
|
||||
if (!completeData.ok) {
|
||||
logger.error(`[${requestId}] Failed to complete upload:`, completeData.error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: completeData.error || 'Failed to complete file upload',
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Files uploaded and shared successfully`)
|
||||
|
||||
// For file uploads, construct a message object
|
||||
const fileTs = completeData.files?.[0]?.created?.toString() || (Date.now() / 1000).toString()
|
||||
const fileMessage = {
|
||||
type: 'message',
|
||||
ts: fileTs,
|
||||
text: validatedData.text,
|
||||
channel: validatedData.channel,
|
||||
files: completeData.files?.map((file: any) => ({
|
||||
id: file?.id,
|
||||
name: file?.name,
|
||||
mimetype: file?.mimetype,
|
||||
size: file?.size,
|
||||
url_private: file?.url_private,
|
||||
permalink: file?.permalink,
|
||||
})),
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
message: fileMessage,
|
||||
ts: fileTs,
|
||||
channel: validatedData.channel,
|
||||
fileCount: uploadedFileIds.length,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error sending Slack message:`, error)
|
||||
return NextResponse.json(
|
||||
|
||||
@@ -10,7 +10,7 @@ const logger = createLogger('SlackUpdateMessageAPI')
|
||||
|
||||
const SlackUpdateMessageSchema = z.object({
|
||||
accessToken: z.string().min(1, 'Access token is required'),
|
||||
channel: z.string().min(1, 'Channel is required'),
|
||||
channel: z.string().min(1, 'Channel ID is required'),
|
||||
timestamp: z.string().min(1, 'Message timestamp is required'),
|
||||
text: z.string().min(1, 'Message text is required'),
|
||||
})
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('SlackUsersAPI')
|
||||
|
||||
interface SlackUser {
|
||||
id: string
|
||||
name: string
|
||||
real_name: string
|
||||
deleted: boolean
|
||||
is_bot: boolean
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const requestId = generateRequestId()
|
||||
const body = await request.json()
|
||||
const { credential, workflowId } = body
|
||||
|
||||
if (!credential) {
|
||||
logger.error('Missing credential in request')
|
||||
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
let accessToken: string
|
||||
const isBotToken = credential.startsWith('xoxb-')
|
||||
|
||||
if (isBotToken) {
|
||||
accessToken = credential
|
||||
logger.info('Using direct bot token for Slack API')
|
||||
} else {
|
||||
const authz = await authorizeCredentialUse(request as any, {
|
||||
credentialId: credential,
|
||||
workflowId,
|
||||
})
|
||||
if (!authz.ok || !authz.credentialOwnerUserId) {
|
||||
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
|
||||
}
|
||||
const resolvedToken = await refreshAccessTokenIfNeeded(
|
||||
credential,
|
||||
authz.credentialOwnerUserId,
|
||||
requestId
|
||||
)
|
||||
if (!resolvedToken) {
|
||||
logger.error('Failed to get access token', {
|
||||
credentialId: credential,
|
||||
userId: authz.credentialOwnerUserId,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Could not retrieve access token',
|
||||
authRequired: true,
|
||||
},
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
accessToken = resolvedToken
|
||||
logger.info('Using OAuth token for Slack API')
|
||||
}
|
||||
|
||||
const data = await fetchSlackUsers(accessToken)
|
||||
|
||||
const users = (data.members || [])
|
||||
.filter((user: SlackUser) => !user.deleted && !user.is_bot)
|
||||
.map((user: SlackUser) => ({
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
real_name: user.real_name || user.name,
|
||||
}))
|
||||
|
||||
logger.info(`Successfully fetched ${users.length} Slack users`, {
|
||||
total: data.members?.length || 0,
|
||||
tokenType: isBotToken ? 'bot_token' : 'oauth',
|
||||
})
|
||||
return NextResponse.json({ users })
|
||||
} catch (error) {
|
||||
logger.error('Error processing Slack users request:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to retrieve Slack users', details: (error as Error).message },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchSlackUsers(accessToken: string) {
|
||||
const url = new URL('https://slack.com/api/users.list')
|
||||
url.searchParams.append('limit', '200')
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Slack API error: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.ok) {
|
||||
throw new Error(data.error || 'Failed to fetch users')
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
@@ -1,288 +0,0 @@
|
||||
import type { Logger } from '@/lib/logs/console/logger'
|
||||
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
|
||||
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
|
||||
|
||||
/**
|
||||
* Sends a message to a Slack channel using chat.postMessage
|
||||
*/
|
||||
export async function postSlackMessage(
|
||||
accessToken: string,
|
||||
channel: string,
|
||||
text: string,
|
||||
threadTs?: string | null
|
||||
): Promise<{ ok: boolean; ts?: string; channel?: string; message?: any; error?: string }> {
|
||||
const response = await fetch('https://slack.com/api/chat.postMessage', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
channel,
|
||||
text,
|
||||
...(threadTs && { thread_ts: threadTs }),
|
||||
}),
|
||||
})
|
||||
|
||||
return response.json()
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a default message object when the API doesn't return one
|
||||
*/
|
||||
export function createDefaultMessageObject(
|
||||
ts: string,
|
||||
text: string,
|
||||
channel: string
|
||||
): Record<string, any> {
|
||||
return {
|
||||
type: 'message',
|
||||
ts,
|
||||
text,
|
||||
channel,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the success response for a sent message
|
||||
*/
|
||||
export function formatMessageSuccessResponse(
|
||||
data: any,
|
||||
text: string
|
||||
): {
|
||||
message: any
|
||||
ts: string
|
||||
channel: string
|
||||
} {
|
||||
const messageObj = data.message || createDefaultMessageObject(data.ts, text, data.channel)
|
||||
return {
|
||||
message: messageObj,
|
||||
ts: data.ts,
|
||||
channel: data.channel,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads files to Slack and returns the uploaded file IDs
|
||||
*/
|
||||
export async function uploadFilesToSlack(
|
||||
files: any[],
|
||||
accessToken: string,
|
||||
requestId: string,
|
||||
logger: Logger
|
||||
): Promise<string[]> {
|
||||
const userFiles = processFilesToUserFiles(files, requestId, logger)
|
||||
const uploadedFileIds: string[] = []
|
||||
|
||||
for (const userFile of userFiles) {
|
||||
logger.info(`[${requestId}] Uploading file: ${userFile.name}`)
|
||||
|
||||
const buffer = await downloadFileFromStorage(userFile, requestId, logger)
|
||||
|
||||
const getUrlResponse = await fetch('https://slack.com/api/files.getUploadURLExternal', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
filename: userFile.name,
|
||||
length: buffer.length.toString(),
|
||||
}),
|
||||
})
|
||||
|
||||
const urlData = await getUrlResponse.json()
|
||||
|
||||
if (!urlData.ok) {
|
||||
logger.error(`[${requestId}] Failed to get upload URL:`, urlData.error)
|
||||
continue
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Got upload URL for ${userFile.name}, file_id: ${urlData.file_id}`)
|
||||
|
||||
const uploadResponse = await fetch(urlData.upload_url, {
|
||||
method: 'POST',
|
||||
body: new Uint8Array(buffer),
|
||||
})
|
||||
|
||||
if (!uploadResponse.ok) {
|
||||
logger.error(`[${requestId}] Failed to upload file data: ${uploadResponse.status}`)
|
||||
continue
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] File data uploaded successfully`)
|
||||
uploadedFileIds.push(urlData.file_id)
|
||||
}
|
||||
|
||||
return uploadedFileIds
|
||||
}
|
||||
|
||||
/**
|
||||
* Completes the file upload process by associating files with a channel
|
||||
*/
|
||||
export async function completeSlackFileUpload(
|
||||
uploadedFileIds: string[],
|
||||
channel: string,
|
||||
text: string,
|
||||
accessToken: string
|
||||
): Promise<{ ok: boolean; files?: any[]; error?: string }> {
|
||||
const response = await fetch('https://slack.com/api/files.completeUploadExternal', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
files: uploadedFileIds.map((id) => ({ id })),
|
||||
channel_id: channel,
|
||||
initial_comment: text,
|
||||
}),
|
||||
})
|
||||
|
||||
return response.json()
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a message object for file uploads
|
||||
*/
|
||||
export function createFileMessageObject(
|
||||
text: string,
|
||||
channel: string,
|
||||
files: any[]
|
||||
): Record<string, any> {
|
||||
const fileTs = files?.[0]?.created?.toString() || (Date.now() / 1000).toString()
|
||||
return {
|
||||
type: 'message',
|
||||
ts: fileTs,
|
||||
text,
|
||||
channel,
|
||||
files: files?.map((file: any) => ({
|
||||
id: file?.id,
|
||||
name: file?.name,
|
||||
mimetype: file?.mimetype,
|
||||
size: file?.size,
|
||||
url_private: file?.url_private,
|
||||
permalink: file?.permalink,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a DM channel with a user and returns the channel ID
|
||||
*/
|
||||
export async function openDMChannel(
|
||||
accessToken: string,
|
||||
userId: string,
|
||||
requestId: string,
|
||||
logger: Logger
|
||||
): Promise<string> {
|
||||
const response = await fetch('https://slack.com/api/conversations.open', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
users: userId,
|
||||
}),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.ok) {
|
||||
logger.error(`[${requestId}] Failed to open DM channel:`, data.error)
|
||||
throw new Error(data.error || 'Failed to open DM channel with user')
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Opened DM channel: ${data.channel.id}`)
|
||||
return data.channel.id
|
||||
}
|
||||
|
||||
export interface SlackMessageParams {
|
||||
accessToken: string
|
||||
channel?: string
|
||||
userId?: string
|
||||
text: string
|
||||
threadTs?: string | null
|
||||
files?: any[] | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a Slack message with optional file attachments
|
||||
* Supports both channel messages and direct messages via userId
|
||||
*/
|
||||
export async function sendSlackMessage(
|
||||
params: SlackMessageParams,
|
||||
requestId: string,
|
||||
logger: Logger
|
||||
): Promise<{
|
||||
success: boolean
|
||||
output?: { message: any; ts: string; channel: string; fileCount?: number }
|
||||
error?: string
|
||||
}> {
|
||||
const { accessToken, text, threadTs, files } = params
|
||||
let { channel } = params
|
||||
|
||||
if (!channel && params.userId) {
|
||||
logger.info(`[${requestId}] Opening DM channel for user: ${params.userId}`)
|
||||
channel = await openDMChannel(accessToken, params.userId, requestId, logger)
|
||||
}
|
||||
|
||||
if (!channel) {
|
||||
return { success: false, error: 'Either channel or userId is required' }
|
||||
}
|
||||
|
||||
// No files - simple message
|
||||
if (!files || files.length === 0) {
|
||||
logger.info(`[${requestId}] No files, using chat.postMessage`)
|
||||
|
||||
const data = await postSlackMessage(accessToken, channel, text, threadTs)
|
||||
|
||||
if (!data.ok) {
|
||||
logger.error(`[${requestId}] Slack API error:`, data.error)
|
||||
return { success: false, error: data.error || 'Failed to send message' }
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Message sent successfully`)
|
||||
return { success: true, output: formatMessageSuccessResponse(data, text) }
|
||||
}
|
||||
|
||||
// Process files
|
||||
logger.info(`[${requestId}] Processing ${files.length} file(s)`)
|
||||
const uploadedFileIds = await uploadFilesToSlack(files, accessToken, requestId, logger)
|
||||
|
||||
// No valid files uploaded - send text-only
|
||||
if (uploadedFileIds.length === 0) {
|
||||
logger.warn(`[${requestId}] No valid files to upload, sending text-only message`)
|
||||
|
||||
const data = await postSlackMessage(accessToken, channel, text, threadTs)
|
||||
|
||||
if (!data.ok) {
|
||||
return { success: false, error: data.error || 'Failed to send message' }
|
||||
}
|
||||
|
||||
return { success: true, output: formatMessageSuccessResponse(data, text) }
|
||||
}
|
||||
|
||||
// Complete file upload
|
||||
const completeData = await completeSlackFileUpload(uploadedFileIds, channel, text, accessToken)
|
||||
|
||||
if (!completeData.ok) {
|
||||
logger.error(`[${requestId}] Failed to complete upload:`, completeData.error)
|
||||
return { success: false, error: completeData.error || 'Failed to complete file upload' }
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Files uploaded and shared successfully`)
|
||||
|
||||
const fileMessage = createFileMessageObject(text, channel, completeData.files || [])
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
message: fileMessage,
|
||||
ts: fileMessage.ts,
|
||||
channel,
|
||||
fileCount: uploadedFileIds.length,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,11 @@ import { userStats, workflow } from '@sim/db/schema'
|
||||
import { eq, sql } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import OpenAI, { AzureOpenAI } from 'openai'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { checkAndBillOverageThreshold } from '@/lib/billing/threshold-billing'
|
||||
import { env } from '@/lib/core/config/env'
|
||||
import { getCostMultiplier, isBillingEnabled } from '@/lib/core/config/feature-flags'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { verifyWorkspaceMembership } from '@/app/api/workflows/utils'
|
||||
import { getModelPricing } from '@/providers/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -137,6 +135,7 @@ async function updateUserStatsForWand(
|
||||
costAdded: costToStore,
|
||||
})
|
||||
|
||||
// Check if user has hit overage threshold and bill incrementally
|
||||
await checkAndBillOverageThreshold(userId)
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Failed to update user stats for wand usage`, error)
|
||||
@@ -147,12 +146,6 @@ export async function POST(req: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
logger.info(`[${requestId}] Received wand generation request`)
|
||||
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
logger.warn(`[${requestId}] Unauthorized wand generation attempt`)
|
||||
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
if (!client) {
|
||||
logger.error(`[${requestId}] AI client not initialized. Missing API key.`)
|
||||
return NextResponse.json(
|
||||
@@ -174,35 +167,6 @@ export async function POST(req: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
if (workflowId) {
|
||||
const [workflowRecord] = await db
|
||||
.select({ workspaceId: workflow.workspaceId, userId: workflow.userId })
|
||||
.from(workflow)
|
||||
.where(eq(workflow.id, workflowId))
|
||||
.limit(1)
|
||||
|
||||
if (!workflowRecord) {
|
||||
logger.warn(`[${requestId}] Workflow not found: ${workflowId}`)
|
||||
return NextResponse.json({ success: false, error: 'Workflow not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
if (workflowRecord.workspaceId) {
|
||||
const permission = await verifyWorkspaceMembership(
|
||||
session.user.id,
|
||||
workflowRecord.workspaceId
|
||||
)
|
||||
if (!permission || (permission !== 'admin' && permission !== 'write')) {
|
||||
logger.warn(
|
||||
`[${requestId}] User ${session.user.id} does not have write access to workspace for workflow ${workflowId}`
|
||||
)
|
||||
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 403 })
|
||||
}
|
||||
} else if (workflowRecord.userId !== session.user.id) {
|
||||
logger.warn(`[${requestId}] User ${session.user.id} does not own workflow ${workflowId}`)
|
||||
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 403 })
|
||||
}
|
||||
}
|
||||
|
||||
const finalSystemPrompt =
|
||||
systemPrompt ||
|
||||
'You are a helpful AI assistant. Generate content exactly as requested by the user.'
|
||||
|
||||
@@ -1,121 +1,17 @@
|
||||
import { db, workflow, workflowDeploymentVersion, workflowMcpTool } from '@sim/db'
|
||||
import { db, workflow, workflowDeploymentVersion } from '@sim/db'
|
||||
import { and, desc, eq } from 'drizzle-orm'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import {
|
||||
extractInputFormatFromBlocks,
|
||||
generateToolInputSchema,
|
||||
} from '@/lib/mcp/workflow-tool-schema'
|
||||
import { deployWorkflow, loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
|
||||
import { hasValidStartBlockInState } from '@/lib/workflows/triggers/trigger-utils'
|
||||
import { deployWorkflow } from '@/lib/workflows/persistence/utils'
|
||||
import { validateWorkflowPermissions } from '@/lib/workflows/utils'
|
||||
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
|
||||
|
||||
const logger = createLogger('WorkflowDeployAPI')
|
||||
|
||||
/**
|
||||
* Check if a workflow has a valid start block by loading from database
|
||||
*/
|
||||
async function hasValidStartBlock(workflowId: string): Promise<boolean> {
|
||||
try {
|
||||
const normalizedData = await loadWorkflowFromNormalizedTables(workflowId)
|
||||
return hasValidStartBlockInState(normalizedData)
|
||||
} catch (error) {
|
||||
logger.warn('Error checking for start block:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
export const runtime = 'nodejs'
|
||||
|
||||
/**
|
||||
* Extract input format from workflow blocks and generate MCP tool parameter schema
|
||||
*/
|
||||
async function generateMcpToolSchema(workflowId: string): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const normalizedData = await loadWorkflowFromNormalizedTables(workflowId)
|
||||
if (!normalizedData?.blocks) {
|
||||
return { type: 'object', properties: {} }
|
||||
}
|
||||
|
||||
const inputFormat = extractInputFormatFromBlocks(normalizedData.blocks)
|
||||
if (!inputFormat || inputFormat.length === 0) {
|
||||
return { type: 'object', properties: {} }
|
||||
}
|
||||
|
||||
return generateToolInputSchema(inputFormat) as unknown as Record<string, unknown>
|
||||
} catch (error) {
|
||||
logger.warn('Error generating MCP tool schema:', error)
|
||||
return { type: 'object', properties: {} }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update all MCP tools that reference this workflow with the latest parameter schema.
|
||||
* If the workflow no longer has a start block, remove all MCP tools.
|
||||
*/
|
||||
async function syncMcpToolsOnDeploy(workflowId: string, requestId: string): Promise<void> {
|
||||
try {
|
||||
// Get all MCP tools that use this workflow
|
||||
const tools = await db
|
||||
.select({ id: workflowMcpTool.id })
|
||||
.from(workflowMcpTool)
|
||||
.where(eq(workflowMcpTool.workflowId, workflowId))
|
||||
|
||||
if (tools.length === 0) {
|
||||
logger.debug(`[${requestId}] No MCP tools to sync for workflow: ${workflowId}`)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if workflow still has a valid start block
|
||||
const hasStart = await hasValidStartBlock(workflowId)
|
||||
if (!hasStart) {
|
||||
// No start block - remove all MCP tools for this workflow
|
||||
await db.delete(workflowMcpTool).where(eq(workflowMcpTool.workflowId, workflowId))
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Removed ${tools.length} MCP tool(s) - workflow no longer has a start block: ${workflowId}`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Generate the latest parameter schema
|
||||
const parameterSchema = await generateMcpToolSchema(workflowId)
|
||||
|
||||
// Update all tools with the new schema
|
||||
await db
|
||||
.update(workflowMcpTool)
|
||||
.set({
|
||||
parameterSchema,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(workflowMcpTool.workflowId, workflowId))
|
||||
|
||||
logger.info(`[${requestId}] Synced ${tools.length} MCP tool(s) for workflow: ${workflowId}`)
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error syncing MCP tools:`, error)
|
||||
// Don't throw - this is a non-critical operation
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all MCP tools that reference this workflow when undeploying
|
||||
*/
|
||||
async function removeMcpToolsOnUndeploy(workflowId: string, requestId: string): Promise<void> {
|
||||
try {
|
||||
const result = await db
|
||||
.delete(workflowMcpTool)
|
||||
.where(eq(workflowMcpTool.workflowId, workflowId))
|
||||
|
||||
logger.info(`[${requestId}] Removed MCP tools for undeployed workflow: ${workflowId}`)
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error removing MCP tools:`, error)
|
||||
// Don't throw - this is a non-critical operation
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const requestId = generateRequestId()
|
||||
const { id } = await params
|
||||
@@ -223,9 +119,6 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
|
||||
logger.info(`[${requestId}] Workflow deployed successfully: ${id}`)
|
||||
|
||||
// Sync MCP tools with the latest parameter schema
|
||||
await syncMcpToolsOnDeploy(id, requestId)
|
||||
|
||||
const responseApiKeyInfo = workflowData!.workspaceId
|
||||
? 'Workspace API keys'
|
||||
: 'Personal API keys'
|
||||
@@ -274,9 +167,6 @@ export async function DELETE(
|
||||
.where(eq(workflow.id, id))
|
||||
})
|
||||
|
||||
// Remove all MCP tools that reference this workflow
|
||||
await removeMcpToolsOnUndeploy(id, requestId)
|
||||
|
||||
logger.info(`[${requestId}] Workflow undeployed successfully: ${id}`)
|
||||
|
||||
// Track workflow undeployment
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import { db, workflow, workflowDeploymentVersion, workflowMcpTool } from '@sim/db'
|
||||
import { db, workflow, workflowDeploymentVersion } from '@sim/db'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import {
|
||||
extractInputFormatFromBlocks,
|
||||
generateToolInputSchema,
|
||||
} from '@/lib/mcp/workflow-tool-schema'
|
||||
import { hasValidStartBlockInState } from '@/lib/workflows/triggers/trigger-utils'
|
||||
import { validateWorkflowPermissions } from '@/lib/workflows/utils'
|
||||
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
|
||||
|
||||
@@ -16,80 +11,6 @@ const logger = createLogger('WorkflowActivateDeploymentAPI')
|
||||
export const dynamic = 'force-dynamic'
|
||||
export const runtime = 'nodejs'
|
||||
|
||||
/**
|
||||
* Extract input format from a deployment version state and generate MCP tool parameter schema
|
||||
*/
|
||||
function generateMcpToolSchemaFromState(state: any): Record<string, unknown> {
|
||||
try {
|
||||
if (!state?.blocks) {
|
||||
return { type: 'object', properties: {} }
|
||||
}
|
||||
|
||||
const inputFormat = extractInputFormatFromBlocks(state.blocks)
|
||||
if (!inputFormat || inputFormat.length === 0) {
|
||||
return { type: 'object', properties: {} }
|
||||
}
|
||||
|
||||
return generateToolInputSchema(inputFormat) as unknown as Record<string, unknown>
|
||||
} catch (error) {
|
||||
logger.warn('Error generating MCP tool schema from state:', error)
|
||||
return { type: 'object', properties: {} }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync MCP tools when activating a deployment version.
|
||||
* If the version has no start block, remove all MCP tools.
|
||||
*/
|
||||
async function syncMcpToolsOnVersionActivate(
|
||||
workflowId: string,
|
||||
versionState: any,
|
||||
requestId: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
// Get all MCP tools that use this workflow
|
||||
const tools = await db
|
||||
.select({ id: workflowMcpTool.id })
|
||||
.from(workflowMcpTool)
|
||||
.where(eq(workflowMcpTool.workflowId, workflowId))
|
||||
|
||||
if (tools.length === 0) {
|
||||
logger.debug(`[${requestId}] No MCP tools to sync for workflow: ${workflowId}`)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the activated version has a valid start block
|
||||
if (!hasValidStartBlockInState(versionState)) {
|
||||
// No start block - remove all MCP tools for this workflow
|
||||
await db.delete(workflowMcpTool).where(eq(workflowMcpTool.workflowId, workflowId))
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Removed ${tools.length} MCP tool(s) - activated version has no start block: ${workflowId}`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Generate the parameter schema from the activated version's state
|
||||
const parameterSchema = generateMcpToolSchemaFromState(versionState)
|
||||
|
||||
// Update all tools with the new schema
|
||||
await db
|
||||
.update(workflowMcpTool)
|
||||
.set({
|
||||
parameterSchema,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(workflowMcpTool.workflowId, workflowId))
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Synced ${tools.length} MCP tool(s) for workflow version activation: ${workflowId}`
|
||||
)
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error syncing MCP tools on version activate:`, error)
|
||||
// Don't throw - this is a non-critical operation
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string; version: string }> }
|
||||
@@ -110,18 +31,6 @@ export async function POST(
|
||||
|
||||
const now = new Date()
|
||||
|
||||
// Get the state of the version being activated for MCP tool sync
|
||||
const [versionData] = await db
|
||||
.select({ state: workflowDeploymentVersion.state })
|
||||
.from(workflowDeploymentVersion)
|
||||
.where(
|
||||
and(
|
||||
eq(workflowDeploymentVersion.workflowId, id),
|
||||
eq(workflowDeploymentVersion.version, versionNum)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
await tx
|
||||
.update(workflowDeploymentVersion)
|
||||
@@ -156,11 +65,6 @@ export async function POST(
|
||||
await tx.update(workflow).set(updateData).where(eq(workflow.id, id))
|
||||
})
|
||||
|
||||
// Sync MCP tools with the activated version's parameter schema
|
||||
if (versionData?.state) {
|
||||
await syncMcpToolsOnVersionActivate(id, versionData.state, requestId)
|
||||
}
|
||||
|
||||
return createSuccessResponse({ success: true, deployedAt: now })
|
||||
} catch (error: any) {
|
||||
logger.error(`[${requestId}] Error activating deployment for workflow: ${id}`, error)
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
import { db, workflow, workflowDeploymentVersion, workflowMcpTool } from '@sim/db'
|
||||
import { db, workflow, workflowDeploymentVersion } from '@sim/db'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { env } from '@/lib/core/config/env'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import {
|
||||
extractInputFormatFromBlocks,
|
||||
generateToolInputSchema,
|
||||
} from '@/lib/mcp/workflow-tool-schema'
|
||||
import { saveWorkflowToNormalizedTables } from '@/lib/workflows/persistence/utils'
|
||||
import { hasValidStartBlockInState } from '@/lib/workflows/triggers/trigger-utils'
|
||||
import { validateWorkflowPermissions } from '@/lib/workflows/utils'
|
||||
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
|
||||
|
||||
@@ -18,80 +13,6 @@ const logger = createLogger('RevertToDeploymentVersionAPI')
|
||||
export const dynamic = 'force-dynamic'
|
||||
export const runtime = 'nodejs'
|
||||
|
||||
/**
|
||||
* Extract input format from a deployment version state and generate MCP tool parameter schema
|
||||
*/
|
||||
function generateMcpToolSchemaFromState(state: any): Record<string, unknown> {
|
||||
try {
|
||||
if (!state?.blocks) {
|
||||
return { type: 'object', properties: {} }
|
||||
}
|
||||
|
||||
const inputFormat = extractInputFormatFromBlocks(state.blocks)
|
||||
if (!inputFormat || inputFormat.length === 0) {
|
||||
return { type: 'object', properties: {} }
|
||||
}
|
||||
|
||||
return generateToolInputSchema(inputFormat) as unknown as Record<string, unknown>
|
||||
} catch (error) {
|
||||
logger.warn('Error generating MCP tool schema from state:', error)
|
||||
return { type: 'object', properties: {} }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync MCP tools when reverting to a deployment version.
|
||||
* If the version has no start block, remove all MCP tools.
|
||||
*/
|
||||
async function syncMcpToolsOnRevert(
|
||||
workflowId: string,
|
||||
versionState: any,
|
||||
requestId: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
// Get all MCP tools that use this workflow
|
||||
const tools = await db
|
||||
.select({ id: workflowMcpTool.id })
|
||||
.from(workflowMcpTool)
|
||||
.where(eq(workflowMcpTool.workflowId, workflowId))
|
||||
|
||||
if (tools.length === 0) {
|
||||
logger.debug(`[${requestId}] No MCP tools to sync for workflow: ${workflowId}`)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the reverted version has a valid start block
|
||||
if (!hasValidStartBlockInState(versionState)) {
|
||||
// No start block - remove all MCP tools for this workflow
|
||||
await db.delete(workflowMcpTool).where(eq(workflowMcpTool.workflowId, workflowId))
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Removed ${tools.length} MCP tool(s) - reverted version has no start block: ${workflowId}`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Generate the parameter schema from the reverted version's state
|
||||
const parameterSchema = generateMcpToolSchemaFromState(versionState)
|
||||
|
||||
// Update all tools with the new schema
|
||||
await db
|
||||
.update(workflowMcpTool)
|
||||
.set({
|
||||
parameterSchema,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(workflowMcpTool.workflowId, workflowId))
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Synced ${tools.length} MCP tool(s) for workflow revert: ${workflowId}`
|
||||
)
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error syncing MCP tools on revert:`, error)
|
||||
// Don't throw - this is a non-critical operation
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string; version: string }> }
|
||||
@@ -166,9 +87,6 @@ export async function POST(
|
||||
.set({ lastSynced: new Date(), updatedAt: new Date() })
|
||||
.where(eq(workflow.id, id))
|
||||
|
||||
// Sync MCP tools with the reverted version's parameter schema
|
||||
await syncMcpToolsOnRevert(id, deployedState, requestId)
|
||||
|
||||
try {
|
||||
const socketServerUrl = env.SOCKET_SERVER_URL || 'http://localhost:3002'
|
||||
await fetch(`${socketServerUrl}/api/workflow-reverted`, {
|
||||
|
||||
@@ -30,7 +30,7 @@ const logger = createLogger('WorkflowExecuteAPI')
|
||||
|
||||
const ExecuteWorkflowSchema = z.object({
|
||||
selectedOutputs: z.array(z.string()).optional().default([]),
|
||||
triggerType: z.enum(['api', 'webhook', 'schedule', 'manual', 'chat', 'mcp']).optional(),
|
||||
triggerType: z.enum(['api', 'webhook', 'schedule', 'manual', 'chat']).optional(),
|
||||
stream: z.boolean().optional(),
|
||||
useDraftState: z.boolean().optional(),
|
||||
input: z.any().optional(),
|
||||
@@ -227,7 +227,7 @@ type AsyncExecutionParams = {
|
||||
workflowId: string
|
||||
userId: string
|
||||
input: any
|
||||
triggerType: 'api' | 'webhook' | 'schedule' | 'manual' | 'chat' | 'mcp'
|
||||
triggerType: 'api' | 'webhook' | 'schedule' | 'manual' | 'chat'
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -370,15 +370,14 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
})
|
||||
|
||||
const executionId = uuidv4()
|
||||
type LoggingTriggerType = 'api' | 'webhook' | 'schedule' | 'manual' | 'chat' | 'mcp'
|
||||
type LoggingTriggerType = 'api' | 'webhook' | 'schedule' | 'manual' | 'chat'
|
||||
let loggingTriggerType: LoggingTriggerType = 'manual'
|
||||
if (
|
||||
triggerType === 'api' ||
|
||||
triggerType === 'chat' ||
|
||||
triggerType === 'webhook' ||
|
||||
triggerType === 'schedule' ||
|
||||
triggerType === 'manual' ||
|
||||
triggerType === 'mcp'
|
||||
triggerType === 'manual'
|
||||
) {
|
||||
loggingTriggerType = triggerType as LoggingTriggerType
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export default function WorkspaceLayout({ children }: { children: React.ReactNod
|
||||
<ProviderModelsLoader />
|
||||
<GlobalCommandsProvider>
|
||||
<Tooltip.Provider delayDuration={600} skipDelayDuration={0}>
|
||||
<div className='flex h-screen w-full bg-[var(--bg)]'>
|
||||
<div className='flex h-screen w-full'>
|
||||
<WorkspacePermissionsProvider>
|
||||
<div className='shrink-0' suppressHydrationWarning>
|
||||
<Sidebar />
|
||||
|
||||
@@ -398,7 +398,7 @@ function InputOutputSection({
|
||||
}, [data])
|
||||
|
||||
return (
|
||||
<div className='flex min-w-0 flex-col gap-[8px] overflow-hidden'>
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
<div
|
||||
className='group flex cursor-pointer items-center justify-between'
|
||||
onClick={() => onToggle(sectionKey)}
|
||||
@@ -436,7 +436,7 @@ function InputOutputSection({
|
||||
<Code.Viewer
|
||||
code={jsonString}
|
||||
language='json'
|
||||
className='!bg-[var(--surface-3)] min-h-0 max-w-full rounded-[6px] border-0 [word-break:break-all]'
|
||||
className='!bg-[var(--surface-3)] min-h-0 overflow-hidden rounded-[6px] border-0'
|
||||
wrapText
|
||||
/>
|
||||
)}
|
||||
@@ -477,7 +477,7 @@ function NestedBlockItem({
|
||||
const isChildrenExpanded = expandedChildren.has(spanId)
|
||||
|
||||
return (
|
||||
<div className='flex min-w-0 flex-col gap-[8px] overflow-hidden'>
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
<ExpandableRowHeader
|
||||
name={span.name}
|
||||
duration={span.duration || 0}
|
||||
@@ -502,7 +502,7 @@ function NestedBlockItem({
|
||||
|
||||
{/* Nested children */}
|
||||
{hasChildren && isChildrenExpanded && (
|
||||
<div className='mt-[2px] flex min-w-0 flex-col gap-[10px] overflow-hidden border-[var(--border)] border-l-2 pl-[10px]'>
|
||||
<div className='mt-[2px] flex flex-col gap-[10px] border-[var(--border)] border-l-2 pl-[10px]'>
|
||||
{span.children!.map((child, childIndex) => (
|
||||
<NestedBlockItem
|
||||
key={child.id || `${spanId}-child-${childIndex}`}
|
||||
@@ -617,7 +617,7 @@ function TraceSpanItem({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='flex min-w-0 flex-col gap-[8px] overflow-hidden rounded-[6px] bg-[var(--surface-1)] px-[10px] py-[8px]'>
|
||||
<div className='flex flex-col gap-[8px] rounded-[6px] bg-[var(--surface-1)] px-[10px] py-[8px]'>
|
||||
<ExpandableRowHeader
|
||||
name={span.name}
|
||||
duration={duration}
|
||||
@@ -642,7 +642,7 @@ function TraceSpanItem({
|
||||
|
||||
{/* For workflow blocks, keep children nested within the card (not as separate cards) */}
|
||||
{!isFirstSpan && isWorkflowBlock && inlineChildren.length > 0 && isCardExpanded && (
|
||||
<div className='mt-[2px] flex min-w-0 flex-col gap-[10px] overflow-hidden border-[var(--border)] border-l-2 pl-[10px]'>
|
||||
<div className='mt-[2px] flex flex-col gap-[10px] border-[var(--border)] border-l-2 pl-[10px]'>
|
||||
{inlineChildren.map((childSpan, index) => (
|
||||
<NestedBlockItem
|
||||
key={childSpan.id || `${spanId}-nested-${index}`}
|
||||
@@ -662,7 +662,7 @@ function TraceSpanItem({
|
||||
|
||||
{/* For non-workflow blocks, render inline children/tool calls */}
|
||||
{!isFirstSpan && !isWorkflowBlock && isCardExpanded && (
|
||||
<div className='mt-[2px] flex min-w-0 flex-col gap-[10px] overflow-hidden border-[var(--border)] border-l-2 pl-[10px]'>
|
||||
<div className='mt-[2px] flex flex-col gap-[10px] border-[var(--border)] border-l-2 pl-[10px]'>
|
||||
{[...toolCallSpans, ...inlineChildren].map((childSpan, index) => {
|
||||
const childId = childSpan.id || `${spanId}-inline-${index}`
|
||||
const childIsError = childSpan.status === 'error'
|
||||
@@ -677,10 +677,7 @@ function TraceSpanItem({
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`inline-${childId}`}
|
||||
className='flex min-w-0 flex-col gap-[8px] overflow-hidden'
|
||||
>
|
||||
<div key={`inline-${childId}`} className='flex flex-col gap-[8px]'>
|
||||
<ExpandableRowHeader
|
||||
name={childSpan.name}
|
||||
duration={childSpan.duration || 0}
|
||||
@@ -730,7 +727,7 @@ function TraceSpanItem({
|
||||
|
||||
{/* Nested children */}
|
||||
{showChildrenInProgressBar && hasNestedChildren && isNestedExpanded && (
|
||||
<div className='mt-[2px] flex min-w-0 flex-col gap-[10px] overflow-hidden border-[var(--border)] border-l-2 pl-[10px]'>
|
||||
<div className='mt-[2px] flex flex-col gap-[10px] border-[var(--border)] border-l-2 pl-[10px]'>
|
||||
{childSpan.children!.map((nestedChild, nestedIndex) => (
|
||||
<NestedBlockItem
|
||||
key={nestedChild.id || `${childId}-nested-${nestedIndex}`}
|
||||
@@ -812,9 +809,9 @@ export function TraceSpans({ traceSpans, totalDuration = 0 }: TraceSpansProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex w-full min-w-0 flex-col gap-[6px] overflow-hidden rounded-[6px] bg-[var(--surface-2)] px-[10px] py-[8px]'>
|
||||
<div className='flex w-full flex-col gap-[6px] rounded-[6px] bg-[var(--surface-2)] px-[10px] py-[8px]'>
|
||||
<span className='font-medium text-[12px] text-[var(--text-tertiary)]'>Trace Span</span>
|
||||
<div className='flex min-w-0 flex-col gap-[8px] overflow-hidden'>
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
{normalizedSpans.map((span, index) => (
|
||||
<TraceSpanItem
|
||||
key={span.id || index}
|
||||
|
||||
@@ -43,7 +43,7 @@ const PRIMARY_BUTTON_STYLES =
|
||||
|
||||
type NotificationType = 'webhook' | 'email' | 'slack'
|
||||
type LogLevel = 'info' | 'error'
|
||||
type TriggerType = 'api' | 'webhook' | 'schedule' | 'manual' | 'chat' | 'mcp'
|
||||
type TriggerType = 'api' | 'webhook' | 'schedule' | 'manual' | 'chat'
|
||||
type AlertRule =
|
||||
| 'none'
|
||||
| 'consecutive_failures'
|
||||
@@ -84,7 +84,7 @@ interface NotificationSettingsProps {
|
||||
}
|
||||
|
||||
const LOG_LEVELS: LogLevel[] = ['info', 'error']
|
||||
const TRIGGER_TYPES: TriggerType[] = ['api', 'webhook', 'schedule', 'manual', 'chat', 'mcp']
|
||||
const TRIGGER_TYPES: TriggerType[] = ['api', 'webhook', 'schedule', 'manual', 'chat']
|
||||
|
||||
function formatAlertConfigLabel(config: {
|
||||
rule: AlertRule
|
||||
@@ -137,7 +137,7 @@ export function NotificationSettings({
|
||||
workflowIds: [] as string[],
|
||||
allWorkflows: true,
|
||||
levelFilter: ['info', 'error'] as LogLevel[],
|
||||
triggerFilter: ['api', 'webhook', 'schedule', 'manual', 'chat', 'mcp'] as TriggerType[],
|
||||
triggerFilter: ['api', 'webhook', 'schedule', 'manual', 'chat'] as TriggerType[],
|
||||
includeFinalOutput: false,
|
||||
includeTraceSpans: false,
|
||||
includeRateLimits: false,
|
||||
@@ -207,7 +207,7 @@ export function NotificationSettings({
|
||||
workflowIds: [],
|
||||
allWorkflows: true,
|
||||
levelFilter: ['info', 'error'],
|
||||
triggerFilter: ['api', 'webhook', 'schedule', 'manual', 'chat', 'mcp'],
|
||||
triggerFilter: ['api', 'webhook', 'schedule', 'manual', 'chat'],
|
||||
includeFinalOutput: false,
|
||||
includeTraceSpans: false,
|
||||
includeRateLimits: false,
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Search, X } from 'lucide-react'
|
||||
import { Badge, Popover, PopoverAnchor, PopoverContent } from '@/components/emcn'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { Button, Popover, PopoverAnchor, PopoverContent } from '@/components/emcn'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { getTriggerOptions } from '@/lib/logs/get-trigger-options'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { getIntegrationMetadata } from '@/lib/logs/get-trigger-options'
|
||||
import { type ParsedFilter, parseQuery } from '@/lib/logs/query-parser'
|
||||
import {
|
||||
type FolderData,
|
||||
@@ -16,15 +18,7 @@ import { useSearchState } from '@/app/workspace/[workspaceId]/logs/hooks/use-sea
|
||||
import { useFolderStore } from '@/stores/folders/store'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
|
||||
function truncateFilterValue(field: string, value: string): string {
|
||||
if ((field === 'executionId' || field === 'workflowId') && value.length > 12) {
|
||||
return `...${value.slice(-6)}`
|
||||
}
|
||||
if (value.length > 20) {
|
||||
return `${value.slice(0, 17)}...`
|
||||
}
|
||||
return value
|
||||
}
|
||||
const logger = createLogger('AutocompleteSearch')
|
||||
|
||||
interface AutocompleteSearchProps {
|
||||
value: string
|
||||
@@ -41,8 +35,11 @@ export function AutocompleteSearch({
|
||||
className,
|
||||
onOpenChange,
|
||||
}: AutocompleteSearchProps) {
|
||||
const params = useParams()
|
||||
const workspaceId = params.workspaceId as string
|
||||
const workflows = useWorkflowRegistry((state) => state.workflows)
|
||||
const folders = useFolderStore((state) => state.folders)
|
||||
const [triggersData, setTriggersData] = useState<TriggerData[]>([])
|
||||
|
||||
const workflowsData = useMemo<WorkflowData[]>(() => {
|
||||
return Object.values(workflows).map((w) => ({
|
||||
@@ -59,13 +56,32 @@ export function AutocompleteSearch({
|
||||
}))
|
||||
}, [folders])
|
||||
|
||||
const triggersData = useMemo<TriggerData[]>(() => {
|
||||
return getTriggerOptions().map((t) => ({
|
||||
value: t.value,
|
||||
label: t.label,
|
||||
color: t.color,
|
||||
}))
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
if (!workspaceId) return
|
||||
|
||||
const fetchTriggers = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/logs/triggers?workspaceId=${workspaceId}`)
|
||||
if (!response.ok) return
|
||||
|
||||
const data = await response.json()
|
||||
const triggers: TriggerData[] = data.triggers.map((trigger: string) => {
|
||||
const metadata = getIntegrationMetadata(trigger)
|
||||
return {
|
||||
value: trigger,
|
||||
label: metadata.label,
|
||||
color: metadata.color,
|
||||
}
|
||||
})
|
||||
|
||||
setTriggersData(triggers)
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch triggers:', error)
|
||||
}
|
||||
}
|
||||
|
||||
fetchTriggers()
|
||||
}, [workspaceId])
|
||||
|
||||
const suggestionEngine = useMemo(() => {
|
||||
return new SearchSuggestions(workflowsData, foldersData, triggersData)
|
||||
@@ -87,6 +103,7 @@ export function AutocompleteSearch({
|
||||
suggestions,
|
||||
sections,
|
||||
highlightedIndex,
|
||||
highlightedBadgeIndex,
|
||||
inputRef,
|
||||
dropdownRef,
|
||||
handleInputChange,
|
||||
@@ -105,6 +122,7 @@ export function AutocompleteSearch({
|
||||
|
||||
const lastExternalValue = useRef(value)
|
||||
useEffect(() => {
|
||||
// Only re-initialize if value changed externally (not from user typing)
|
||||
if (value !== lastExternalValue.current) {
|
||||
lastExternalValue.current = value
|
||||
const parsed = parseQuery(value)
|
||||
@@ -112,6 +130,7 @@ export function AutocompleteSearch({
|
||||
}
|
||||
}, [value, initializeFromQuery])
|
||||
|
||||
// Initial sync on mount
|
||||
useEffect(() => {
|
||||
if (value) {
|
||||
const parsed = parseQuery(value)
|
||||
@@ -170,49 +189,40 @@ export function AutocompleteSearch({
|
||||
<div className='flex flex-1 items-center gap-[6px] overflow-x-auto pr-[6px] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden'>
|
||||
{/* Applied Filter Badges */}
|
||||
{appliedFilters.map((filter, index) => (
|
||||
<Badge
|
||||
<Button
|
||||
key={`${filter.field}-${filter.value}-${index}`}
|
||||
variant='outline'
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
className='h-6 shrink-0 cursor-pointer whitespace-nowrap rounded-md px-2 text-[11px]'
|
||||
onClick={() => removeBadge(index)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault()
|
||||
removeBadge(index)
|
||||
}
|
||||
className={cn(
|
||||
'h-6 flex-shrink-0 gap-1 rounded-[6px] px-2 text-[11px]',
|
||||
highlightedBadgeIndex === index && 'border'
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
removeBadge(index)
|
||||
}}
|
||||
>
|
||||
<span className='text-[var(--text-muted)]'>{filter.field}:</span>
|
||||
<span className='text-[var(--text-primary)]'>
|
||||
{filter.operator !== '=' && filter.operator}
|
||||
{truncateFilterValue(filter.field, filter.originalValue)}
|
||||
{filter.originalValue}
|
||||
</span>
|
||||
<X className='h-3 w-3 shrink-0' />
|
||||
</Badge>
|
||||
<X className='h-3 w-3' />
|
||||
</Button>
|
||||
))}
|
||||
|
||||
{/* Text Search Badge (if present) */}
|
||||
{hasTextSearch && (
|
||||
<Badge
|
||||
<Button
|
||||
variant='outline'
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
className='h-6 shrink-0 cursor-pointer whitespace-nowrap rounded-md px-2 text-[11px]'
|
||||
onClick={() => handleFiltersChange(appliedFilters, '')}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault()
|
||||
handleFiltersChange(appliedFilters, '')
|
||||
}
|
||||
className='h-6 flex-shrink-0 gap-1 rounded-[6px] px-2 text-[11px]'
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
handleFiltersChange(appliedFilters, '')
|
||||
}}
|
||||
>
|
||||
<span className='max-w-[150px] truncate text-[var(--text-primary)]'>
|
||||
"{textSearch}"
|
||||
</span>
|
||||
<X className='h-3 w-3 shrink-0' />
|
||||
</Badge>
|
||||
<span className='text-[var(--text-primary)]'>"{textSearch}"</span>
|
||||
<X className='h-3 w-3' />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Input - only current typing */}
|
||||
@@ -251,8 +261,9 @@ export function AutocompleteSearch({
|
||||
sideOffset={4}
|
||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||
>
|
||||
<div className='max-h-96 overflow-y-auto px-1'>
|
||||
<div className='max-h-96 overflow-y-auto'>
|
||||
{sections.length > 0 ? (
|
||||
// Multi-section layout
|
||||
<div className='py-1'>
|
||||
{/* Show all results (no header) */}
|
||||
{suggestions[0]?.category === 'show-all' && (
|
||||
@@ -260,9 +271,9 @@ export function AutocompleteSearch({
|
||||
key={suggestions[0].id}
|
||||
data-index={0}
|
||||
className={cn(
|
||||
'w-full rounded-[6px] px-3 py-2 text-left transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[var(--border-focus)]',
|
||||
'hover:bg-[var(--surface-9)]',
|
||||
highlightedIndex === 0 && 'bg-[var(--surface-9)]'
|
||||
'w-full px-3 py-1.5 text-left transition-colors focus:outline-none',
|
||||
'hover:bg-[var(--surface-9)] dark:hover:bg-[var(--surface-9)]',
|
||||
highlightedIndex === 0 && 'bg-[var(--surface-9)] dark:bg-[var(--surface-9)]'
|
||||
)}
|
||||
onMouseEnter={() => setHighlightedIndex(0)}
|
||||
onMouseDown={(e) => {
|
||||
@@ -276,7 +287,7 @@ export function AutocompleteSearch({
|
||||
|
||||
{sections.map((section) => (
|
||||
<div key={section.title}>
|
||||
<div className='px-3 py-1.5 font-medium text-[12px] text-[var(--text-tertiary)] uppercase tracking-wide'>
|
||||
<div className='border-[var(--divider)] border-t px-3 py-1.5 font-medium text-[11px] text-[var(--text-tertiary)] uppercase tracking-wide'>
|
||||
{section.title}
|
||||
</div>
|
||||
{section.suggestions.map((suggestion) => {
|
||||
@@ -290,9 +301,9 @@ export function AutocompleteSearch({
|
||||
key={suggestion.id}
|
||||
data-index={index}
|
||||
className={cn(
|
||||
'w-full rounded-[6px] px-3 py-2 text-left transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[var(--border-focus)]',
|
||||
'hover:bg-[var(--surface-9)]',
|
||||
isHighlighted && 'bg-[var(--surface-9)]'
|
||||
'w-full px-3 py-1.5 text-left transition-colors focus:outline-none',
|
||||
'hover:bg-[var(--surface-9)] dark:hover:bg-[var(--surface-9)]',
|
||||
isHighlighted && 'bg-[var(--surface-9)] dark:bg-[var(--surface-9)]'
|
||||
)}
|
||||
onMouseEnter={() => setHighlightedIndex(index)}
|
||||
onMouseDown={(e) => {
|
||||
@@ -301,11 +312,19 @@ export function AutocompleteSearch({
|
||||
}}
|
||||
>
|
||||
<div className='flex items-center justify-between gap-3'>
|
||||
<div className='min-w-0 flex-1 truncate text-[13px]'>
|
||||
{suggestion.label}
|
||||
<div className='flex min-w-0 flex-1 items-center gap-2'>
|
||||
{suggestion.category === 'trigger' && suggestion.color && (
|
||||
<div
|
||||
className='h-2 w-2 flex-shrink-0 rounded-full'
|
||||
style={{ backgroundColor: suggestion.color }}
|
||||
/>
|
||||
)}
|
||||
<div className='min-w-0 flex-1 truncate text-[13px]'>
|
||||
{suggestion.label}
|
||||
</div>
|
||||
</div>
|
||||
{suggestion.value !== suggestion.label && (
|
||||
<div className='shrink-0 font-mono text-[11px] text-[var(--text-muted)]'>
|
||||
<div className='flex-shrink-0 font-mono text-[11px] text-[var(--text-muted)]'>
|
||||
{suggestion.category === 'workflow' ||
|
||||
suggestion.category === 'folder'
|
||||
? `${suggestion.category}:`
|
||||
@@ -323,7 +342,7 @@ export function AutocompleteSearch({
|
||||
// Single section layout
|
||||
<div className='py-1'>
|
||||
{suggestionType === 'filters' && (
|
||||
<div className='px-3 py-1.5 font-medium text-[12px] text-[var(--text-tertiary)] uppercase tracking-wide'>
|
||||
<div className='border-[var(--divider)] border-b px-3 py-1.5 font-medium text-[11px] text-[var(--text-tertiary)] uppercase tracking-wide'>
|
||||
SUGGESTED FILTERS
|
||||
</div>
|
||||
)}
|
||||
@@ -333,9 +352,10 @@ export function AutocompleteSearch({
|
||||
key={suggestion.id}
|
||||
data-index={index}
|
||||
className={cn(
|
||||
'w-full rounded-[6px] px-3 py-2 text-left transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[var(--border-focus)]',
|
||||
'hover:bg-[var(--surface-9)]',
|
||||
index === highlightedIndex && 'bg-[var(--surface-9)]'
|
||||
'w-full px-3 py-1.5 text-left transition-colors focus:outline-none',
|
||||
'hover:bg-[var(--surface-9)] dark:hover:bg-[var(--surface-9)]',
|
||||
index === highlightedIndex &&
|
||||
'bg-[var(--surface-9)] dark:bg-[var(--surface-9)]'
|
||||
)}
|
||||
onMouseEnter={() => setHighlightedIndex(index)}
|
||||
onMouseDown={(e) => {
|
||||
@@ -344,9 +364,17 @@ export function AutocompleteSearch({
|
||||
}}
|
||||
>
|
||||
<div className='flex items-center justify-between gap-3'>
|
||||
<div className='min-w-0 flex-1 text-[13px]'>{suggestion.label}</div>
|
||||
<div className='flex min-w-0 flex-1 items-center gap-2'>
|
||||
{suggestion.category === 'trigger' && suggestion.color && (
|
||||
<div
|
||||
className='h-2 w-2 flex-shrink-0 rounded-full'
|
||||
style={{ backgroundColor: suggestion.color }}
|
||||
/>
|
||||
)}
|
||||
<div className='min-w-0 flex-1 text-[13px]'>{suggestion.label}</div>
|
||||
</div>
|
||||
{suggestion.description && (
|
||||
<div className='shrink-0 text-[11px] text-[var(--text-muted)]'>
|
||||
<div className='flex-shrink-0 text-[11px] text-[var(--text-muted)]'>
|
||||
{suggestion.value}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -21,7 +21,7 @@ import { useFolderStore } from '@/stores/folders/store'
|
||||
import { useFilterStore } from '@/stores/logs/filters/store'
|
||||
import { AutocompleteSearch } from './components/search'
|
||||
|
||||
const CORE_TRIGGER_TYPES = ['manual', 'api', 'schedule', 'chat', 'webhook', 'mcp'] as const
|
||||
const CORE_TRIGGER_TYPES = ['manual', 'api', 'schedule', 'chat', 'webhook'] as const
|
||||
|
||||
const TIME_RANGE_OPTIONS: ComboboxOption[] = [
|
||||
{ value: 'All time', label: 'All time' },
|
||||
|
||||
@@ -21,15 +21,21 @@ export function useSearchState({
|
||||
const [currentInput, setCurrentInput] = useState('')
|
||||
const [textSearch, setTextSearch] = useState('')
|
||||
|
||||
// Dropdown state
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [suggestions, setSuggestions] = useState<Suggestion[]>([])
|
||||
const [sections, setSections] = useState<SuggestionSection[]>([])
|
||||
const [highlightedIndex, setHighlightedIndex] = useState(-1)
|
||||
|
||||
// Badge interaction
|
||||
const [highlightedBadgeIndex, setHighlightedBadgeIndex] = useState<number | null>(null)
|
||||
|
||||
// Refs
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||
const debounceRef = useRef<NodeJS.Timeout | null>(null)
|
||||
|
||||
// Update suggestions when input changes
|
||||
const updateSuggestions = useCallback(
|
||||
(input: string) => {
|
||||
const suggestionGroup = getSuggestions(input)
|
||||
@@ -49,10 +55,13 @@ export function useSearchState({
|
||||
[getSuggestions]
|
||||
)
|
||||
|
||||
// Handle input changes
|
||||
const handleInputChange = useCallback(
|
||||
(value: string) => {
|
||||
setCurrentInput(value)
|
||||
setHighlightedBadgeIndex(null) // Clear badge highlight on any input
|
||||
|
||||
// Debounce suggestion updates
|
||||
if (debounceRef.current) {
|
||||
clearTimeout(debounceRef.current)
|
||||
}
|
||||
@@ -64,9 +73,11 @@ export function useSearchState({
|
||||
[updateSuggestions, debounceMs]
|
||||
)
|
||||
|
||||
// Handle suggestion selection
|
||||
const handleSuggestionSelect = useCallback(
|
||||
(suggestion: Suggestion) => {
|
||||
if (suggestion.category === 'show-all') {
|
||||
// Treat as text search
|
||||
setTextSearch(suggestion.value)
|
||||
setCurrentInput('')
|
||||
setIsOpen(false)
|
||||
@@ -74,12 +85,15 @@ export function useSearchState({
|
||||
return
|
||||
}
|
||||
|
||||
// Check if this is a filter-key suggestion (ends with ':')
|
||||
if (suggestion.category === 'filters' && suggestion.value.endsWith(':')) {
|
||||
// Set input to the filter key and keep dropdown open for values
|
||||
setCurrentInput(suggestion.value)
|
||||
updateSuggestions(suggestion.value)
|
||||
return
|
||||
}
|
||||
|
||||
// For filter values, workflows, folders - add as a filter
|
||||
const newFilter: ParsedFilter = {
|
||||
field: suggestion.value.split(':')[0] as any,
|
||||
operator: '=',
|
||||
@@ -96,12 +110,15 @@ export function useSearchState({
|
||||
setCurrentInput('')
|
||||
setTextSearch('')
|
||||
|
||||
// Notify parent
|
||||
onFiltersChange(updatedFilters, '')
|
||||
|
||||
// Focus back on input and reopen dropdown with empty suggestions
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus()
|
||||
}
|
||||
|
||||
// Show filter keys dropdown again after selection
|
||||
setTimeout(() => {
|
||||
updateSuggestions('')
|
||||
}, 50)
|
||||
@@ -109,10 +126,12 @@ export function useSearchState({
|
||||
[appliedFilters, onFiltersChange, updateSuggestions]
|
||||
)
|
||||
|
||||
// Remove a badge
|
||||
const removeBadge = useCallback(
|
||||
(index: number) => {
|
||||
const updatedFilters = appliedFilters.filter((_, i) => i !== index)
|
||||
setAppliedFilters(updatedFilters)
|
||||
setHighlightedBadgeIndex(null)
|
||||
onFiltersChange(updatedFilters, textSearch)
|
||||
|
||||
if (inputRef.current) {
|
||||
@@ -122,22 +141,39 @@ export function useSearchState({
|
||||
[appliedFilters, textSearch, onFiltersChange]
|
||||
)
|
||||
|
||||
// Handle keyboard navigation
|
||||
const handleKeyDown = useCallback(
|
||||
(event: React.KeyboardEvent) => {
|
||||
// Backspace on empty input - badge deletion
|
||||
if (event.key === 'Backspace' && currentInput === '') {
|
||||
if (appliedFilters.length > 0) {
|
||||
event.preventDefault()
|
||||
removeBadge(appliedFilters.length - 1)
|
||||
event.preventDefault()
|
||||
|
||||
if (highlightedBadgeIndex !== null) {
|
||||
// Delete highlighted badge
|
||||
removeBadge(highlightedBadgeIndex)
|
||||
} else if (appliedFilters.length > 0) {
|
||||
// Highlight last badge
|
||||
setHighlightedBadgeIndex(appliedFilters.length - 1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Clear badge highlight on any other key when not in dropdown navigation
|
||||
if (
|
||||
highlightedBadgeIndex !== null &&
|
||||
!['ArrowDown', 'ArrowUp', 'Enter'].includes(event.key)
|
||||
) {
|
||||
setHighlightedBadgeIndex(null)
|
||||
}
|
||||
|
||||
// Enter key
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
|
||||
if (isOpen && highlightedIndex >= 0 && suggestions[highlightedIndex]) {
|
||||
handleSuggestionSelect(suggestions[highlightedIndex])
|
||||
} else if (currentInput.trim()) {
|
||||
// Submit current input as text search
|
||||
setTextSearch(currentInput.trim())
|
||||
setCurrentInput('')
|
||||
setIsOpen(false)
|
||||
@@ -146,6 +182,7 @@ export function useSearchState({
|
||||
return
|
||||
}
|
||||
|
||||
// Dropdown navigation
|
||||
if (!isOpen) return
|
||||
|
||||
switch (event.key) {
|
||||
@@ -179,6 +216,7 @@ export function useSearchState({
|
||||
},
|
||||
[
|
||||
currentInput,
|
||||
highlightedBadgeIndex,
|
||||
appliedFilters,
|
||||
isOpen,
|
||||
highlightedIndex,
|
||||
@@ -189,10 +227,12 @@ export function useSearchState({
|
||||
]
|
||||
)
|
||||
|
||||
// Handle focus
|
||||
const handleFocus = useCallback(() => {
|
||||
updateSuggestions(currentInput)
|
||||
}, [currentInput, updateSuggestions])
|
||||
|
||||
// Handle blur
|
||||
const handleBlur = useCallback(() => {
|
||||
setTimeout(() => {
|
||||
setIsOpen(false)
|
||||
@@ -200,6 +240,7 @@ export function useSearchState({
|
||||
}, 150)
|
||||
}, [])
|
||||
|
||||
// Clear all filters
|
||||
const clearAll = useCallback(() => {
|
||||
setAppliedFilters([])
|
||||
setCurrentInput('')
|
||||
@@ -212,6 +253,7 @@ export function useSearchState({
|
||||
}
|
||||
}, [onFiltersChange])
|
||||
|
||||
// Initialize from external value (URL params, etc.)
|
||||
const initializeFromQuery = useCallback((query: string, filters: ParsedFilter[]) => {
|
||||
setAppliedFilters(filters)
|
||||
setTextSearch(query)
|
||||
@@ -219,6 +261,7 @@ export function useSearchState({
|
||||
}, [])
|
||||
|
||||
return {
|
||||
// State
|
||||
appliedFilters,
|
||||
currentInput,
|
||||
textSearch,
|
||||
@@ -226,10 +269,13 @@ export function useSearchState({
|
||||
suggestions,
|
||||
sections,
|
||||
highlightedIndex,
|
||||
highlightedBadgeIndex,
|
||||
|
||||
// Refs
|
||||
inputRef,
|
||||
dropdownRef,
|
||||
|
||||
// Handlers
|
||||
handleInputChange,
|
||||
handleSuggestionSelect,
|
||||
handleKeyDown,
|
||||
@@ -239,6 +285,7 @@ export function useSearchState({
|
||||
clearAll,
|
||||
initializeFromQuery,
|
||||
|
||||
// Setters for external control
|
||||
setHighlightedIndex,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Badge } from '@/components/emcn'
|
||||
import { getIntegrationMetadata } from '@/lib/logs/get-trigger-options'
|
||||
import { getBlock } from '@/blocks/registry'
|
||||
|
||||
const CORE_TRIGGER_TYPES = ['manual', 'api', 'schedule', 'chat', 'webhook', 'mcp'] as const
|
||||
const CORE_TRIGGER_TYPES = ['manual', 'api', 'schedule', 'chat', 'webhook'] as const
|
||||
const RUNNING_COLOR = '#22c55e' as const
|
||||
const PENDING_COLOR = '#f59e0b' as const
|
||||
|
||||
|
||||
@@ -1,861 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
AlertTriangle,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Plus,
|
||||
RefreshCw,
|
||||
Server,
|
||||
Trash2,
|
||||
} from 'lucide-react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Input as EmcnInput,
|
||||
Label,
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverItem,
|
||||
PopoverTrigger,
|
||||
} from '@/components/emcn'
|
||||
import { Skeleton } from '@/components/ui'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { generateToolInputSchema, sanitizeToolName } from '@/lib/mcp/workflow-tool-schema'
|
||||
import {
|
||||
useAddWorkflowMcpTool,
|
||||
useDeleteWorkflowMcpTool,
|
||||
useUpdateWorkflowMcpTool,
|
||||
useWorkflowMcpServers,
|
||||
useWorkflowMcpTools,
|
||||
type WorkflowMcpServer,
|
||||
type WorkflowMcpTool,
|
||||
} from '@/hooks/queries/workflow-mcp-servers'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
|
||||
const logger = createLogger('McpToolDeploy')
|
||||
|
||||
interface McpToolDeployProps {
|
||||
workflowId: string
|
||||
workflowName: string
|
||||
workflowDescription?: string | null
|
||||
isDeployed: boolean
|
||||
onAddedToServer?: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract input format from workflow blocks using SubBlockStore
|
||||
* The actual input format values are stored in useSubBlockStore, not directly in the block structure
|
||||
*/
|
||||
function extractInputFormat(
|
||||
blocks: Record<string, unknown>
|
||||
): Array<{ name: string; type: string }> {
|
||||
// Find the starter block
|
||||
for (const [blockId, block] of Object.entries(blocks)) {
|
||||
if (!block || typeof block !== 'object') continue
|
||||
|
||||
const blockObj = block as Record<string, unknown>
|
||||
const blockType = blockObj.type
|
||||
|
||||
// Check for all possible start/trigger block types
|
||||
if (
|
||||
blockType === 'starter' ||
|
||||
blockType === 'start' ||
|
||||
blockType === 'start_trigger' || // This is the unified start block type
|
||||
blockType === 'api' ||
|
||||
blockType === 'api_trigger' ||
|
||||
blockType === 'input_trigger'
|
||||
) {
|
||||
// Get the inputFormat value from the SubBlockStore (where the actual values are stored)
|
||||
const inputFormatValue = useSubBlockStore.getState().getValue(blockId, 'inputFormat')
|
||||
|
||||
if (Array.isArray(inputFormatValue) && inputFormatValue.length > 0) {
|
||||
return inputFormatValue
|
||||
.filter(
|
||||
(field: unknown): field is { name: string; type: string } =>
|
||||
field !== null &&
|
||||
typeof field === 'object' &&
|
||||
'name' in field &&
|
||||
typeof (field as { name: unknown }).name === 'string' &&
|
||||
(field as { name: string }).name.trim() !== ''
|
||||
)
|
||||
.map((field) => ({
|
||||
name: field.name.trim(),
|
||||
type: field.type || 'string',
|
||||
}))
|
||||
}
|
||||
|
||||
// Fallback: try to get from block's subBlocks structure (for backwards compatibility)
|
||||
const subBlocks = blockObj.subBlocks as Record<string, unknown> | undefined
|
||||
if (subBlocks?.inputFormat) {
|
||||
const inputFormatSubBlock = subBlocks.inputFormat as Record<string, unknown>
|
||||
const value = inputFormatSubBlock.value
|
||||
if (Array.isArray(value) && value.length > 0) {
|
||||
return value
|
||||
.filter(
|
||||
(field: unknown): field is { name: string; type: string } =>
|
||||
field !== null &&
|
||||
typeof field === 'object' &&
|
||||
'name' in field &&
|
||||
typeof (field as { name: unknown }).name === 'string' &&
|
||||
(field as { name: string }).name.trim() !== ''
|
||||
)
|
||||
.map((field) => ({
|
||||
name: field.name.trim(),
|
||||
type: field.type || 'string',
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate JSON Schema from input format using the shared utility
|
||||
* Optionally applies custom descriptions from the UI
|
||||
*/
|
||||
function generateParameterSchema(
|
||||
inputFormat: Array<{ name: string; type: string }>,
|
||||
customDescriptions?: Record<string, string>
|
||||
): Record<string, unknown> {
|
||||
// Convert to InputFormatField with descriptions
|
||||
const fieldsWithDescriptions = inputFormat.map((field) => ({
|
||||
...field,
|
||||
description: customDescriptions?.[field.name]?.trim() || undefined,
|
||||
}))
|
||||
return generateToolInputSchema(fieldsWithDescriptions) as unknown as Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract parameter names from a tool's parameter schema
|
||||
*/
|
||||
function getToolParameterNames(schema: Record<string, unknown>): string[] {
|
||||
const properties = schema.properties as Record<string, unknown> | undefined
|
||||
if (!properties) return []
|
||||
return Object.keys(properties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the tool's parameters differ from the current workflow's input format
|
||||
*/
|
||||
function hasParameterMismatch(
|
||||
tool: WorkflowMcpTool,
|
||||
currentInputFormat: Array<{ name: string; type: string }>
|
||||
): boolean {
|
||||
const toolParams = getToolParameterNames(tool.parameterSchema as Record<string, unknown>)
|
||||
const currentParams = currentInputFormat.map((f) => f.name)
|
||||
|
||||
if (toolParams.length !== currentParams.length) return true
|
||||
|
||||
const toolParamSet = new Set(toolParams)
|
||||
for (const param of currentParams) {
|
||||
if (!toolParamSet.has(param)) return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Component to query tools for a single server and report back via callback.
|
||||
* This pattern avoids calling hooks in a loop.
|
||||
*/
|
||||
function ServerToolsQuery({
|
||||
workspaceId,
|
||||
server,
|
||||
workflowId,
|
||||
onData,
|
||||
}: {
|
||||
workspaceId: string
|
||||
server: WorkflowMcpServer
|
||||
workflowId: string
|
||||
onData: (serverId: string, tool: WorkflowMcpTool | null, isLoading: boolean) => void
|
||||
}) {
|
||||
const { data: tools, isLoading } = useWorkflowMcpTools(workspaceId, server.id)
|
||||
|
||||
useEffect(() => {
|
||||
const tool = tools?.find((t) => t.workflowId === workflowId) || null
|
||||
onData(server.id, tool, isLoading)
|
||||
}, [tools, isLoading, workflowId, server.id, onData])
|
||||
|
||||
return null // This component doesn't render anything
|
||||
}
|
||||
|
||||
interface ToolOnServerProps {
|
||||
server: WorkflowMcpServer
|
||||
tool: WorkflowMcpTool
|
||||
workspaceId: string
|
||||
currentInputFormat: Array<{ name: string; type: string }>
|
||||
currentParameterSchema: Record<string, unknown>
|
||||
workflowDescription: string | null | undefined
|
||||
onRemoved: (serverId: string) => void
|
||||
onUpdated: () => void
|
||||
}
|
||||
|
||||
function ToolOnServer({
|
||||
server,
|
||||
tool,
|
||||
workspaceId,
|
||||
currentInputFormat,
|
||||
currentParameterSchema,
|
||||
workflowDescription,
|
||||
onRemoved,
|
||||
onUpdated,
|
||||
}: ToolOnServerProps) {
|
||||
const deleteToolMutation = useDeleteWorkflowMcpTool()
|
||||
const updateToolMutation = useUpdateWorkflowMcpTool()
|
||||
const [showConfirm, setShowConfirm] = useState(false)
|
||||
const [showDetails, setShowDetails] = useState(false)
|
||||
|
||||
const needsUpdate = hasParameterMismatch(tool, currentInputFormat)
|
||||
const toolParams = getToolParameterNames(tool.parameterSchema as Record<string, unknown>)
|
||||
|
||||
const handleRemove = async () => {
|
||||
try {
|
||||
await deleteToolMutation.mutateAsync({
|
||||
workspaceId,
|
||||
serverId: server.id,
|
||||
toolId: tool.id,
|
||||
})
|
||||
onRemoved(server.id)
|
||||
} catch (error) {
|
||||
logger.error('Failed to remove tool:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdate = async () => {
|
||||
try {
|
||||
await updateToolMutation.mutateAsync({
|
||||
workspaceId,
|
||||
serverId: server.id,
|
||||
toolId: tool.id,
|
||||
toolDescription: workflowDescription || `Execute workflow`,
|
||||
parameterSchema: currentParameterSchema,
|
||||
})
|
||||
onUpdated()
|
||||
logger.info(`Updated tool ${tool.id} with new parameters`)
|
||||
} catch (error) {
|
||||
logger.error('Failed to update tool:', error)
|
||||
}
|
||||
}
|
||||
|
||||
if (showConfirm) {
|
||||
return (
|
||||
<div className='flex items-center justify-between rounded-[6px] border border-[var(--text-error)]/30 bg-[var(--surface-3)] px-[10px] py-[8px]'>
|
||||
<span className='text-[12px] text-[var(--text-secondary)]'>Remove from {server.name}?</span>
|
||||
<div className='flex items-center gap-[4px]'>
|
||||
<Button
|
||||
variant='ghost'
|
||||
onClick={() => setShowConfirm(false)}
|
||||
className='h-[24px] px-[8px] text-[11px]'
|
||||
disabled={deleteToolMutation.isPending}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant='ghost'
|
||||
onClick={handleRemove}
|
||||
className='h-[24px] px-[8px] text-[11px] text-[var(--text-error)] hover:text-[var(--text-error)]'
|
||||
disabled={deleteToolMutation.isPending}
|
||||
>
|
||||
{deleteToolMutation.isPending ? 'Removing...' : 'Remove'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='rounded-[6px] border bg-[var(--surface-3)]'>
|
||||
<div
|
||||
className='flex cursor-pointer items-center justify-between px-[10px] py-[8px]'
|
||||
onClick={() => setShowDetails(!showDetails)}
|
||||
>
|
||||
<div className='flex items-center gap-[8px]'>
|
||||
{showDetails ? (
|
||||
<ChevronDown className='h-[12px] w-[12px] text-[var(--text-tertiary)]' />
|
||||
) : (
|
||||
<ChevronRight className='h-[12px] w-[12px] text-[var(--text-tertiary)]' />
|
||||
)}
|
||||
<span className='text-[13px] text-[var(--text-primary)]'>{server.name}</span>
|
||||
{server.isPublished && (
|
||||
<Badge variant='outline' className='text-[10px]'>
|
||||
Published
|
||||
</Badge>
|
||||
)}
|
||||
{needsUpdate && (
|
||||
<Badge
|
||||
variant='outline'
|
||||
className='border-amber-500/50 bg-amber-500/10 text-[10px] text-amber-500'
|
||||
>
|
||||
<AlertTriangle className='mr-[4px] h-[10px] w-[10px]' />
|
||||
Needs Update
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className='flex items-center gap-[4px]' onClick={(e) => e.stopPropagation()}>
|
||||
{needsUpdate && (
|
||||
<Button
|
||||
variant='ghost'
|
||||
onClick={handleUpdate}
|
||||
disabled={updateToolMutation.isPending}
|
||||
className='h-[24px] px-[8px] text-[11px] text-amber-500 hover:text-amber-600'
|
||||
>
|
||||
<RefreshCw
|
||||
className={cn(
|
||||
'mr-[4px] h-[10px] w-[10px]',
|
||||
updateToolMutation.isPending && 'animate-spin'
|
||||
)}
|
||||
/>
|
||||
{updateToolMutation.isPending ? 'Updating...' : 'Update'}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant='ghost'
|
||||
onClick={() => setShowConfirm(true)}
|
||||
className='h-[24px] w-[24px] p-0 text-[var(--text-tertiary)] hover:text-[var(--text-error)]'
|
||||
>
|
||||
<Trash2 className='h-[12px] w-[12px]' />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showDetails && (
|
||||
<div className='border-[var(--border)] border-t px-[10px] py-[8px]'>
|
||||
<div className='flex flex-col gap-[6px]'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<span className='text-[11px] text-[var(--text-muted)]'>Tool Name</span>
|
||||
<span className='font-mono text-[11px] text-[var(--text-secondary)]'>
|
||||
{tool.toolName}
|
||||
</span>
|
||||
</div>
|
||||
<div className='flex items-start justify-between gap-[8px]'>
|
||||
<span className='flex-shrink-0 text-[11px] text-[var(--text-muted)]'>
|
||||
Description
|
||||
</span>
|
||||
<span className='text-right text-[11px] text-[var(--text-secondary)]'>
|
||||
{tool.toolDescription || '—'}
|
||||
</span>
|
||||
</div>
|
||||
<div className='flex items-start justify-between gap-[8px]'>
|
||||
<span className='flex-shrink-0 text-[11px] text-[var(--text-muted)]'>
|
||||
Parameters ({toolParams.length})
|
||||
</span>
|
||||
<div className='flex flex-wrap justify-end gap-[4px]'>
|
||||
{toolParams.length === 0 ? (
|
||||
<span className='text-[11px] text-[var(--text-muted)]'>None</span>
|
||||
) : (
|
||||
toolParams.map((param) => (
|
||||
<Badge key={param} variant='outline' className='text-[9px]'>
|
||||
{param}
|
||||
</Badge>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function McpToolDeploy({
|
||||
workflowId,
|
||||
workflowName,
|
||||
workflowDescription,
|
||||
isDeployed,
|
||||
onAddedToServer,
|
||||
}: McpToolDeployProps) {
|
||||
const params = useParams()
|
||||
const workspaceId = params.workspaceId as string
|
||||
|
||||
const {
|
||||
data: servers = [],
|
||||
isLoading: isLoadingServers,
|
||||
refetch: refetchServers,
|
||||
} = useWorkflowMcpServers(workspaceId)
|
||||
const addToolMutation = useAddWorkflowMcpTool()
|
||||
|
||||
// Get workflow blocks
|
||||
const blocks = useWorkflowStore((state) => state.blocks)
|
||||
|
||||
// Find the starter block ID to subscribe to its inputFormat changes
|
||||
const starterBlockId = useMemo(() => {
|
||||
for (const [blockId, block] of Object.entries(blocks)) {
|
||||
if (!block || typeof block !== 'object') continue
|
||||
const blockType = (block as { type?: string }).type
|
||||
// Check for all possible start/trigger block types
|
||||
if (
|
||||
blockType === 'starter' ||
|
||||
blockType === 'start' ||
|
||||
blockType === 'start_trigger' || // This is the unified start block type
|
||||
blockType === 'api' ||
|
||||
blockType === 'api_trigger' ||
|
||||
blockType === 'input_trigger'
|
||||
) {
|
||||
return blockId
|
||||
}
|
||||
}
|
||||
return null
|
||||
}, [blocks])
|
||||
|
||||
// Subscribe to the inputFormat value in SubBlockStore for reactivity
|
||||
// Use workflowId prop directly (not activeWorkflowId from registry) to ensure we get the correct workflow's data
|
||||
const subBlockValues = useSubBlockStore((state) =>
|
||||
workflowId ? (state.workflowValues[workflowId] ?? {}) : {}
|
||||
)
|
||||
|
||||
// Extract and normalize input format - now reactive to SubBlockStore changes
|
||||
const inputFormat = useMemo(() => {
|
||||
// First try to get from SubBlockStore (where runtime values are stored)
|
||||
if (starterBlockId && subBlockValues[starterBlockId]) {
|
||||
const inputFormatValue = subBlockValues[starterBlockId].inputFormat
|
||||
|
||||
if (Array.isArray(inputFormatValue) && inputFormatValue.length > 0) {
|
||||
const filtered = inputFormatValue
|
||||
.filter(
|
||||
(field: unknown): field is { name: string; type: string } =>
|
||||
field !== null &&
|
||||
typeof field === 'object' &&
|
||||
'name' in field &&
|
||||
typeof (field as { name: unknown }).name === 'string' &&
|
||||
(field as { name: string }).name.trim() !== ''
|
||||
)
|
||||
.map((field) => ({
|
||||
name: field.name.trim(),
|
||||
type: field.type || 'string',
|
||||
}))
|
||||
if (filtered.length > 0) {
|
||||
return filtered
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: try to get from block structure (for initial load or backwards compatibility)
|
||||
if (starterBlockId && blocks[starterBlockId]) {
|
||||
const startBlock = blocks[starterBlockId]
|
||||
const subBlocksValue = startBlock?.subBlocks?.inputFormat?.value as unknown
|
||||
|
||||
if (Array.isArray(subBlocksValue) && subBlocksValue.length > 0) {
|
||||
const validFields: Array<{ name: string; type: string }> = []
|
||||
for (const field of subBlocksValue) {
|
||||
if (
|
||||
field !== null &&
|
||||
typeof field === 'object' &&
|
||||
'name' in field &&
|
||||
typeof field.name === 'string' &&
|
||||
field.name.trim() !== ''
|
||||
) {
|
||||
validFields.push({
|
||||
name: field.name.trim(),
|
||||
type: typeof field.type === 'string' ? field.type : 'string',
|
||||
})
|
||||
}
|
||||
}
|
||||
if (validFields.length > 0) {
|
||||
return validFields
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Last fallback: use extractInputFormat helper
|
||||
return extractInputFormat(blocks)
|
||||
}, [starterBlockId, subBlockValues, blocks])
|
||||
|
||||
const [selectedServer, setSelectedServer] = useState<WorkflowMcpServer | null>(null)
|
||||
const [toolName, setToolName] = useState('')
|
||||
const [toolDescription, setToolDescription] = useState('')
|
||||
const [showServerSelector, setShowServerSelector] = useState(false)
|
||||
const [showParameterSchema, setShowParameterSchema] = useState(false)
|
||||
// Track custom descriptions for each parameter
|
||||
const [parameterDescriptions, setParameterDescriptions] = useState<Record<string, string>>({})
|
||||
|
||||
const parameterSchema = useMemo(
|
||||
() => generateParameterSchema(inputFormat, parameterDescriptions),
|
||||
[inputFormat, parameterDescriptions]
|
||||
)
|
||||
|
||||
// Track tools data from each server using state instead of hooks in a loop
|
||||
const [serverToolsMap, setServerToolsMap] = useState<
|
||||
Record<string, { tool: WorkflowMcpTool | null; isLoading: boolean }>
|
||||
>({})
|
||||
|
||||
// Stable callback to handle tool data from ServerToolsQuery components
|
||||
const handleServerToolData = useCallback(
|
||||
(serverId: string, tool: WorkflowMcpTool | null, isLoading: boolean) => {
|
||||
setServerToolsMap((prev) => {
|
||||
// Only update if data has changed to prevent infinite loops
|
||||
const existing = prev[serverId]
|
||||
if (existing?.tool?.id === tool?.id && existing?.isLoading === isLoading) {
|
||||
return prev
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
[serverId]: { tool, isLoading },
|
||||
}
|
||||
})
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
// Find which servers already have this workflow as a tool and get the tool info
|
||||
const serversWithThisWorkflow = useMemo(() => {
|
||||
const result: Array<{ server: WorkflowMcpServer; tool: WorkflowMcpTool }> = []
|
||||
for (const server of servers) {
|
||||
const toolInfo = serverToolsMap[server.id]
|
||||
if (toolInfo?.tool) {
|
||||
result.push({ server, tool: toolInfo.tool })
|
||||
}
|
||||
}
|
||||
return result
|
||||
}, [servers, serverToolsMap])
|
||||
|
||||
// Check if any tools need updating
|
||||
const toolsNeedingUpdate = useMemo(() => {
|
||||
return serversWithThisWorkflow.filter(({ tool }) => hasParameterMismatch(tool, inputFormat))
|
||||
}, [serversWithThisWorkflow, inputFormat])
|
||||
|
||||
// Load existing parameter descriptions from the first deployed tool
|
||||
useEffect(() => {
|
||||
if (serversWithThisWorkflow.length > 0) {
|
||||
const existingTool = serversWithThisWorkflow[0].tool
|
||||
const schema = existingTool.parameterSchema as Record<string, unknown> | undefined
|
||||
const properties = schema?.properties as Record<string, { description?: string }> | undefined
|
||||
|
||||
if (properties) {
|
||||
const descriptions: Record<string, string> = {}
|
||||
for (const [name, prop] of Object.entries(properties)) {
|
||||
// Only use description if it differs from the field name (i.e., it's custom)
|
||||
if (
|
||||
prop.description &&
|
||||
prop.description !== name &&
|
||||
prop.description !== 'Array of file objects'
|
||||
) {
|
||||
descriptions[name] = prop.description
|
||||
}
|
||||
}
|
||||
if (Object.keys(descriptions).length > 0) {
|
||||
setParameterDescriptions(descriptions)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [serversWithThisWorkflow])
|
||||
|
||||
// Reset form when selected server changes
|
||||
useEffect(() => {
|
||||
if (selectedServer) {
|
||||
setToolName(sanitizeToolName(workflowName))
|
||||
setToolDescription(workflowDescription || `Execute ${workflowName} workflow`)
|
||||
}
|
||||
}, [selectedServer, workflowName, workflowDescription])
|
||||
|
||||
const handleAddTool = useCallback(async () => {
|
||||
if (!selectedServer || !toolName.trim()) return
|
||||
|
||||
try {
|
||||
await addToolMutation.mutateAsync({
|
||||
workspaceId,
|
||||
serverId: selectedServer.id,
|
||||
workflowId,
|
||||
toolName: toolName.trim(),
|
||||
toolDescription: toolDescription.trim() || undefined,
|
||||
parameterSchema,
|
||||
})
|
||||
|
||||
setSelectedServer(null)
|
||||
setToolName('')
|
||||
setToolDescription('')
|
||||
|
||||
// Refetch servers to update tool count
|
||||
refetchServers()
|
||||
onAddedToServer?.()
|
||||
|
||||
logger.info(`Added workflow ${workflowId} as tool to server ${selectedServer.id}`)
|
||||
} catch (error) {
|
||||
logger.error('Failed to add tool:', error)
|
||||
}
|
||||
}, [
|
||||
selectedServer,
|
||||
toolName,
|
||||
toolDescription,
|
||||
workspaceId,
|
||||
workflowId,
|
||||
parameterSchema,
|
||||
addToolMutation,
|
||||
refetchServers,
|
||||
onAddedToServer,
|
||||
])
|
||||
|
||||
const handleToolChanged = useCallback(
|
||||
(removedServerId?: string) => {
|
||||
// If a tool was removed from a specific server, clear just that entry
|
||||
// The ServerToolsQuery component will re-query and update the map
|
||||
if (removedServerId) {
|
||||
setServerToolsMap((prev) => {
|
||||
const next = { ...prev }
|
||||
delete next[removedServerId]
|
||||
return next
|
||||
})
|
||||
}
|
||||
refetchServers()
|
||||
},
|
||||
[refetchServers]
|
||||
)
|
||||
|
||||
const availableServers = useMemo(() => {
|
||||
const addedServerIds = new Set(serversWithThisWorkflow.map((s) => s.server.id))
|
||||
return servers.filter((server) => !addedServerIds.has(server.id))
|
||||
}, [servers, serversWithThisWorkflow])
|
||||
|
||||
if (!isDeployed) {
|
||||
return (
|
||||
<div className='flex h-full flex-col items-center justify-center gap-[12px] text-center'>
|
||||
<Server className='h-[32px] w-[32px] text-[var(--text-muted)]' />
|
||||
<div className='flex flex-col gap-[4px]'>
|
||||
<p className='text-[14px] text-[var(--text-primary)]'>Deploy workflow first</p>
|
||||
<p className='text-[13px] text-[var(--text-muted)]'>
|
||||
You need to deploy your workflow before adding it as an MCP tool.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (isLoadingServers) {
|
||||
return (
|
||||
<div className='flex flex-col gap-[16px]'>
|
||||
<Skeleton className='h-[60px] w-full' />
|
||||
<Skeleton className='h-[40px] w-full' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (servers.length === 0) {
|
||||
return (
|
||||
<div className='flex h-full flex-col items-center justify-center gap-[12px] text-center'>
|
||||
<Server className='h-[32px] w-[32px] text-[var(--text-muted)]' />
|
||||
<div className='flex flex-col gap-[4px]'>
|
||||
<p className='text-[14px] text-[var(--text-primary)]'>No MCP servers yet</p>
|
||||
<p className='text-[13px] text-[var(--text-muted)]'>
|
||||
Create a Workflow MCP Server in Settings → Workflow MCP Servers first.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex flex-col gap-[16px]'>
|
||||
{/* Query tools for each server using separate components to follow Rules of Hooks */}
|
||||
{servers.map((server) => (
|
||||
<ServerToolsQuery
|
||||
key={server.id}
|
||||
workspaceId={workspaceId}
|
||||
server={server}
|
||||
workflowId={workflowId}
|
||||
onData={handleServerToolData}
|
||||
/>
|
||||
))}
|
||||
|
||||
<div className='flex flex-col gap-[4px]'>
|
||||
<p className='text-[13px] text-[var(--text-secondary)]'>
|
||||
Add this workflow as an MCP tool to make it callable by external MCP clients like Cursor
|
||||
or Claude Desktop.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Update Warning */}
|
||||
{toolsNeedingUpdate.length > 0 && (
|
||||
<div className='flex items-center gap-[8px] rounded-[6px] border border-amber-500/30 bg-amber-500/10 px-[10px] py-[8px]'>
|
||||
<AlertTriangle className='h-[14px] w-[14px] flex-shrink-0 text-amber-500' />
|
||||
<p className='text-[12px] text-amber-600 dark:text-amber-400'>
|
||||
{toolsNeedingUpdate.length} server{toolsNeedingUpdate.length > 1 ? 's have' : ' has'}{' '}
|
||||
outdated tool definitions. Click "Update" on each to sync with current parameters.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Parameter Schema Preview */}
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => setShowParameterSchema(!showParameterSchema)}
|
||||
className='flex items-center gap-[6px] text-left'
|
||||
>
|
||||
{showParameterSchema ? (
|
||||
<ChevronDown className='h-[12px] w-[12px] text-[var(--text-tertiary)]' />
|
||||
) : (
|
||||
<ChevronRight className='h-[12px] w-[12px] text-[var(--text-tertiary)]' />
|
||||
)}
|
||||
<Label className='cursor-pointer text-[13px] text-[var(--text-primary)]'>
|
||||
Current Tool Parameters ({inputFormat.length})
|
||||
</Label>
|
||||
</button>
|
||||
|
||||
{showParameterSchema && (
|
||||
<div className='rounded-[6px] border bg-[var(--surface-4)] p-[12px]'>
|
||||
{inputFormat.length === 0 ? (
|
||||
<p className='text-[12px] text-[var(--text-muted)]'>
|
||||
No parameters defined. Add input fields in the Starter block to define tool
|
||||
parameters.
|
||||
</p>
|
||||
) : (
|
||||
<div className='flex flex-col gap-[12px]'>
|
||||
{inputFormat.map((field, index) => (
|
||||
<div key={index} className='flex flex-col gap-[6px]'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<span className='font-mono text-[12px] text-[var(--text-primary)]'>
|
||||
{field.name}
|
||||
</span>
|
||||
<Badge variant='outline' className='text-[10px]'>
|
||||
{field.type}
|
||||
</Badge>
|
||||
</div>
|
||||
<EmcnInput
|
||||
value={parameterDescriptions[field.name] || ''}
|
||||
onChange={(e) =>
|
||||
setParameterDescriptions((prev) => ({
|
||||
...prev,
|
||||
[field.name]: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder={`Describe what "${field.name}" is for...`}
|
||||
className='h-[32px] text-[12px]'
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<p className='text-[11px] text-[var(--text-muted)]'>
|
||||
Descriptions help MCP clients understand what each parameter is for.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Servers with this workflow */}
|
||||
{serversWithThisWorkflow.length > 0 && (
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
<Label className='text-[13px] text-[var(--text-primary)]'>
|
||||
Added to ({serversWithThisWorkflow.length})
|
||||
</Label>
|
||||
<div className='flex flex-col gap-[6px]'>
|
||||
{serversWithThisWorkflow.map(({ server, tool }) => (
|
||||
<ToolOnServer
|
||||
key={server.id}
|
||||
server={server}
|
||||
tool={tool}
|
||||
workspaceId={workspaceId}
|
||||
currentInputFormat={inputFormat}
|
||||
currentParameterSchema={parameterSchema}
|
||||
workflowDescription={workflowDescription}
|
||||
onRemoved={(serverId) => handleToolChanged(serverId)}
|
||||
onUpdated={() => handleToolChanged()}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Add to new server */}
|
||||
{availableServers.length > 0 ? (
|
||||
<>
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
<Label className='text-[13px] text-[var(--text-primary)]'>Add to Server</Label>
|
||||
<Popover open={showServerSelector} onOpenChange={setShowServerSelector}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant='default'
|
||||
className='h-[36px] w-full justify-between border bg-[var(--surface-3)]'
|
||||
>
|
||||
<span className={cn(!selectedServer && 'text-[var(--text-muted)]')}>
|
||||
{selectedServer?.name || 'Choose a server...'}
|
||||
</span>
|
||||
<ChevronDown className='h-[14px] w-[14px] text-[var(--text-tertiary)]' />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
side='bottom'
|
||||
align='start'
|
||||
sideOffset={4}
|
||||
className='w-[var(--radix-popover-trigger-width)]'
|
||||
border
|
||||
>
|
||||
{availableServers.map((server) => (
|
||||
<PopoverItem
|
||||
key={server.id}
|
||||
onClick={() => {
|
||||
setSelectedServer(server)
|
||||
setShowServerSelector(false)
|
||||
}}
|
||||
>
|
||||
<Server className='mr-[8px] h-[14px] w-[14px] text-[var(--text-tertiary)]' />
|
||||
<span>{server.name}</span>
|
||||
{server.isPublished && (
|
||||
<Badge variant='outline' className='ml-auto text-[10px]'>
|
||||
Published
|
||||
</Badge>
|
||||
)}
|
||||
</PopoverItem>
|
||||
))}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
{selectedServer && (
|
||||
<>
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
<Label className='text-[13px] text-[var(--text-primary)]'>Tool Name</Label>
|
||||
<EmcnInput
|
||||
value={toolName}
|
||||
onChange={(e) => setToolName(e.target.value)}
|
||||
placeholder='e.g., book_flight'
|
||||
className='h-[36px]'
|
||||
/>
|
||||
<p className='text-[11px] text-[var(--text-muted)]'>
|
||||
Use lowercase letters, numbers, and underscores only.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
<Label className='text-[13px] text-[var(--text-primary)]'>Description</Label>
|
||||
<EmcnInput
|
||||
value={toolDescription}
|
||||
onChange={(e) => setToolDescription(e.target.value)}
|
||||
placeholder='Describe what this tool does...'
|
||||
className='h-[36px]'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={handleAddTool}
|
||||
disabled={addToolMutation.isPending || !toolName.trim()}
|
||||
className='!bg-[var(--brand-tertiary-2)] !text-[var(--text-inverse)] hover:!bg-[var(--brand-tertiary-2)]/90'
|
||||
>
|
||||
<Plus className='mr-[6px] h-[14px] w-[14px]' />
|
||||
{addToolMutation.isPending ? 'Adding...' : 'Add to Server'}
|
||||
</Button>
|
||||
|
||||
{addToolMutation.isError && (
|
||||
<p className='text-[12px] text-[var(--text-error)]'>
|
||||
{addToolMutation.error?.message || 'Failed to add tool'}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : serversWithThisWorkflow.length > 0 ? (
|
||||
<p className='text-[13px] text-[var(--text-muted)]'>
|
||||
This workflow has been added to all available servers.
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -24,7 +24,6 @@ import type { WorkflowState } from '@/stores/workflows/workflow/types'
|
||||
import { ApiDeploy } from './components/api/api'
|
||||
import { ChatDeploy, type ExistingChat } from './components/chat/chat'
|
||||
import { GeneralDeploy } from './components/general/general'
|
||||
import { McpToolDeploy } from './components/mcp-tool/mcp-tool'
|
||||
import { TemplateDeploy } from './components/template/template'
|
||||
|
||||
const logger = createLogger('DeployModal')
|
||||
@@ -50,7 +49,7 @@ interface WorkflowDeploymentInfo {
|
||||
needsRedeployment: boolean
|
||||
}
|
||||
|
||||
type TabView = 'general' | 'api' | 'chat' | 'template' | 'mcp-tool'
|
||||
type TabView = 'general' | 'api' | 'chat' | 'template'
|
||||
|
||||
export function DeployModal({
|
||||
open,
|
||||
@@ -553,7 +552,6 @@ export function DeployModal({
|
||||
<ModalTabsTrigger value='api'>API</ModalTabsTrigger>
|
||||
<ModalTabsTrigger value='chat'>Chat</ModalTabsTrigger>
|
||||
<ModalTabsTrigger value='template'>Template</ModalTabsTrigger>
|
||||
<ModalTabsTrigger value='mcp-tool'>MCP Tool</ModalTabsTrigger>
|
||||
</ModalTabsList>
|
||||
|
||||
<ModalBody className='min-h-0 flex-1'>
|
||||
@@ -612,17 +610,6 @@ export function DeployModal({
|
||||
/>
|
||||
)}
|
||||
</ModalTabsContent>
|
||||
|
||||
<ModalTabsContent value='mcp-tool'>
|
||||
{workflowId && (
|
||||
<McpToolDeploy
|
||||
workflowId={workflowId}
|
||||
workflowName={workflowMetadata?.name || 'Workflow'}
|
||||
workflowDescription={workflowMetadata?.description}
|
||||
isDeployed={isDeployed}
|
||||
/>
|
||||
)}
|
||||
</ModalTabsContent>
|
||||
</ModalBody>
|
||||
</ModalTabs>
|
||||
|
||||
|
||||
@@ -9,51 +9,30 @@ import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/c
|
||||
import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential'
|
||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import type { SelectorContext, SelectorKey } from '@/hooks/selectors/types'
|
||||
import type { SelectorContext } from '@/hooks/selectors/types'
|
||||
|
||||
type SlackSelectorType = 'channel-selector' | 'user-selector'
|
||||
|
||||
const SELECTOR_CONFIG: Record<
|
||||
SlackSelectorType,
|
||||
{ selectorKey: SelectorKey; placeholder: string; label: string }
|
||||
> = {
|
||||
'channel-selector': {
|
||||
selectorKey: 'slack.channels',
|
||||
placeholder: 'Select Slack channel',
|
||||
label: 'Channel',
|
||||
},
|
||||
'user-selector': {
|
||||
selectorKey: 'slack.users',
|
||||
placeholder: 'Select Slack user',
|
||||
label: 'User',
|
||||
},
|
||||
}
|
||||
|
||||
interface SlackSelectorInputProps {
|
||||
interface ChannelSelectorInputProps {
|
||||
blockId: string
|
||||
subBlock: SubBlockConfig
|
||||
disabled?: boolean
|
||||
onSelect?: (value: string) => void
|
||||
onChannelSelect?: (channelId: string) => void
|
||||
isPreview?: boolean
|
||||
previewValue?: any | null
|
||||
previewContextValues?: Record<string, any>
|
||||
}
|
||||
|
||||
export function SlackSelectorInput({
|
||||
export function ChannelSelectorInput({
|
||||
blockId,
|
||||
subBlock,
|
||||
disabled = false,
|
||||
onSelect,
|
||||
onChannelSelect,
|
||||
isPreview = false,
|
||||
previewValue,
|
||||
previewContextValues,
|
||||
}: SlackSelectorInputProps) {
|
||||
const selectorType = subBlock.type as SlackSelectorType
|
||||
const config = SELECTOR_CONFIG[selectorType]
|
||||
|
||||
}: ChannelSelectorInputProps) {
|
||||
const params = useParams()
|
||||
const workflowIdFromUrl = (params?.workflowId as string) || ''
|
||||
const [storeValue] = useSubBlockValue(blockId, subBlock.id)
|
||||
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlock.id)
|
||||
const [authMethod] = useSubBlockValue(blockId, 'authMethod')
|
||||
const [botToken] = useSubBlockValue(blockId, 'botToken')
|
||||
const [connectedCredential] = useSubBlockValue(blockId, 'credential')
|
||||
@@ -61,32 +40,37 @@ export function SlackSelectorInput({
|
||||
const effectiveAuthMethod = previewContextValues?.authMethod ?? authMethod
|
||||
const effectiveBotToken = previewContextValues?.botToken ?? botToken
|
||||
const effectiveCredential = previewContextValues?.credential ?? connectedCredential
|
||||
const [_selectedValue, setSelectedValue] = useState<string | null>(null)
|
||||
const [_channelInfo, setChannelInfo] = useState<string | null>(null)
|
||||
|
||||
// Use serviceId to identify the service and derive providerId for credential lookup
|
||||
const serviceId = subBlock.serviceId || ''
|
||||
const effectiveProviderId = useMemo(() => getProviderIdFromServiceId(serviceId), [serviceId])
|
||||
const isSlack = serviceId === 'slack'
|
||||
|
||||
// Central dependsOn gating
|
||||
const { finalDisabled, dependsOn } = useDependsOnGate(blockId, subBlock, {
|
||||
disabled,
|
||||
isPreview,
|
||||
previewContextValues,
|
||||
})
|
||||
|
||||
// Choose credential strictly based on auth method - use effective values
|
||||
const credential: string =
|
||||
(effectiveAuthMethod as string) === 'bot_token'
|
||||
? (effectiveBotToken as string) || ''
|
||||
: (effectiveCredential as string) || ''
|
||||
|
||||
// Determine if connected OAuth credential is foreign (not applicable for bot tokens)
|
||||
const { isForeignCredential } = useForeignCredential(
|
||||
effectiveProviderId,
|
||||
(effectiveAuthMethod as string) === 'bot_token' ? '' : (effectiveCredential as string) || ''
|
||||
)
|
||||
|
||||
// Get the current value from the store or prop value if in preview mode (same pattern as file-selector)
|
||||
useEffect(() => {
|
||||
const val = isPreview && previewValue !== undefined ? previewValue : storeValue
|
||||
if (typeof val === 'string') {
|
||||
setSelectedValue(val)
|
||||
setChannelInfo(val)
|
||||
}
|
||||
}, [isPreview, previewValue, storeValue])
|
||||
|
||||
@@ -107,14 +91,11 @@ export function SlackSelectorInput({
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<div className='w-full rounded border p-4 text-center text-muted-foreground text-sm'>
|
||||
{config.label} selector not supported for service: {serviceId || 'unknown'}
|
||||
Channel selector not supported for service: {serviceId || 'unknown'}
|
||||
</div>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side='top'>
|
||||
<p>
|
||||
This {config.label.toLowerCase()} selector is not yet implemented for{' '}
|
||||
{serviceId || 'unknown'}
|
||||
</p>
|
||||
<p>This channel selector is not yet implemented for {serviceId || 'unknown'}</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
)
|
||||
@@ -127,16 +108,16 @@ export function SlackSelectorInput({
|
||||
<SelectorCombobox
|
||||
blockId={blockId}
|
||||
subBlock={subBlock}
|
||||
selectorKey={config.selectorKey}
|
||||
selectorKey='slack.channels'
|
||||
selectorContext={context}
|
||||
disabled={finalDisabled || shouldForceDisable || isForeignCredential}
|
||||
isPreview={isPreview}
|
||||
previewValue={previewValue ?? null}
|
||||
placeholder={subBlock.placeholder || config.placeholder}
|
||||
placeholder={subBlock.placeholder || 'Select Slack channel'}
|
||||
onOptionChange={(value) => {
|
||||
setSelectedValue(value)
|
||||
setChannelInfo(value)
|
||||
if (!isPreview) {
|
||||
onSelect?.(value)
|
||||
onChannelSelect?.(value)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -179,9 +179,6 @@ const SCOPE_DESCRIPTIONS: Record<string, string> = {
|
||||
'groups:history': 'Read private messages',
|
||||
'chat:write': 'Send messages',
|
||||
'chat:write.public': 'Post to public channels',
|
||||
'im:write': 'Send direct messages',
|
||||
'im:history': 'Read direct message history',
|
||||
'im:read': 'View direct message channels',
|
||||
'users:read': 'View workspace users',
|
||||
'files:write': 'Upload files',
|
||||
'files:read': 'Download and read files',
|
||||
@@ -347,13 +344,6 @@ export function OAuthRequiredModal({
|
||||
return
|
||||
}
|
||||
|
||||
if (providerId === 'servicenow') {
|
||||
// Pass the current URL so we can redirect back after OAuth
|
||||
const returnUrl = encodeURIComponent(window.location.href)
|
||||
window.location.href = `/api/auth/servicenow/authorize?returnUrl=${returnUrl}`
|
||||
return
|
||||
}
|
||||
|
||||
await client.oauth2.link({
|
||||
providerId,
|
||||
callbackURL: window.location.href,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export { ChannelSelectorInput } from './channel-selector/channel-selector-input'
|
||||
export { CheckboxList } from './checkbox-list/checkbox-list'
|
||||
export { Code } from './code/code'
|
||||
export { ComboBox } from './combobox/combobox'
|
||||
@@ -23,7 +24,6 @@ export { ProjectSelectorInput } from './project-selector/project-selector-input'
|
||||
export { ResponseFormat } from './response/response-format'
|
||||
export { ScheduleSave } from './schedule-save/schedule-save'
|
||||
export { ShortInput } from './short-input/short-input'
|
||||
export { SlackSelectorInput } from './slack-selector/slack-selector-input'
|
||||
export { SliderInput } from './slider-input/slider-input'
|
||||
export { InputFormat } from './starter/input-format'
|
||||
export { SubBlockInputController } from './sub-block-input-controller'
|
||||
|
||||
@@ -85,11 +85,11 @@ export function ShortInput({
|
||||
const persistSubBlockValueRef = useRef<(value: string) => void>(() => {})
|
||||
|
||||
const justPastedRef = useRef(false)
|
||||
|
||||
const webhookManagement = useWebhookManagement({
|
||||
blockId,
|
||||
triggerId: undefined,
|
||||
isPreview,
|
||||
useWebhookUrl,
|
||||
})
|
||||
|
||||
const wandHook = useWand({
|
||||
|
||||
@@ -91,7 +91,8 @@ export function FieldFormat({
|
||||
placeholder = 'fieldName',
|
||||
showType = true,
|
||||
showValue = false,
|
||||
valuePlaceholder = 'Enter default value',
|
||||
valuePlaceholder = 'Enter test value',
|
||||
config,
|
||||
}: FieldFormatProps) {
|
||||
const [storeValue, setStoreValue] = useSubBlockValue<Field[]>(blockId, subBlockId)
|
||||
const valueInputRefs = useRef<Record<string, HTMLInputElement | HTMLTextAreaElement>>({})
|
||||
@@ -453,6 +454,7 @@ export function FieldFormat({
|
||||
)
|
||||
}
|
||||
|
||||
// Export specific components for backward compatibility
|
||||
export function InputFormat(props: Omit<FieldFormatProps, 'title' | 'placeholder'>) {
|
||||
return <FieldFormat {...props} title='Input' placeholder='firstName' />
|
||||
}
|
||||
|
||||
@@ -13,10 +13,6 @@ import {
|
||||
} from '@/components/emcn'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import {
|
||||
createEnvVarPattern,
|
||||
createWorkflowVariablePattern,
|
||||
} from '@/executor/utils/reference-validation'
|
||||
|
||||
interface CodeEditorProps {
|
||||
value: string
|
||||
@@ -136,28 +132,15 @@ export function CodeEditor({
|
||||
return highlight(code, languages[language], language)
|
||||
}
|
||||
|
||||
const escapeHtml = (text: string) =>
|
||||
text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
|
||||
const placeholders: Array<{
|
||||
placeholder: string
|
||||
original: string
|
||||
type: 'env' | 'param' | 'variable'
|
||||
}> = []
|
||||
const placeholders: Array<{ placeholder: string; original: string; type: 'env' | 'param' }> = []
|
||||
let processedCode = code
|
||||
|
||||
processedCode = processedCode.replace(createEnvVarPattern(), (match) => {
|
||||
processedCode = processedCode.replace(/\{\{([^}]+)\}\}/g, (match) => {
|
||||
const placeholder = `__ENV_VAR_${placeholders.length}__`
|
||||
placeholders.push({ placeholder, original: match, type: 'env' })
|
||||
return placeholder
|
||||
})
|
||||
|
||||
processedCode = processedCode.replace(createWorkflowVariablePattern(), (match) => {
|
||||
const placeholder = `__VARIABLE_${placeholders.length}__`
|
||||
placeholders.push({ placeholder, original: match, type: 'variable' })
|
||||
return placeholder
|
||||
})
|
||||
|
||||
if (schemaParameters.length > 0) {
|
||||
schemaParameters.forEach((param) => {
|
||||
const escapedName = param.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
@@ -173,10 +156,9 @@ export function CodeEditor({
|
||||
let highlighted = highlight(processedCode, languages[language], language)
|
||||
|
||||
placeholders.forEach(({ placeholder, original, type }) => {
|
||||
const escapedOriginal = type === 'variable' ? escapeHtml(original) : original
|
||||
const replacement =
|
||||
type === 'env' || type === 'variable'
|
||||
? `<span style="color: #34B5FF;">${escapedOriginal}</span>`
|
||||
type === 'env'
|
||||
? `<span style="color: #34B5FF;">${original}</span>`
|
||||
: `<span style="color: #34B5FF; font-weight: 500;">${original}</span>`
|
||||
|
||||
highlighted = highlighted.replace(placeholder, replacement)
|
||||
|
||||
@@ -18,18 +18,12 @@ interface McpTool {
|
||||
inputSchema?: any
|
||||
}
|
||||
|
||||
interface McpServer {
|
||||
id: string
|
||||
url?: string
|
||||
}
|
||||
|
||||
interface StoredTool {
|
||||
type: 'mcp'
|
||||
title: string
|
||||
toolId: string
|
||||
params: {
|
||||
serverId: string
|
||||
serverUrl?: string
|
||||
toolName: string
|
||||
serverName: string
|
||||
}
|
||||
@@ -40,7 +34,6 @@ interface StoredTool {
|
||||
|
||||
interface McpToolsListProps {
|
||||
mcpTools: McpTool[]
|
||||
mcpServers?: McpServer[]
|
||||
searchQuery: string
|
||||
customFilter: (name: string, query: string) => number
|
||||
onToolSelect: (tool: StoredTool) => void
|
||||
@@ -52,7 +45,6 @@ interface McpToolsListProps {
|
||||
*/
|
||||
export function McpToolsList({
|
||||
mcpTools,
|
||||
mcpServers = [],
|
||||
searchQuery,
|
||||
customFilter,
|
||||
onToolSelect,
|
||||
@@ -67,48 +59,44 @@ export function McpToolsList({
|
||||
return (
|
||||
<>
|
||||
<PopoverSection>MCP Tools</PopoverSection>
|
||||
{filteredTools.map((mcpTool) => {
|
||||
const server = mcpServers.find((s) => s.id === mcpTool.serverId)
|
||||
return (
|
||||
<ToolCommand.Item
|
||||
key={mcpTool.id}
|
||||
value={mcpTool.name}
|
||||
onSelect={() => {
|
||||
if (disabled) return
|
||||
{filteredTools.map((mcpTool) => (
|
||||
<ToolCommand.Item
|
||||
key={mcpTool.id}
|
||||
value={mcpTool.name}
|
||||
onSelect={() => {
|
||||
if (disabled) return
|
||||
|
||||
const newTool: StoredTool = {
|
||||
type: 'mcp',
|
||||
title: mcpTool.name,
|
||||
toolId: mcpTool.id,
|
||||
params: {
|
||||
serverId: mcpTool.serverId,
|
||||
serverUrl: server?.url,
|
||||
toolName: mcpTool.name,
|
||||
serverName: mcpTool.serverName,
|
||||
},
|
||||
isExpanded: true,
|
||||
usageControl: 'auto',
|
||||
schema: {
|
||||
...mcpTool.inputSchema,
|
||||
description: mcpTool.description,
|
||||
},
|
||||
}
|
||||
const newTool: StoredTool = {
|
||||
type: 'mcp',
|
||||
title: mcpTool.name,
|
||||
toolId: mcpTool.id,
|
||||
params: {
|
||||
serverId: mcpTool.serverId,
|
||||
toolName: mcpTool.name,
|
||||
serverName: mcpTool.serverName,
|
||||
},
|
||||
isExpanded: true,
|
||||
usageControl: 'auto',
|
||||
schema: {
|
||||
...mcpTool.inputSchema,
|
||||
description: mcpTool.description,
|
||||
},
|
||||
}
|
||||
|
||||
onToolSelect(newTool)
|
||||
}}
|
||||
onToolSelect(newTool)
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className='flex h-[15px] w-[15px] flex-shrink-0 items-center justify-center rounded'
|
||||
style={{ background: mcpTool.bgColor }}
|
||||
>
|
||||
<div
|
||||
className='flex h-[15px] w-[15px] flex-shrink-0 items-center justify-center rounded'
|
||||
style={{ background: mcpTool.bgColor }}
|
||||
>
|
||||
<IconComponent icon={mcpTool.icon} className='h-[11px] w-[11px] text-white' />
|
||||
</div>
|
||||
<span className='truncate' title={`${mcpTool.name} (${mcpTool.serverName})`}>
|
||||
{mcpTool.name}
|
||||
</span>
|
||||
</ToolCommand.Item>
|
||||
)
|
||||
})}
|
||||
<IconComponent icon={mcpTool.icon} className='h-[11px] w-[11px] text-white' />
|
||||
</div>
|
||||
<span className='truncate' title={`${mcpTool.name} (${mcpTool.serverName})`}>
|
||||
{mcpTool.name}
|
||||
</span>
|
||||
</ToolCommand.Item>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useQuery } from '@tanstack/react-query'
|
||||
import { Loader2, PlusIcon, WrenchIcon, XIcon } from 'lucide-react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import {
|
||||
Badge,
|
||||
Combobox,
|
||||
Popover,
|
||||
PopoverContent,
|
||||
@@ -13,7 +12,6 @@ import {
|
||||
PopoverSearch,
|
||||
PopoverSection,
|
||||
PopoverTrigger,
|
||||
Tooltip,
|
||||
} from '@/components/emcn'
|
||||
import { McpIcon } from '@/components/icons'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
@@ -26,6 +24,7 @@ import {
|
||||
type OAuthService,
|
||||
} from '@/lib/oauth/oauth'
|
||||
import {
|
||||
ChannelSelectorInput,
|
||||
CheckboxList,
|
||||
Code,
|
||||
ComboBox,
|
||||
@@ -34,7 +33,6 @@ import {
|
||||
LongInput,
|
||||
ProjectSelectorInput,
|
||||
ShortInput,
|
||||
SlackSelectorInput,
|
||||
SliderInput,
|
||||
Table,
|
||||
TimeInput,
|
||||
@@ -57,11 +55,9 @@ import {
|
||||
type CustomTool as CustomToolDefinition,
|
||||
useCustomTools,
|
||||
} from '@/hooks/queries/custom-tools'
|
||||
import { useMcpServers } from '@/hooks/queries/mcp'
|
||||
import { useWorkflows } from '@/hooks/queries/workflows'
|
||||
import { useMcpTools } from '@/hooks/use-mcp-tools'
|
||||
import { getProviderFromModel, supportsToolUsageControl } from '@/providers/utils'
|
||||
import { useSettingsModalStore } from '@/stores/settings-modal/store'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import {
|
||||
formatParameterLabel,
|
||||
@@ -524,7 +520,7 @@ function ChannelSelectorSyncWrapper({
|
||||
}) {
|
||||
return (
|
||||
<GenericSyncWrapper blockId={blockId} paramId={paramId} value={value} onChange={onChange}>
|
||||
<SlackSelectorInput
|
||||
<ChannelSelectorInput
|
||||
blockId={blockId}
|
||||
subBlock={{
|
||||
id: paramId,
|
||||
@@ -534,7 +530,7 @@ function ChannelSelectorSyncWrapper({
|
||||
placeholder: uiComponent.placeholder,
|
||||
dependsOn: uiComponent.dependsOn,
|
||||
}}
|
||||
onSelect={onChange}
|
||||
onChannelSelect={onChange}
|
||||
disabled={disabled}
|
||||
previewContextValues={previewContextValues}
|
||||
/>
|
||||
@@ -806,66 +802,6 @@ export function ToolInput({
|
||||
refreshTools,
|
||||
} = useMcpTools(workspaceId)
|
||||
|
||||
const { data: mcpServers = [], isLoading: mcpServersLoading } = useMcpServers(workspaceId)
|
||||
const openSettingsModal = useSettingsModalStore((state) => state.openModal)
|
||||
const mcpDataLoading = mcpLoading || mcpServersLoading
|
||||
|
||||
/**
|
||||
* Returns issue info for an MCP tool using shared validation logic.
|
||||
*/
|
||||
const getMcpToolIssue = useCallback(
|
||||
(tool: StoredTool) => {
|
||||
if (tool.type !== 'mcp') return null
|
||||
|
||||
const { getMcpToolIssue: validateTool } = require('@/lib/mcp/tool-validation')
|
||||
|
||||
return validateTool(
|
||||
{
|
||||
serverId: tool.params?.serverId as string,
|
||||
serverUrl: tool.params?.serverUrl as string | undefined,
|
||||
toolName: tool.params?.toolName as string,
|
||||
schema: tool.schema,
|
||||
},
|
||||
mcpServers.map((s) => ({
|
||||
id: s.id,
|
||||
url: s.url,
|
||||
connectionStatus: s.connectionStatus,
|
||||
lastError: s.lastError,
|
||||
})),
|
||||
mcpTools.map((t) => ({
|
||||
serverId: t.serverId,
|
||||
name: t.name,
|
||||
inputSchema: t.inputSchema,
|
||||
}))
|
||||
)
|
||||
},
|
||||
[mcpTools, mcpServers]
|
||||
)
|
||||
|
||||
const isMcpToolUnavailable = useCallback(
|
||||
(tool: StoredTool): boolean => {
|
||||
const { isToolUnavailable } = require('@/lib/mcp/tool-validation')
|
||||
return isToolUnavailable(getMcpToolIssue(tool))
|
||||
},
|
||||
[getMcpToolIssue]
|
||||
)
|
||||
|
||||
const hasMcpToolIssue = useCallback(
|
||||
(tool: StoredTool): boolean => {
|
||||
return getMcpToolIssue(tool) !== null
|
||||
},
|
||||
[getMcpToolIssue]
|
||||
)
|
||||
|
||||
// Filter out MCP tools from unavailable servers for the dropdown
|
||||
const availableMcpTools = useMemo(() => {
|
||||
return mcpTools.filter((mcpTool) => {
|
||||
const server = mcpServers.find((s) => s.id === mcpTool.serverId)
|
||||
// Only include tools from connected servers
|
||||
return server && server.connectionStatus === 'connected'
|
||||
})
|
||||
}, [mcpTools, mcpServers])
|
||||
|
||||
// Reset search query when popover opens
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
@@ -1913,10 +1849,9 @@ export function ToolInput({
|
||||
)
|
||||
})()}
|
||||
|
||||
{/* Display MCP tools (only from available servers) */}
|
||||
{/* Display MCP tools */}
|
||||
<McpToolsList
|
||||
mcpTools={availableMcpTools}
|
||||
mcpServers={mcpServers}
|
||||
mcpTools={mcpTools}
|
||||
searchQuery={searchQuery || ''}
|
||||
customFilter={customFilter}
|
||||
onToolSelect={handleMcpToolSelect}
|
||||
@@ -2105,46 +2040,9 @@ export function ToolInput({
|
||||
<span className='truncate font-medium text-[13px] text-[var(--text-primary)]'>
|
||||
{isCustomTool ? customToolTitle : tool.title}
|
||||
</span>
|
||||
{isMcpTool &&
|
||||
!mcpDataLoading &&
|
||||
(() => {
|
||||
const issue = getMcpToolIssue(tool)
|
||||
if (!issue) return null
|
||||
const { getIssueBadgeLabel } = require('@/lib/mcp/tool-validation')
|
||||
const serverId = tool.params?.serverId
|
||||
return (
|
||||
<div
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
openSettingsModal({ section: 'mcp', mcpServerId: serverId })
|
||||
}}
|
||||
>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Badge
|
||||
variant='outline'
|
||||
className='cursor-pointer transition-colors hover:bg-[var(--warning)]/10'
|
||||
style={{
|
||||
borderColor: 'var(--warning)',
|
||||
color: 'var(--warning)',
|
||||
}}
|
||||
>
|
||||
{getIssueBadgeLabel(issue)}
|
||||
</Badge>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<span className='text-sm'>
|
||||
{issue.message} · Click to open settings
|
||||
</span>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
<div className='flex flex-shrink-0 items-center gap-[8px]'>
|
||||
{supportsToolControl && !(isMcpTool && isMcpToolUnavailable(tool)) && (
|
||||
{supportsToolControl && (
|
||||
<Popover
|
||||
open={usageControlPopoverIndex === toolIndex}
|
||||
onOpenChange={(open) =>
|
||||
@@ -2488,10 +2386,9 @@ export function ToolInput({
|
||||
)
|
||||
})()}
|
||||
|
||||
{/* Display MCP tools (only from available servers) */}
|
||||
{/* Display MCP tools */}
|
||||
<McpToolsList
|
||||
mcpTools={availableMcpTools}
|
||||
mcpServers={mcpServers}
|
||||
mcpTools={mcpTools}
|
||||
searchQuery={searchQuery || ''}
|
||||
customFilter={customFilter}
|
||||
onToolSelect={handleMcpToolSelect}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Button } from '@/components/ui/button'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import type { FieldDiffStatus } from '@/lib/workflows/diff/types'
|
||||
import {
|
||||
ChannelSelectorInput,
|
||||
CheckboxList,
|
||||
Code,
|
||||
ComboBox,
|
||||
@@ -31,7 +32,6 @@ import {
|
||||
ResponseFormat,
|
||||
ScheduleSave,
|
||||
ShortInput,
|
||||
SlackSelectorInput,
|
||||
SliderInput,
|
||||
Switch,
|
||||
Table,
|
||||
@@ -157,7 +157,6 @@ const renderLabel = (
|
||||
isWandEnabled: boolean
|
||||
isPreview: boolean
|
||||
isStreaming: boolean
|
||||
disabled: boolean
|
||||
onSearchClick: () => void
|
||||
onSearchBlur: () => void
|
||||
onSearchChange: (value: string) => void
|
||||
@@ -176,7 +175,6 @@ const renderLabel = (
|
||||
isWandEnabled,
|
||||
isPreview,
|
||||
isStreaming,
|
||||
disabled,
|
||||
onSearchClick,
|
||||
onSearchBlur,
|
||||
onSearchChange,
|
||||
@@ -210,7 +208,7 @@ const renderLabel = (
|
||||
</div>
|
||||
|
||||
{/* Wand inline prompt */}
|
||||
{isWandEnabled && !isPreview && !disabled && (
|
||||
{isWandEnabled && !isPreview && (
|
||||
<div className='flex min-w-0 flex-1 items-center justify-end pr-[4px]'>
|
||||
{!isSearchActive ? (
|
||||
<Button
|
||||
@@ -732,9 +730,8 @@ function SubBlockComponent({
|
||||
)
|
||||
|
||||
case 'channel-selector':
|
||||
case 'user-selector':
|
||||
return (
|
||||
<SlackSelectorInput
|
||||
<ChannelSelectorInput
|
||||
blockId={blockId}
|
||||
subBlock={config}
|
||||
disabled={isDisabled}
|
||||
@@ -827,7 +824,6 @@ function SubBlockComponent({
|
||||
isWandEnabled,
|
||||
isPreview,
|
||||
isStreaming: wandControlRef.current?.isWandStreaming ?? false,
|
||||
disabled: isDisabled,
|
||||
onSearchClick: handleSearchClick,
|
||||
onSearchBlur: handleSearchBlur,
|
||||
onSearchChange: handleSearchChange,
|
||||
|
||||
@@ -26,7 +26,7 @@ const SUBFLOW_CONFIG = {
|
||||
},
|
||||
typeKey: 'loopType' as const,
|
||||
storeKey: 'loops' as const,
|
||||
maxIterations: 1000,
|
||||
maxIterations: 100,
|
||||
configKeys: {
|
||||
iterations: 'iterations' as const,
|
||||
items: 'forEachItems' as const,
|
||||
|
||||
@@ -1741,7 +1741,7 @@ export function Terminal() {
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
<div className={clsx('flex-1 overflow-y-auto', !wrapText && 'overflow-x-auto')}>
|
||||
<div className='flex-1 overflow-x-auto overflow-y-auto'>
|
||||
{shouldShowCodeDisplay ? (
|
||||
<OutputCodeContent
|
||||
code={selectedEntry.input.code}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { memo, useMemo } from 'react'
|
||||
import { X } from 'lucide-react'
|
||||
import { BaseEdge, EdgeLabelRenderer, type EdgeProps, getSmoothStepPath } from 'reactflow'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import type { EdgeDiffStatus } from '@/lib/workflows/diff/types'
|
||||
import { useExecutionStore } from '@/stores/execution/store'
|
||||
import { useWorkflowDiffStore } from '@/stores/workflow-diff'
|
||||
@@ -11,7 +9,7 @@ interface WorkflowEdgeProps extends EdgeProps {
|
||||
targetHandle?: string | null
|
||||
}
|
||||
|
||||
const WorkflowEdgeComponent = ({
|
||||
export const WorkflowEdge = ({
|
||||
id,
|
||||
sourceX,
|
||||
sourceY,
|
||||
@@ -43,64 +41,65 @@ const WorkflowEdgeComponent = ({
|
||||
const isInsideLoop = data?.isInsideLoop ?? false
|
||||
const parentLoopId = data?.parentLoopId
|
||||
|
||||
// Combined store subscription to reduce subscription overhead
|
||||
const { diffAnalysis, isShowingDiff, isDiffReady } = useWorkflowDiffStore(
|
||||
useShallow((state) => ({
|
||||
diffAnalysis: state.diffAnalysis,
|
||||
isShowingDiff: state.isShowingDiff,
|
||||
isDiffReady: state.isDiffReady,
|
||||
}))
|
||||
)
|
||||
const diffAnalysis = useWorkflowDiffStore((state) => state.diffAnalysis)
|
||||
const isShowingDiff = useWorkflowDiffStore((state) => state.isShowingDiff)
|
||||
const isDiffReady = useWorkflowDiffStore((state) => state.isDiffReady)
|
||||
const lastRunEdges = useExecutionStore((state) => state.lastRunEdges)
|
||||
|
||||
const generateEdgeIdentity = (
|
||||
sourceId: string,
|
||||
targetId: string,
|
||||
sourceHandle?: string | null,
|
||||
targetHandle?: string | null
|
||||
): string => {
|
||||
const actualSourceHandle = sourceHandle || 'source'
|
||||
const actualTargetHandle = targetHandle || 'target'
|
||||
return `${sourceId}-${actualSourceHandle}-${targetId}-${actualTargetHandle}`
|
||||
}
|
||||
|
||||
const edgeIdentifier = generateEdgeIdentity(source, target, sourceHandle, targetHandle)
|
||||
|
||||
let edgeDiffStatus: EdgeDiffStatus = null
|
||||
|
||||
if (data?.isDeleted) {
|
||||
edgeDiffStatus = 'deleted'
|
||||
} else if (diffAnalysis?.edge_diff && edgeIdentifier && isDiffReady) {
|
||||
if (isShowingDiff) {
|
||||
if (diffAnalysis.edge_diff.new_edges.includes(edgeIdentifier)) {
|
||||
edgeDiffStatus = 'new'
|
||||
} else if (diffAnalysis.edge_diff.unchanged_edges.includes(edgeIdentifier)) {
|
||||
edgeDiffStatus = 'unchanged'
|
||||
}
|
||||
} else {
|
||||
if (diffAnalysis.edge_diff.deleted_edges.includes(edgeIdentifier)) {
|
||||
edgeDiffStatus = 'deleted'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const dataSourceHandle = (data as { sourceHandle?: string } | undefined)?.sourceHandle
|
||||
const isErrorEdge = (sourceHandle ?? dataSourceHandle) === 'error'
|
||||
|
||||
// Check if this edge was traversed during last execution
|
||||
const edgeRunStatus = lastRunEdges.get(id)
|
||||
|
||||
// Memoize diff status calculation to avoid recomputing on every render
|
||||
const edgeDiffStatus = useMemo((): EdgeDiffStatus => {
|
||||
if (data?.isDeleted) return 'deleted'
|
||||
if (!diffAnalysis?.edge_diff || !isDiffReady) return null
|
||||
const getEdgeColor = () => {
|
||||
if (edgeDiffStatus === 'deleted') return 'var(--text-error)'
|
||||
if (isErrorEdge) return 'var(--text-error)'
|
||||
if (edgeDiffStatus === 'new') return 'var(--brand-tertiary)'
|
||||
// Show run path status if edge was traversed
|
||||
if (edgeRunStatus === 'success') return 'var(--border-success)'
|
||||
if (edgeRunStatus === 'error') return 'var(--text-error)'
|
||||
return 'var(--surface-12)'
|
||||
}
|
||||
|
||||
const actualSourceHandle = sourceHandle || 'source'
|
||||
const actualTargetHandle = targetHandle || 'target'
|
||||
const edgeIdentifier = `${source}-${actualSourceHandle}-${target}-${actualTargetHandle}`
|
||||
|
||||
if (isShowingDiff) {
|
||||
if (diffAnalysis.edge_diff.new_edges.includes(edgeIdentifier)) return 'new'
|
||||
if (diffAnalysis.edge_diff.unchanged_edges.includes(edgeIdentifier)) return 'unchanged'
|
||||
} else {
|
||||
if (diffAnalysis.edge_diff.deleted_edges.includes(edgeIdentifier)) return 'deleted'
|
||||
}
|
||||
return null
|
||||
}, [
|
||||
data?.isDeleted,
|
||||
diffAnalysis,
|
||||
isDiffReady,
|
||||
isShowingDiff,
|
||||
source,
|
||||
target,
|
||||
sourceHandle,
|
||||
targetHandle,
|
||||
])
|
||||
|
||||
// Memoize edge style to prevent object recreation
|
||||
const edgeStyle = useMemo(() => {
|
||||
let color = 'var(--surface-12)'
|
||||
if (edgeDiffStatus === 'deleted') color = 'var(--text-error)'
|
||||
else if (isErrorEdge) color = 'var(--text-error)'
|
||||
else if (edgeDiffStatus === 'new') color = 'var(--brand-tertiary)'
|
||||
else if (edgeRunStatus === 'success') color = 'var(--border-success)'
|
||||
else if (edgeRunStatus === 'error') color = 'var(--text-error)'
|
||||
|
||||
return {
|
||||
...(style ?? {}),
|
||||
strokeWidth: edgeDiffStatus ? 3 : isSelected ? 2.5 : 2,
|
||||
stroke: color,
|
||||
strokeDasharray: edgeDiffStatus === 'deleted' ? '10,5' : undefined,
|
||||
opacity: edgeDiffStatus === 'deleted' ? 0.7 : isSelected ? 0.5 : 1,
|
||||
}
|
||||
}, [style, edgeDiffStatus, isSelected, isErrorEdge, edgeRunStatus])
|
||||
const edgeStyle = {
|
||||
...(style ?? {}),
|
||||
strokeWidth: edgeDiffStatus ? 3 : isSelected ? 2.5 : 2,
|
||||
stroke: getEdgeColor(),
|
||||
strokeDasharray: edgeDiffStatus === 'deleted' ? '10,5' : undefined,
|
||||
opacity: edgeDiffStatus === 'deleted' ? 0.7 : isSelected ? 0.5 : 1,
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -149,5 +148,3 @@ const WorkflowEdgeComponent = ({
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const WorkflowEdge = memo(WorkflowEdgeComponent)
|
||||
|
||||
@@ -43,29 +43,26 @@ export interface CurrentWorkflow {
|
||||
*/
|
||||
export function useCurrentWorkflow(): CurrentWorkflow {
|
||||
// Get normal workflow state - optimized with shallow comparison
|
||||
// This prevents re-renders when only subblock values change (not block structure)
|
||||
const normalWorkflow = useWorkflowStore(
|
||||
useShallow((state) => ({
|
||||
blocks: state.blocks,
|
||||
edges: state.edges,
|
||||
loops: state.loops,
|
||||
parallels: state.parallels,
|
||||
lastSaved: state.lastSaved,
|
||||
isDeployed: state.isDeployed,
|
||||
deployedAt: state.deployedAt,
|
||||
deploymentStatuses: state.deploymentStatuses,
|
||||
needsRedeployment: state.needsRedeployment,
|
||||
}))
|
||||
useShallow((state) => {
|
||||
const workflow = state.getWorkflowState()
|
||||
return {
|
||||
blocks: workflow.blocks,
|
||||
edges: workflow.edges,
|
||||
loops: workflow.loops,
|
||||
parallels: workflow.parallels,
|
||||
lastSaved: workflow.lastSaved,
|
||||
isDeployed: workflow.isDeployed,
|
||||
deployedAt: workflow.deployedAt,
|
||||
deploymentStatuses: workflow.deploymentStatuses,
|
||||
needsRedeployment: workflow.needsRedeployment,
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
// Get diff state - optimized with shallow comparison
|
||||
const { isShowingDiff, isDiffReady, hasActiveDiff, baselineWorkflow } = useWorkflowDiffStore(
|
||||
useShallow((state) => ({
|
||||
isShowingDiff: state.isShowingDiff,
|
||||
isDiffReady: state.isDiffReady,
|
||||
hasActiveDiff: state.hasActiveDiff,
|
||||
baselineWorkflow: state.baselineWorkflow,
|
||||
}))
|
||||
)
|
||||
// Get diff state - now including isDiffReady
|
||||
const { isShowingDiff, isDiffReady, hasActiveDiff, baselineWorkflow } = useWorkflowDiffStore()
|
||||
|
||||
// Create the abstracted interface - optimized to prevent unnecessary re-renders
|
||||
const currentWorkflow = useMemo((): CurrentWorkflow => {
|
||||
|
||||
@@ -252,12 +252,23 @@ export function useNodeUtilities(blocks: Record<string, any>) {
|
||||
*/
|
||||
const calculateLoopDimensions = useCallback(
|
||||
(nodeId: string): { width: number; height: number } => {
|
||||
const minWidth = CONTAINER_DIMENSIONS.DEFAULT_WIDTH
|
||||
const minHeight = CONTAINER_DIMENSIONS.DEFAULT_HEIGHT
|
||||
|
||||
// Match styling in subflow-node.tsx:
|
||||
// - Header section: 50px total height
|
||||
// - Content area: px-[16px] pb-[0px] pt-[16px] pr-[70px]
|
||||
// Left padding: 16px, Right padding: 64px, Top padding: 16px, Bottom padding: -6px (reduced by additional 6px from 0 to achieve 14px total reduction from original 8px)
|
||||
// - Children are positioned relative to the content area (after header, inside padding)
|
||||
const headerHeight = 50
|
||||
const leftPadding = 16
|
||||
const rightPadding = 80
|
||||
const topPadding = 16
|
||||
const bottomPadding = 16
|
||||
|
||||
const childNodes = getNodes().filter((node) => node.parentId === nodeId)
|
||||
if (childNodes.length === 0) {
|
||||
return {
|
||||
width: CONTAINER_DIMENSIONS.DEFAULT_WIDTH,
|
||||
height: CONTAINER_DIMENSIONS.DEFAULT_HEIGHT,
|
||||
}
|
||||
return { width: minWidth, height: minHeight }
|
||||
}
|
||||
|
||||
let maxRight = 0
|
||||
@@ -265,21 +276,21 @@ export function useNodeUtilities(blocks: Record<string, any>) {
|
||||
|
||||
childNodes.forEach((node) => {
|
||||
const { width: nodeWidth, height: nodeHeight } = getBlockDimensions(node.id)
|
||||
maxRight = Math.max(maxRight, node.position.x + nodeWidth)
|
||||
maxBottom = Math.max(maxBottom, node.position.y + nodeHeight)
|
||||
|
||||
// Child positions are relative to content area's inner top-left (inside padding)
|
||||
// Calculate the rightmost and bottommost edges of children
|
||||
const rightEdge = node.position.x + nodeWidth
|
||||
const bottomEdge = node.position.y + nodeHeight
|
||||
|
||||
maxRight = Math.max(maxRight, rightEdge)
|
||||
maxBottom = Math.max(maxBottom, bottomEdge)
|
||||
})
|
||||
|
||||
const width = Math.max(
|
||||
CONTAINER_DIMENSIONS.DEFAULT_WIDTH,
|
||||
CONTAINER_DIMENSIONS.LEFT_PADDING + maxRight + CONTAINER_DIMENSIONS.RIGHT_PADDING
|
||||
)
|
||||
const height = Math.max(
|
||||
CONTAINER_DIMENSIONS.DEFAULT_HEIGHT,
|
||||
CONTAINER_DIMENSIONS.HEADER_HEIGHT +
|
||||
CONTAINER_DIMENSIONS.TOP_PADDING +
|
||||
maxBottom +
|
||||
CONTAINER_DIMENSIONS.BOTTOM_PADDING
|
||||
)
|
||||
// Container dimensions = header + padding + children bounds + padding
|
||||
// Width: left padding + max child right edge + right padding (64px)
|
||||
const width = Math.max(minWidth, leftPadding + maxRight + rightPadding)
|
||||
// Height: header + top padding + max child bottom edge + bottom padding (8px)
|
||||
const height = Math.max(minHeight, headerHeight + topPadding + maxBottom + bottomPadding)
|
||||
|
||||
return { width, height }
|
||||
},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user