Compare commits

...

30 Commits

Author SHA1 Message Date
Waleed
e24f31cbce v0.5.27: sidebar updates, ssrf patches, gpt-5.2, stagehand fixes 2025-12-11 14:45:25 -08:00
Waleed
3b9cbeaa49 fix(tools): updated browser use and stagehand to use the latest models (#2319)
* fix(stagehand): incl stagehand in the standalone build

* fix(stagehand): updated browser use and stagehand to use the latest models
2025-12-11 14:39:25 -08:00
Waleed
c592e54118 fix(pg): for pg tools, use count isntead of length for number of rows impacted (#2317) 2025-12-11 14:03:00 -08:00
Waleed
d02499a915 fix(ollama): fixed messages array for ollama, added gpt-5.2 (#2315)
* fix(ollama): fixed messages array for ollama, added gpt-5.2

* ack PR comments
2025-12-11 13:51:51 -08:00
Siddharth Ganesan
c0bb85479d fix(parallel): variable resolution in collection (#2314)
* Fix var resolution in parallel

* Fix parallel

* Clean logs

* FIx loop error port
2025-12-11 13:51:22 -08:00
Waleed
b595273c3f fix(vuln): fix dns rebinding/ssrf vulnerability (#2316) 2025-12-11 13:42:28 -08:00
Emir Karabeg
193a15aca4 improvement(sidebar): auto-scroll (#2312)
* fix(sidebar): re-render auto-scroll

* improvement: sidebar-scrolling listener
2025-12-11 12:54:14 -08:00
Vikhyath Mondreti
39d5d797ec fix(workflow-changes): changes detected in autolayout (#2313) 2025-12-11 12:45:48 -08:00
Waleed
3fbd57caf1 v0.5.26: tool fixes, templates and knowledgebase fixes, deployment versions in logs 2025-12-11 00:52:13 -08:00
Waleed
3db8f82449 feat(deployment-version): capture deployment version in log (#2304)
* feat(deployment-version): capture deployment version in log

* improvement: terminal store, logs version, toolbar

---------

Co-authored-by: Emir Karabeg <emirkarabeg@berkeley.edu>
2025-12-11 00:34:08 -08:00
Waleed
207a14970b fix(dbs): remove harness from validation on user-provided db creds (#2308) 2025-12-11 00:25:48 -08:00
Waleed
0997989f36 fix(x): fix x optional tool params (#2307)
* fix(x): fix x optional tool params

* ack pr comments
2025-12-11 00:09:35 -08:00
Waleed
7fd912d8a9 fix(stagehand): upgraded stagehand sdk to remove deps incomptaible with bun runtime (#2305)
* fix(stagehand): upgraded stagehand sdk to remove deps incomptaible with bun runtime

* ack PR comments
2025-12-10 23:42:33 -08:00
Siddharth Ganesan
c550e4591c fix(condition): fix condition block for no outgoing edge (#2306)
* Fix condition block evaluation

* Lint
2025-12-10 23:24:27 -08:00
Emir Karabeg
a881dc1877 feat(sidebar): scroll to workflow/folder (#2302)
* feat(sidebar): scroll to workflow/folder

* improvement: sidebar scrolling optimizations
2025-12-10 22:08:10 -08:00
Waleed
da36c453b5 feat(i18n): update translations (#2303)
Co-authored-by: waleedlatif1 <waleedlatif1@users.noreply.github.com>
2025-12-10 22:07:18 -08:00
Waleed
635823664c fix(tools): fix perplexity & parallel ai tag dropdown inaccuracies (#2300)
* fix(tools): fix perplexity & parallel ai tag dropdown inaccuracies

* fixed stt, tts and added output conditions to conditionally display tag dropdown values based on other subblock values

* updated exa to match latest API
2025-12-10 21:46:31 -08:00
Waleed
14846ab05c feat(i18n): update translations (#2299)
Co-authored-by: waleedlatif1 <waleedlatif1@users.noreply.github.com>
2025-12-10 21:16:32 -08:00
Waleed
6c99c841f4 fix(tools): fixed zendesk tools, kb upload failure for md files, stronger typing (#2297)
* fix(tools): fixed zendesk tools, kb upload failure for md files, stronger typing

* ack PR comments
2025-12-10 20:42:16 -08:00
Waleed
37d7902fcd fix(dashboard): prevent dashboard from getting unmounted when on the logs page (#2298) 2025-12-10 20:41:26 -08:00
Emir Karabeg
1e563b1e0a fix(ui/ux): templates and knowledge pages (#2296) 2025-12-10 19:56:23 -08:00
Vikhyath Mondreti
b5da61377c v0.5.25: minor ui improvements, copilot billing fix 2025-12-10 18:32:27 -08:00
Vikhyath Mondreti
1d62ece915 fix(billing): copilot should directly deduct credit balance (#2294) 2025-12-10 18:25:36 -08:00
Emir Karabeg
bbab2ff732 improvement(ui/ux): small styling improvements (#2293)
* improvement: action-bar duplicate, copilot header name truncating, dashboard table scrolling

* improvement: workflow block handle positioning

* improvement: copilot notification fix message

* improvement: dashboard layout, popover hovering

* fix: filtering table into row, workflow name truncating
2025-12-10 17:20:59 -08:00
Waleed
18b7032494 v0.5.24: agent tool and UX improvements, redis service overhaul (#2291)
* feat(folders): add the ability to create a folder within a folder in popover (#2287)

* fix(agent): filter out empty params to ensure LLM can set tool params at runtime (#2288)

* fix(mcp): added backfill effect to add missing descriptions for mcp tools (#2290)

* fix(redis): cleanup access pattern across callsites (#2289)

* fix(redis): cleanup access pattern across callsites

* swap redis command to be non blocking

* improvement(log-details): polling, trace spans (#2292)

---------

Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
Co-authored-by: Emir Karabeg <78010029+emir-karabeg@users.noreply.github.com>
2025-12-10 13:09:21 -08:00
Emir Karabeg
76bc2fae83 improvement(log-details): polling, trace spans (#2292) 2025-12-10 13:08:24 -08:00
Vikhyath Mondreti
1cfe229056 fix(redis): cleanup access pattern across callsites (#2289)
* fix(redis): cleanup access pattern across callsites

* swap redis command to be non blocking
2025-12-10 12:37:33 -08:00
Waleed
6791d968b8 fix(mcp): added backfill effect to add missing descriptions for mcp tools (#2290) 2025-12-10 11:57:18 -08:00
Waleed
163db5cf50 fix(agent): filter out empty params to ensure LLM can set tool params at runtime (#2288) 2025-12-10 11:38:54 -08:00
Waleed
bbbb13af7e feat(folders): add the ability to create a folder within a folder in popover (#2287) 2025-12-10 11:19:57 -08:00
210 changed files with 12904 additions and 3948 deletions

View File

@@ -4151,7 +4151,7 @@ export function DuckDuckGoIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} xmlns='http://www.w3.org/2000/svg' viewBox='-108 -108 216 216'>
<circle r='108' fill='#d53' />
<circle r='96' fill='none' stroke='#ffffff' stroke-width='7' />
<circle r='96' fill='none' stroke='#ffffff' strokeWidth={7} />
<path
d='M-32-55C-62-48-51-6-51-6l19 93 7 3M-39-73h-8l11 4s-11 0-11 7c24-1 35 5 35 5'
fill='#ddd'

View File

@@ -46,11 +46,11 @@ Durchsuchen Sie das Web mit Exa AI. Liefert relevante Suchergebnisse mit Titeln,
| `type` | string | Nein | Suchtyp: neural, keyword, auto oder fast \(Standard: auto\) |
| `includeDomains` | string | Nein | Kommagetrennte Liste von Domains, die in den Ergebnissen enthalten sein sollen |
| `excludeDomains` | string | Nein | Kommagetrennte Liste von Domains, die aus den Ergebnissen ausgeschlossen werden sollen |
| `category` | string | Nein | Nach Kategorie filtern: company, research_paper, news_article, pdf, github, tweet, movie, song, personal_site |
| `category` | string | Nein | Nach Kategorie filtern: company, research paper, news, pdf, github, tweet, personal site, linkedin profile, financial report |
| `text` | boolean | Nein | Vollständigen Textinhalt in Ergebnissen einschließen \(Standard: false\) |
| `highlights` | boolean | Nein | Hervorgehobene Ausschnitte in Ergebnissen einschließen \(Standard: false\) |
| `summary` | boolean | Nein | KI-generierte Zusammenfassungen in Ergebnissen einschließen \(Standard: false\) |
| `livecrawl` | string | Nein | Live-Crawling-Modus: always, fallback oder never \(Standard: never\) |
| `livecrawl` | string | Nein | Live-Crawling-Modus: never \(Standard\), fallback, always oder preferred \(immer livecrawl versuchen, bei Fehlschlag auf Cache zurückgreifen\) |
| `apiKey` | string | Ja | Exa AI API-Schlüssel |
#### Ausgabe
@@ -69,11 +69,11 @@ Ruft den Inhalt von Webseiten mit Exa AI ab. Gibt den Titel, Textinhalt und opti
| --------- | ---- | -------- | ----------- |
| `urls` | string | Ja | Kommagetrennte Liste von URLs, von denen Inhalte abgerufen werden sollen |
| `text` | boolean | Nein | Wenn true, gibt den vollständigen Seitentext mit Standardeinstellungen zurück. Wenn false, deaktiviert die Textrückgabe. |
| `summaryQuery` | string | Nein | Abfrage zur Steuerung der Zusammenfassungserstellung |
| `subpages` | number | Nein | Anzahl der Unterseiten, die von den angegebenen URLs gecrawlt werden sollen |
| `summaryQuery` | string | Nein | Anfrage zur Steuerung der Zusammenfassungserstellung |
| `subpages` | number | Nein | Anzahl der Unterseiten, die von den bereitgestellten URLs gecrawlt werden sollen |
| `subpageTarget` | string | Nein | Kommagetrennte Schlüsselwörter zur Zielausrichtung auf bestimmte Unterseiten \(z.B. "docs,tutorial,about"\) |
| `highlights` | boolean | Nein | Hervorgehobene Ausschnitte in Ergebnissen einschließen \(Standard: false\) |
| `livecrawl` | string | Nein | Live-Crawling-Modus: always, fallback oder never \(Standard: never\) |
| `livecrawl` | string | Nein | Live-Crawling-Modus: never \(Standard\), fallback, always oder preferred \(immer livecrawl versuchen, bei Fehlschlag auf Cache zurückgreifen\) |
| `apiKey` | string | Ja | Exa AI API-Schlüssel |
#### Ausgabe
@@ -95,11 +95,10 @@ Finde Webseiten, die einer bestimmten URL ähnlich sind, mit Exa AI. Gibt eine L
| `text` | boolean | Nein | Ob der vollständige Text der ähnlichen Seiten eingeschlossen werden soll |
| `includeDomains` | string | Nein | Kommagetrennte Liste von Domains, die in den Ergebnissen enthalten sein sollen |
| `excludeDomains` | string | Nein | Kommagetrennte Liste von Domains, die aus den Ergebnissen ausgeschlossen werden sollen |
| `excludeSourceDomain` | boolean | Nein | Quell-Domain aus Ergebnissen ausschließen \(Standard: false\) |
| `category` | string | Nein | Nach Kategorie filtern: company, research_paper, news_article, pdf, github, tweet, movie, song, personal_site |
| `excludeSourceDomain` | boolean | Nein | Die Quell-Domain aus den Ergebnissen ausschließen \(Standard: false\) |
| `highlights` | boolean | Nein | Hervorgehobene Ausschnitte in Ergebnissen einschließen \(Standard: false\) |
| `summary` | boolean | Nein | KI-generierte Zusammenfassungen in Ergebnissen einschließen \(Standard: false\) |
| `livecrawl` | string | Nein | Live-Crawling-Modus: always, fallback oder never \(Standard: never\) |
| `livecrawl` | string | Nein | Live-Crawling-Modus: never \(Standard\), fallback, always oder preferred \(versucht immer livecrawl, fällt auf Cache zurück, wenn es fehlschlägt\) |
| `apiKey` | string | Ja | Exa AI API-Schlüssel |
#### Ausgabe

View File

@@ -91,11 +91,11 @@ Führen Sie umfassende tiefgehende Recherchen im Web mit Parallel AI durch. Synt
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `status` | string | Aufgabenstatus (laufend, abgeschlossen, fehlgeschlagen) |
| `status` | string | Aufgabenstatus (abgeschlossen, fehlgeschlagen) |
| `run_id` | string | Eindeutige ID für diese Rechercheaufgabe |
| `message` | string | Statusmeldung (für laufende Aufgaben) |
| `message` | string | Statusmeldung |
| `content` | object | Rechercheergebnisse (strukturiert basierend auf output_schema) |
| `basis` | array | Zitate und Quellen mit Auszügen und Vertrauensstufen |
| `basis` | array | Zitate und Quellen mit Begründung und Vertrauensstufen |
## Hinweise

View File

@@ -51,8 +51,9 @@ Generieren Sie Vervollständigungen mit Perplexity AI-Chatmodellen
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Status des Operationserfolgs |
| `output` | object | Ergebnisse der Chat-Vervollständigung |
| `content` | string | Generierter Textinhalt |
| `model` | string | Für die Generierung verwendetes Modell |
| `usage` | object | Informationen zur Token-Nutzung |
### `perplexity_search`
@@ -76,8 +77,7 @@ Erhalte bewertete Suchergebnisse von Perplexity
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Status des Operationserfolgs |
| `output` | object | Suchergebnisse |
| `results` | array | Array von Suchergebnissen |
## Hinweise

View File

@@ -37,7 +37,7 @@ Verarbeitet einen bereitgestellten Gedanken/eine Anweisung und macht ihn für na
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `thought` | string | Ja | Der vom Benutzer im Thinking Step-Block bereitgestellte Denkprozess oder die Anweisung. |
| `thought` | string | Ja | Ihre interne Argumentation, Analyse oder Denkprozess. Nutzen Sie dies, um das Problem Schritt für Schritt zu durchdenken, bevor Sie antworten. |
#### Output

View File

@@ -31,39 +31,32 @@ Integrieren Sie Übersetzen in den Workflow. Kann Text in jede Sprache übersetz
## Tools
### `openai_chat`
### `llm_chat`
Senden Sie eine Chat-Completion-Anfrage an jeden unterstützten LLM-Anbieter
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `model` | string | Ja | Das zu verwendende Modell (z.B. gpt-4o, claude-sonnet-4-5, gemini-2.0-flash) |
| `systemPrompt` | string | Nein | System-Prompt zur Festlegung des Assistentenverhaltens |
| `context` | string | Ja | Die Benutzernachricht oder der Kontext, der an das Modell gesendet wird |
| `apiKey` | string | Nein | API-Schlüssel für den Anbieter (verwendet den Plattformschlüssel, wenn für gehostete Modelle nicht angegeben) |
| `temperature` | number | Nein | Temperatur für die Antwortgenerierung (0-2) |
| `maxTokens` | number | Nein | Maximale Tokens in der Antwort |
| `azureEndpoint` | string | Nein | Azure OpenAI-Endpunkt-URL |
| `azureApiVersion` | string | Nein | Azure OpenAI API-Version |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `content` | string | Übersetzter Text |
| `model` | string | Verwendetes Modell |
| `tokens` | json | Token-Nutzung |
| `content` | string | Der generierte Antwortinhalt |
| `model` | string | Das für die Generierung verwendete Modell |
| `tokens` | object | Informationen zur Token-Nutzung |
### `anthropic_chat`
### `google_chat`
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `content` | string | Übersetzter Text |
| `model` | string | Verwendetes Modell |
| `tokens` | json | Token-Nutzung |
## Notizen
## Hinweise
- Kategorie: `tools`
- Typ: `translate`

View File

@@ -250,9 +250,9 @@ Eine Mediendatei (Bild, Video, Dokument) zu WordPress.com hochladen
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `siteId` | string | Ja | WordPress.com-Website-ID oder Domain \(z.B. 12345678 oder meinwebsite.wordpress.com\) |
| `file` | string | Ja | Base64-kodierte Dateidaten oder URL, von der die Datei abgerufen werden soll |
| `filename` | string | Ja | Dateiname mit Erweiterung \(z.B. bild.jpg\) |
| `siteId` | string | Ja | WordPress.com-Site-ID oder Domain \(z.B. 12345678 oder mysite.wordpress.com\) |
| `file` | file | Nein | Hochzuladende Datei \(UserFile-Objekt\) |
| `filename` | string | Nein | Optionale Überschreibung des Dateinamens \(z.B. bild.jpg\) |
| `title` | string | Nein | Medientitel |
| `caption` | string | Nein | Medienunterschrift |
| `altText` | string | Nein | Alternativer Text für Barrierefreiheit |

View File

@@ -170,28 +170,9 @@ Videos aus einer YouTube-Playlist abrufen.
| --------- | ---- | ----------- |
| `items` | array | Array von Videos in der Playlist |
### `youtube_related_videos`
Finde Videos, die mit einem bestimmten YouTube-Video verwandt sind.
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `videoId` | string | Ja | YouTube-Video-ID, für die verwandte Videos gefunden werden sollen |
| `maxResults` | number | Nein | Maximale Anzahl der zurückzugebenden verwandten Videos \(1-50\) |
| `pageToken` | string | Nein | Page-Token für Paginierung |
| `apiKey` | string | Ja | YouTube API-Schlüssel |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `items` | array | Array von verwandten Videos |
### `youtube_comments`
Rufe Kommentare von einem YouTube-Video ab.
Kommentare von einem YouTube-Video abrufen.
#### Eingabe
@@ -200,7 +181,7 @@ Rufe Kommentare von einem YouTube-Video ab.
| `videoId` | string | Ja | YouTube-Video-ID |
| `maxResults` | number | Nein | Maximale Anzahl der zurückzugebenden Kommentare |
| `order` | string | Nein | Reihenfolge der Kommentare: time oder relevance |
| `pageToken` | string | Nein | Page-Token für Paginierung |
| `pageToken` | string | Nein | Seitentoken für Paginierung |
| `apiKey` | string | Ja | YouTube API-Schlüssel |
#### Ausgabe

View File

@@ -73,8 +73,9 @@ Eine Liste von Tickets aus Zendesk mit optionaler Filterung abrufen
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Erfolgsstatus der Operation |
| `output` | object | Ticket-Daten und Metadaten |
| `tickets` | array | Array von Ticket-Objekten |
| `paging` | object | Paginierungsinformationen |
| `metadata` | object | Operationsmetadaten |
### `zendesk_get_ticket`
@@ -93,8 +94,8 @@ Ein einzelnes Ticket anhand der ID von Zendesk abrufen
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Erfolgsstatus der Operation |
| `output` | object | Ticket-Daten |
| `ticket` | object | Ticket-Objekt |
| `metadata` | object | Operationsmetadaten |
### `zendesk_create_ticket`
@@ -120,10 +121,10 @@ Ein neues Ticket in Zendesk erstellen mit Unterstützung für benutzerdefinierte
#### Output
| Parameter | Type | Description |
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Erfolgsstatus der Operation |
| `output` | object | Erstellte Ticket-Daten |
| `ticket` | object | Erstelltes Ticket-Objekt |
| `metadata` | object | Operationsmetadaten |
### `zendesk_create_tickets_bulk`
@@ -140,10 +141,10 @@ Erstellen Sie mehrere Tickets in Zendesk auf einmal (maximal 100)
#### Output
| Parameter | Type | Description |
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Erfolgsstatus der Operation |
| `output` | object | Status des Massenerstell-Jobs |
| `jobStatus` | object | Job-Status-Objekt |
| `metadata` | object | Operationsmetadaten |
### `zendesk_update_ticket`
@@ -171,8 +172,8 @@ Aktualisieren Sie ein bestehendes Ticket in Zendesk mit Unterstützung für benu
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Status des Operationserfolgs |
| `output` | object | Aktualisierte Ticket-Daten |
| `ticket` | object | Aktualisiertes Ticket-Objekt |
| `metadata` | object | Operationsmetadaten |
### `zendesk_update_tickets_bulk`
@@ -196,8 +197,8 @@ Mehrere Tickets in Zendesk auf einmal aktualisieren (max. 100)
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Status des Operationserfolgs |
| `output` | object | Status des Massenaktualisierungsauftrags |
| `jobStatus` | object | Job-Status-Objekt |
| `metadata` | object | Operationsmetadaten |
### `zendesk_delete_ticket`
@@ -216,8 +217,8 @@ Ein Ticket aus Zendesk löschen
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Status des Operationserfolgs |
| `output` | object | Löschbestätigung |
| `deleted` | boolean | Löschvorgang erfolgreich |
| `metadata` | object | Operationsmetadaten |
### `zendesk_merge_tickets`
@@ -238,8 +239,8 @@ Mehrere Tickets in ein Zielticket zusammenführen
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Status des Operationserfolgs |
| `output` | object | Status des Zusammenführungsauftrags |
| `jobStatus` | object | Job-Status-Objekt |
| `metadata` | object | Operationsmetadaten |
### `zendesk_get_users`
@@ -261,8 +262,9 @@ Eine Liste von Benutzern aus Zendesk mit optionaler Filterung abrufen
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Status des Operationserfolgs |
| `output` | object | Benutzerdaten und Metadaten |
| `users` | array | Array von Benutzerobjekten |
| `paging` | object | Paginierungsinformationen |
| `metadata` | object | Operationsmetadaten |
### `zendesk_get_user`
@@ -281,8 +283,8 @@ Einen einzelnen Benutzer anhand der ID von Zendesk abrufen
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Status des Operationserfolgs |
| `output` | object | Benutzerdaten |
| `user` | object | Benutzerobjekt |
| `metadata` | object | Operationsmetadaten |
### `zendesk_get_current_user`
@@ -300,8 +302,8 @@ Den aktuell authentifizierten Benutzer von Zendesk abrufen
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Status des Operationserfolgs |
| `output` | object | Daten des aktuellen Benutzers |
| `user` | object | Aktuelles Benutzerobjekt |
| `metadata` | object | Operationsmetadaten |
### `zendesk_search_users`
@@ -323,8 +325,9 @@ Nach Benutzern in Zendesk mit einer Abfragezeichenfolge suchen
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Status des Operationserfolgs |
| `output` | object | Suchergebnisse für Benutzer |
| `users` | array | Array von Benutzerobjekten |
| `paging` | object | Paginierungsinformationen |
| `metadata` | object | Operationsmetadaten |
### `zendesk_create_user`
@@ -348,10 +351,10 @@ Einen neuen Benutzer in Zendesk erstellen
#### Output
| Parameter | Type | Beschreibung |
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Status des Operationserfolgs |
| `output` | object | Erstellte Benutzerdaten |
| `user` | object | Erstelltes Benutzerobjekt |
| `metadata` | object | Operationsmetadaten |
### `zendesk_create_users_bulk`
@@ -368,10 +371,10 @@ Erstellen mehrerer Benutzer in Zendesk mittels Massenimport
#### Output
| Parameter | Type | Beschreibung |
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Status des Operationserfolgs |
| `output` | object | Status des Massenimport-Jobs |
| `jobStatus` | object | Job-Statusobjekt |
| `metadata` | object | Operationsmetadaten |
### `zendesk_update_user`
@@ -398,8 +401,8 @@ Aktualisieren eines vorhandenen Benutzers in Zendesk
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Erfolgsstatus der Operation |
| `output` | object | Aktualisierte Benutzerdaten |
| `user` | object | Aktualisiertes Benutzerobjekt |
| `metadata` | object | Operationsmetadaten |
### `zendesk_update_users_bulk`
@@ -418,8 +421,8 @@ Mehrere Benutzer in Zendesk über Massenaktualisierung aktualisieren
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Erfolgsstatus der Operation |
| `output` | object | Status des Massenaktualisierungsauftrags |
| `jobStatus` | object | Job-Statusobjekt |
| `metadata` | object | Operationsmetadaten |
### `zendesk_delete_user`
@@ -438,8 +441,8 @@ Einen Benutzer aus Zendesk löschen
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Erfolgsstatus der Operation |
| `output` | object | Daten des gelöschten Benutzers |
| `deleted` | boolean | Löscherfolg |
| `metadata` | object | Operationsmetadaten |
### `zendesk_get_organizations`
@@ -459,8 +462,9 @@ Eine Liste von Organisationen von Zendesk abrufen
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Erfolgsstatus der Operation |
| `output` | object | Organisationsdaten und Metadaten |
| `organizations` | array | Array von Organisationsobjekten |
| `paging` | object | Paginierungsinformationen |
| `metadata` | object | Operationsmetadaten |
### `zendesk_get_organization`
@@ -479,8 +483,8 @@ Eine einzelne Organisation anhand der ID von Zendesk abrufen
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Erfolgsstatus der Operation |
| `output` | object | Organisationsdaten |
| `organization` | object | Organisationsobjekt |
| `metadata` | object | Operationsmetadaten |
### `zendesk_autocomplete_organizations`
@@ -499,10 +503,11 @@ Autovervollständigung von Organisationen in Zendesk nach Namenspräfix (für Na
#### Output
| Parameter | Type | Beschreibung |
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Erfolgsstatus der Operation |
| `output` | object | Suchergebnisse für Organisationen |
| `organizations` | array | Array von Organisationsobjekten |
| `paging` | object | Paginierungsinformationen |
| `metadata` | object | Operationsmetadaten |
### `zendesk_create_organization`
@@ -524,10 +529,10 @@ Eine neue Organisation in Zendesk erstellen
#### Output
| Parameter | Type | Beschreibung |
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Erfolgsstatus der Operation |
| `output` | object | Daten der erstellten Organisation |
| `organization` | object | Erstelltes Organisationsobjekt |
| `metadata` | object | Operationsmetadaten |
### `zendesk_create_organizations_bulk`
@@ -546,8 +551,8 @@ Mehrere Organisationen in Zendesk über Massenimport erstellen
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Status des Operationserfolgs |
| `output` | object | Status des Massenerfassungsauftrags |
| `jobStatus` | object | Job-Statusobjekt |
| `metadata` | object | Operationsmetadaten |
### `zendesk_update_organization`
@@ -572,8 +577,8 @@ Eine bestehende Organisation in Zendesk aktualisieren
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Status des Operationserfolgs |
| `output` | object | Aktualisierte Organisationsdaten |
| `organization` | object | Aktualisiertes Organisationsobjekt |
| `metadata` | object | Operationsmetadaten |
### `zendesk_delete_organization`
@@ -592,8 +597,8 @@ Eine Organisation aus Zendesk löschen
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Status des Operationserfolgs |
| `output` | object | Gelöschte Organisationsdaten |
| `deleted` | boolean | Löscherfolg |
| `metadata` | object | Operationsmetadaten |
### `zendesk_search`
@@ -616,8 +621,9 @@ Einheitliche Suche über Tickets, Benutzer und Organisationen in Zendesk
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Status des Operationserfolgs |
| `output` | object | Suchergebnisse |
| `results` | array | Array von Ergebnisobjekten |
| `paging` | object | Paginierungsinformationen |
| `metadata` | object | Operationsmetadaten |
### `zendesk_search_count`
@@ -636,8 +642,8 @@ Zählen der Anzahl von Suchergebnissen, die einer Abfrage in Zendesk entsprechen
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `success` | boolean | Erfolgsstatus der Operation |
| `output` | object | Suchergebnis-Anzahl |
| `count` | number | Anzahl der übereinstimmenden Ergebnisse |
| `metadata` | object | Operationsmetadaten |
## Hinweise

View File

@@ -49,11 +49,11 @@ Search the web using Exa AI. Returns relevant search results with titles, URLs,
| `type` | string | No | Search type: neural, keyword, auto or fast \(default: auto\) |
| `includeDomains` | string | No | Comma-separated list of domains to include in results |
| `excludeDomains` | string | No | Comma-separated list of domains to exclude from results |
| `category` | string | No | Filter by category: company, research_paper, news_article, pdf, github, tweet, movie, song, personal_site |
| `category` | string | No | Filter by category: company, research paper, news, pdf, github, tweet, personal site, linkedin profile, financial report |
| `text` | boolean | No | Include full text content in results \(default: false\) |
| `highlights` | boolean | No | Include highlighted snippets in results \(default: false\) |
| `summary` | boolean | No | Include AI-generated summaries in results \(default: false\) |
| `livecrawl` | string | No | Live crawling mode: always, fallback, or never \(default: never\) |
| `livecrawl` | string | No | Live crawling mode: never \(default\), fallback, always, or preferred \(always try livecrawl, fall back to cache if fails\) |
| `apiKey` | string | Yes | Exa AI API Key |
#### Output
@@ -76,7 +76,7 @@ Retrieve the contents of webpages using Exa AI. Returns the title, text content,
| `subpages` | number | No | Number of subpages to crawl from the provided URLs |
| `subpageTarget` | string | No | Comma-separated keywords to target specific subpages \(e.g., "docs,tutorial,about"\) |
| `highlights` | boolean | No | Include highlighted snippets in results \(default: false\) |
| `livecrawl` | string | No | Live crawling mode: always, fallback, or never \(default: never\) |
| `livecrawl` | string | No | Live crawling mode: never \(default\), fallback, always, or preferred \(always try livecrawl, fall back to cache if fails\) |
| `apiKey` | string | Yes | Exa AI API Key |
#### Output
@@ -99,10 +99,9 @@ Find webpages similar to a given URL using Exa AI. Returns a list of similar lin
| `includeDomains` | string | No | Comma-separated list of domains to include in results |
| `excludeDomains` | string | No | Comma-separated list of domains to exclude from results |
| `excludeSourceDomain` | boolean | No | Exclude the source domain from results \(default: false\) |
| `category` | string | No | Filter by category: company, research_paper, news_article, pdf, github, tweet, movie, song, personal_site |
| `highlights` | boolean | No | Include highlighted snippets in results \(default: false\) |
| `summary` | boolean | No | Include AI-generated summaries in results \(default: false\) |
| `livecrawl` | string | No | Live crawling mode: always, fallback, or never \(default: never\) |
| `livecrawl` | string | No | Live crawling mode: never \(default\), fallback, always, or preferred \(always try livecrawl, fall back to cache if fails\) |
| `apiKey` | string | Yes | Exa AI API Key |
#### Output

View File

@@ -94,11 +94,11 @@ Conduct comprehensive deep research across the web using Parallel AI. Synthesize
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `status` | string | Task status \(running, completed, failed\) |
| `status` | string | Task status \(completed, failed\) |
| `run_id` | string | Unique ID for this research task |
| `message` | string | Status message \(for running tasks\) |
| `message` | string | Status message |
| `content` | object | Research results \(structured based on output_schema\) |
| `basis` | array | Citations and sources with excerpts and confidence levels |
| `basis` | array | Citations and sources with reasoning and confidence levels |

View File

@@ -54,8 +54,9 @@ Generate completions using Perplexity AI chat models
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Chat completion results |
| `content` | string | Generated text content |
| `model` | string | Model used for generation |
| `usage` | object | Token usage information |
### `perplexity_search`
@@ -79,8 +80,7 @@ Get ranked search results from Perplexity
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Search results |
| `results` | array | Array of search results |

View File

@@ -40,7 +40,7 @@ Processes a provided thought/instruction, making it available for subsequent ste
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `thought` | string | Yes | The thought process or instruction provided by the user in the Thinking Step block. |
| `thought` | string | Yes | Your internal reasoning, analysis, or thought process. Use this to think through the problem step by step before responding. |
#### Output

View File

@@ -34,38 +34,30 @@ Integrate Translate into the workflow. Can translate text to any language.
## Tools
### `openai_chat`
### `llm_chat`
Send a chat completion request to any supported LLM provider
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `model` | string | Yes | The model to use \(e.g., gpt-4o, claude-sonnet-4-5, gemini-2.0-flash\) |
| `systemPrompt` | string | No | System prompt to set the behavior of the assistant |
| `context` | string | Yes | The user message or context to send to the model |
| `apiKey` | string | No | API key for the provider \(uses platform key if not provided for hosted models\) |
| `temperature` | number | No | Temperature for response generation \(0-2\) |
| `maxTokens` | number | No | Maximum tokens in the response |
| `azureEndpoint` | string | No | Azure OpenAI endpoint URL |
| `azureApiVersion` | string | No | Azure OpenAI API version |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Translated text |
| `model` | string | Model used |
| `tokens` | json | Token usage |
### `anthropic_chat`
### `google_chat`
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Translated text |
| `model` | string | Model used |
| `tokens` | json | Token usage |
| `content` | string | The generated response content |
| `model` | string | The model used for generation |
| `tokens` | object | Token usage information |

View File

@@ -254,8 +254,8 @@ Upload a media file (image, video, document) to WordPress.com
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `siteId` | string | Yes | WordPress.com site ID or domain \(e.g., 12345678 or mysite.wordpress.com\) |
| `file` | string | Yes | Base64 encoded file data or URL to fetch file from |
| `filename` | string | Yes | Filename with extension \(e.g., image.jpg\) |
| `file` | file | No | File to upload \(UserFile object\) |
| `filename` | string | No | Optional filename override \(e.g., image.jpg\) |
| `title` | string | No | Media title |
| `caption` | string | No | Media caption |
| `altText` | string | No | Alternative text for accessibility |

View File

@@ -173,25 +173,6 @@ Get videos from a YouTube playlist.
| --------- | ---- | ----------- |
| `items` | array | Array of videos in the playlist |
### `youtube_related_videos`
Find videos related to a specific YouTube video.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `videoId` | string | Yes | YouTube video ID to find related videos for |
| `maxResults` | number | No | Maximum number of related videos to return \(1-50\) |
| `pageToken` | string | No | Page token for pagination |
| `apiKey` | string | Yes | YouTube API Key |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `items` | array | Array of related videos |
### `youtube_comments`
Get comments from a YouTube video.

View File

@@ -76,8 +76,9 @@ Retrieve a list of tickets from Zendesk with optional filtering
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Tickets data and metadata |
| `tickets` | array | Array of ticket objects |
| `paging` | object | Pagination information |
| `metadata` | object | Operation metadata |
### `zendesk_get_ticket`
@@ -96,8 +97,8 @@ Get a single ticket by ID from Zendesk
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Ticket data |
| `ticket` | object | Ticket object |
| `metadata` | object | Operation metadata |
### `zendesk_create_ticket`
@@ -125,8 +126,8 @@ Create a new ticket in Zendesk with support for custom fields
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Created ticket data |
| `ticket` | object | Created ticket object |
| `metadata` | object | Operation metadata |
### `zendesk_create_tickets_bulk`
@@ -145,8 +146,8 @@ Create multiple tickets in Zendesk at once (max 100)
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Bulk create job status |
| `jobStatus` | object | Job status object |
| `metadata` | object | Operation metadata |
### `zendesk_update_ticket`
@@ -174,8 +175,8 @@ Update an existing ticket in Zendesk with support for custom fields
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Updated ticket data |
| `ticket` | object | Updated ticket object |
| `metadata` | object | Operation metadata |
### `zendesk_update_tickets_bulk`
@@ -199,8 +200,8 @@ Update multiple tickets in Zendesk at once (max 100)
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Bulk update job status |
| `jobStatus` | object | Job status object |
| `metadata` | object | Operation metadata |
### `zendesk_delete_ticket`
@@ -219,8 +220,8 @@ Delete a ticket from Zendesk
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Delete confirmation |
| `deleted` | boolean | Deletion success |
| `metadata` | object | Operation metadata |
### `zendesk_merge_tickets`
@@ -241,8 +242,8 @@ Merge multiple tickets into a target ticket
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Merge job status |
| `jobStatus` | object | Job status object |
| `metadata` | object | Operation metadata |
### `zendesk_get_users`
@@ -264,8 +265,9 @@ Retrieve a list of users from Zendesk with optional filtering
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Users data and metadata |
| `users` | array | Array of user objects |
| `paging` | object | Pagination information |
| `metadata` | object | Operation metadata |
### `zendesk_get_user`
@@ -284,8 +286,8 @@ Get a single user by ID from Zendesk
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | User data |
| `user` | object | User object |
| `metadata` | object | Operation metadata |
### `zendesk_get_current_user`
@@ -303,8 +305,8 @@ Get the currently authenticated user from Zendesk
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Current user data |
| `user` | object | Current user object |
| `metadata` | object | Operation metadata |
### `zendesk_search_users`
@@ -326,8 +328,9 @@ Search for users in Zendesk using a query string
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Users search results |
| `users` | array | Array of user objects |
| `paging` | object | Pagination information |
| `metadata` | object | Operation metadata |
### `zendesk_create_user`
@@ -353,8 +356,8 @@ Create a new user in Zendesk
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Created user data |
| `user` | object | Created user object |
| `metadata` | object | Operation metadata |
### `zendesk_create_users_bulk`
@@ -373,8 +376,8 @@ Create multiple users in Zendesk using bulk import
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Bulk creation job status |
| `jobStatus` | object | Job status object |
| `metadata` | object | Operation metadata |
### `zendesk_update_user`
@@ -401,8 +404,8 @@ Update an existing user in Zendesk
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Updated user data |
| `user` | object | Updated user object |
| `metadata` | object | Operation metadata |
### `zendesk_update_users_bulk`
@@ -421,8 +424,8 @@ Update multiple users in Zendesk using bulk update
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Bulk update job status |
| `jobStatus` | object | Job status object |
| `metadata` | object | Operation metadata |
### `zendesk_delete_user`
@@ -441,8 +444,8 @@ Delete a user from Zendesk
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Deleted user data |
| `deleted` | boolean | Deletion success |
| `metadata` | object | Operation metadata |
### `zendesk_get_organizations`
@@ -462,8 +465,9 @@ Retrieve a list of organizations from Zendesk
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Organizations data and metadata |
| `organizations` | array | Array of organization objects |
| `paging` | object | Pagination information |
| `metadata` | object | Operation metadata |
### `zendesk_get_organization`
@@ -482,8 +486,8 @@ Get a single organization by ID from Zendesk
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Organization data |
| `organization` | object | Organization object |
| `metadata` | object | Operation metadata |
### `zendesk_autocomplete_organizations`
@@ -504,8 +508,9 @@ Autocomplete organizations in Zendesk by name prefix (for name matching/autocomp
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Organizations search results |
| `organizations` | array | Array of organization objects |
| `paging` | object | Pagination information |
| `metadata` | object | Operation metadata |
### `zendesk_create_organization`
@@ -529,8 +534,8 @@ Create a new organization in Zendesk
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Created organization data |
| `organization` | object | Created organization object |
| `metadata` | object | Operation metadata |
### `zendesk_create_organizations_bulk`
@@ -549,8 +554,8 @@ Create multiple organizations in Zendesk using bulk import
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Bulk creation job status |
| `jobStatus` | object | Job status object |
| `metadata` | object | Operation metadata |
### `zendesk_update_organization`
@@ -575,8 +580,8 @@ Update an existing organization in Zendesk
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Updated organization data |
| `organization` | object | Updated organization object |
| `metadata` | object | Operation metadata |
### `zendesk_delete_organization`
@@ -595,8 +600,8 @@ Delete an organization from Zendesk
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Deleted organization data |
| `deleted` | boolean | Deletion success |
| `metadata` | object | Operation metadata |
### `zendesk_search`
@@ -619,8 +624,9 @@ Unified search across tickets, users, and organizations in Zendesk
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Search results |
| `results` | array | Array of result objects |
| `paging` | object | Pagination information |
| `metadata` | object | Operation metadata |
### `zendesk_search_count`
@@ -639,8 +645,8 @@ Count the number of search results matching a query in Zendesk
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Operation success status |
| `output` | object | Search count result |
| `count` | number | Number of matching results |
| `metadata` | object | Operation metadata |

View File

@@ -46,11 +46,11 @@ Busca en la web usando Exa AI. Devuelve resultados de búsqueda relevantes con t
| `type` | string | No | Tipo de búsqueda: neural, keyword, auto o fast \(predeterminado: auto\) |
| `includeDomains` | string | No | Lista separada por comas de dominios a incluir en los resultados |
| `excludeDomains` | string | No | Lista separada por comas de dominios a excluir de los resultados |
| `category` | string | No | Filtrar por categoría: company, research_paper, news_article, pdf, github, tweet, movie, song, personal_site |
| `category` | string | No | Filtrar por categoría: company, research paper, news, pdf, github, tweet, personal site, linkedin profile, financial report |
| `text` | boolean | No | Incluir contenido de texto completo en los resultados \(predeterminado: false\) |
| `highlights` | boolean | No | Incluir fragmentos destacados en los resultados \(predeterminado: false\) |
| `summary` | boolean | No | Incluir resúmenes generados por IA en los resultados \(predeterminado: false\) |
| `livecrawl` | string | No | Modo de rastreo en vivo: always, fallback o never \(predeterminado: never\) |
| `livecrawl` | string | No | Modo de rastreo en vivo: never \(predeterminado\), fallback, always, o preferred \(siempre intenta livecrawl, recurre a caché si falla\) |
| `apiKey` | string | Sí | Clave API de Exa AI |
#### Salida
@@ -67,13 +67,13 @@ Recupera el contenido de páginas web usando Exa AI. Devuelve el título, conten
| Parámetro | Tipo | Obligatorio | Descripción |
| --------- | ---- | ---------- | ----------- |
| `urls` | string | Sí | Lista separada por comas de URLs de las que recuperar contenido |
| `urls` | string | Sí | Lista separada por comas de URLs para recuperar contenido |
| `text` | boolean | No | Si es true, devuelve el texto completo de la página con la configuración predeterminada. Si es false, desactiva la devolución de texto. |
| `summaryQuery` | string | No | Consulta para guiar la generación del resumen |
| `subpages` | number | No | Número de subpáginas a rastrear desde las URLs proporcionadas |
| `subpageTarget` | string | No | Palabras clave separadas por comas para dirigirse a subpáginas específicas \(por ejemplo, "docs,tutorial,about"\) |
| `highlights` | boolean | No | Incluir fragmentos destacados en los resultados \(predeterminado: false\) |
| `livecrawl` | string | No | Modo de rastreo en vivo: always, fallback o never \(predeterminado: never\) |
| `livecrawl` | string | No | Modo de rastreo en vivo: never \(predeterminado\), fallback, always, o preferred \(siempre intenta livecrawl, recurre a caché si falla\) |
| `apiKey` | string | Sí | Clave API de Exa AI |
#### Salida
@@ -96,10 +96,9 @@ Encuentra páginas web similares a una URL determinada utilizando Exa AI. Devuel
| `includeDomains` | string | No | Lista separada por comas de dominios a incluir en los resultados |
| `excludeDomains` | string | No | Lista separada por comas de dominios a excluir de los resultados |
| `excludeSourceDomain` | boolean | No | Excluir el dominio de origen de los resultados \(predeterminado: false\) |
| `category` | string | No | Filtrar por categoría: company, research_paper, news_article, pdf, github, tweet, movie, song, personal_site |
| `highlights` | boolean | No | Incluir fragmentos destacados en los resultados \(predeterminado: false\) |
| `summary` | boolean | No | Incluir resúmenes generados por IA en los resultados \(predeterminado: false\) |
| `livecrawl` | string | No | Modo de rastreo en vivo: always, fallback o never \(predeterminado: never\) |
| `livecrawl` | string | No | Modo de rastreo en vivo: never \(predeterminado\), fallback, always o preferred \(siempre intenta rastreo en vivo, recurre a caché si falla\) |
| `apiKey` | string | Sí | Clave API de Exa AI |
#### Salida

View File

@@ -91,11 +91,11 @@ Realiza investigaciones exhaustivas y profundas en la web utilizando Parallel AI
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `status` | string | Estado de la tarea (en ejecución, completada, fallida) |
| `status` | string | Estado de la tarea (completada, fallida) |
| `run_id` | string | ID único para esta tarea de investigación |
| `message` | string | Mensaje de estado (para tareas en ejecución) |
| `message` | string | Mensaje de estado |
| `content` | object | Resultados de la investigación (estructurados según output_schema) |
| `basis` | array | Citas y fuentes con extractos y niveles de confianza |
| `basis` | array | Citas y fuentes con razonamiento y niveles de confianza |
## Notas

View File

@@ -51,8 +51,9 @@ Genera completados utilizando los modelos de chat de Perplexity AI
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Resultados del completado de chat |
| `content` | string | Contenido de texto generado |
| `model` | string | Modelo utilizado para la generación |
| `usage` | object | Información de uso de tokens |
### `perplexity_search`
@@ -76,8 +77,7 @@ Obtén resultados de búsqueda clasificados de Perplexity
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Resultados de búsqueda |
| `results` | array | Array de resultados de búsqueda |
## Notas

View File

@@ -35,9 +35,9 @@ Procesa un pensamiento/instrucción proporcionado, haciéndolo disponible para l
#### Entrada
| Parámetro | Tipo | Requerido | Descripción |
| Parámetro | Tipo | Obligatorio | Descripción |
| --------- | ---- | -------- | ----------- |
| `thought` | string | Sí | El proceso de pensamiento o instrucción proporcionado por el usuario en el bloque de Paso de Pensamiento. |
| `thought` | string | Sí | Tu razonamiento interno, análisis o proceso de pensamiento. Utiliza esto para analizar el problema paso a paso antes de responder. |
#### Salida

View File

@@ -31,37 +31,30 @@ Integra Translate en el flujo de trabajo. Puede traducir texto a cualquier idiom
## Herramientas
### `openai_chat`
### `llm_chat`
Envía una solicitud de completado de chat a cualquier proveedor de LLM compatible
#### Entrada
| Parámetro | Tipo | Requerido | Descripción |
| Parámetro | Tipo | Obligatorio | Descripción |
| --------- | ---- | -------- | ----------- |
| `model` | string | Sí | El modelo a utilizar \(p. ej., gpt-4o, claude-sonnet-4-5, gemini-2.0-flash\) |
| `systemPrompt` | string | No | Prompt del sistema para establecer el comportamiento del asistente |
| `context` | string | Sí | El mensaje del usuario o contexto para enviar al modelo |
| `apiKey` | string | No | Clave API para el proveedor \(usa la clave de la plataforma si no se proporciona para modelos alojados\) |
| `temperature` | number | No | Temperatura para la generación de respuestas \(0-2\) |
| `maxTokens` | number | No | Tokens máximos en la respuesta |
| `azureEndpoint` | string | No | URL del endpoint de Azure OpenAI |
| `azureApiVersion` | string | No | Versión de la API de Azure OpenAI |
#### Salida
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `content` | string | Texto traducido |
| `model` | string | Modelo utilizado |
| `tokens` | json | Uso de tokens |
### `anthropic_chat`
### `google_chat`
#### Entrada
| Parámetro | Tipo | Requerido | Descripción |
| --------- | ---- | -------- | ----------- |
#### Salida
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `content` | string | Texto traducido |
| `model` | string | Modelo utilizado |
| `tokens` | json | Uso de tokens |
| `content` | string | El contenido de la respuesta generada |
| `model` | string | El modelo utilizado para la generación |
| `tokens` | object | Información de uso de tokens |
## Notas

View File

@@ -251,12 +251,12 @@ Subir un archivo multimedia (imagen, video, documento) a WordPress.com
| Parámetro | Tipo | Obligatorio | Descripción |
| --------- | ---- | -------- | ----------- |
| `siteId` | string | Sí | ID del sitio o dominio de WordPress.com \(p. ej., 12345678 o misitio.wordpress.com\) |
| `file` | string | | Datos del archivo codificados en Base64 o URL para obtener el archivo |
| `filename` | string | | Nombre del archivo con extensión \(p. ej., imagen.jpg\) |
| `title` | string | No | Título del archivo multimedia |
| `caption` | string | No | Leyenda del archivo multimedia |
| `file` | file | No | Archivo para subir \(objeto UserFile\) |
| `filename` | string | No | Anulación opcional del nombre de archivo \(p. ej., imagen.jpg\) |
| `title` | string | No | Título del medio |
| `caption` | string | No | Leyenda del medio |
| `altText` | string | No | Texto alternativo para accesibilidad |
| `description` | string | No | Descripción del archivo multimedia |
| `description` | string | No | Descripción del medio |
#### Salida

View File

@@ -170,28 +170,9 @@ Obtener videos de una lista de reproducción de YouTube.
| --------- | ---- | ----------- |
| `items` | array | Array de videos en la lista de reproducción |
### `youtube_related_videos`
Encuentra videos relacionados con un video específico de YouTube.
#### Entrada
| Parámetro | Tipo | Obligatorio | Descripción |
| --------- | ---- | ----------- | ----------- |
| `videoId` | string | Sí | ID del video de YouTube para encontrar videos relacionados |
| `maxResults` | number | No | Número máximo de videos relacionados a devolver \(1-50\) |
| `pageToken` | string | No | Token de página para paginación |
| `apiKey` | string | Sí | Clave API de YouTube |
#### Salida
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `items` | array | Array de videos relacionados |
### `youtube_comments`
Obtiene comentarios de un video de YouTube.
Obtener comentarios de un video de YouTube.
#### Entrada

View File

@@ -73,8 +73,9 @@ Recupera una lista de tickets de Zendesk con filtrado opcional
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Datos y metadatos de los tickets |
| `tickets` | array | Array de objetos de ticket |
| `paging` | object | Información de paginación |
| `metadata` | object | Metadatos de la operación |
### `zendesk_get_ticket`
@@ -93,8 +94,8 @@ Obtener un solo ticket por ID desde Zendesk
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Datos del ticket |
| `ticket` | object | Objeto de ticket |
| `metadata` | object | Metadatos de la operación |
### `zendesk_create_ticket`
@@ -122,8 +123,8 @@ Crear un nuevo ticket en Zendesk con soporte para campos personalizados
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Datos del ticket creado |
| `ticket` | object | Objeto de ticket creado |
| `metadata` | object | Metadatos de la operación |
### `zendesk_create_tickets_bulk`
@@ -142,8 +143,8 @@ Crear múltiples tickets en Zendesk a la vez (máximo 100)
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Estado del trabajo de creación masiva |
| `jobStatus` | object | Objeto de estado del trabajo |
| `metadata` | object | Metadatos de la operación |
### `zendesk_update_ticket`
@@ -171,8 +172,8 @@ Actualizar un ticket existente en Zendesk con soporte para campos personalizados
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Datos actualizados del ticket |
| `ticket` | object | Objeto de ticket actualizado |
| `metadata` | object | Metadatos de la operación |
### `zendesk_update_tickets_bulk`
@@ -196,8 +197,8 @@ Actualizar múltiples tickets en Zendesk a la vez (máximo 100)
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Estado del trabajo de actualización masiva |
| `jobStatus` | object | Objeto de estado del trabajo |
| `metadata` | object | Metadatos de la operación |
### `zendesk_delete_ticket`
@@ -216,8 +217,8 @@ Eliminar un ticket de Zendesk
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Confirmación de eliminación |
| `deleted` | boolean | Éxito de la eliminación |
| `metadata` | object | Metadatos de la operación |
### `zendesk_merge_tickets`
@@ -238,8 +239,8 @@ Fusionar múltiples tickets en un ticket objetivo
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Estado del trabajo de fusión |
| `jobStatus` | object | Objeto de estado del trabajo |
| `metadata` | object | Metadatos de la operación |
### `zendesk_get_users`
@@ -261,8 +262,9 @@ Recuperar una lista de usuarios de Zendesk con filtrado opcional
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Datos de usuarios y metadatos |
| `users` | array | Array de objetos de usuario |
| `paging` | object | Información de paginación |
| `metadata` | object | Metadatos de la operación |
### `zendesk_get_user`
@@ -281,8 +283,8 @@ Obtener un solo usuario por ID desde Zendesk
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Datos del usuario |
| `user` | object | Objeto de usuario |
| `metadata` | object | Metadatos de la operación |
### `zendesk_get_current_user`
@@ -300,8 +302,8 @@ Obtener el usuario actualmente autenticado desde Zendesk
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Datos del usuario actual |
| `user` | object | Objeto del usuario actual |
| `metadata` | object | Metadatos de la operación |
### `zendesk_search_users`
@@ -323,8 +325,9 @@ Buscar usuarios en Zendesk usando una cadena de consulta
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Resultados de búsqueda de usuarios |
| `users` | array | Array de objetos de usuario |
| `paging` | object | Información de paginación |
| `metadata` | object | Metadatos de la operación |
### `zendesk_create_user`
@@ -350,8 +353,8 @@ Crear un nuevo usuario en Zendesk
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Datos del usuario creado |
| `user` | object | Objeto del usuario creado |
| `metadata` | object | Metadatos de la operación |
### `zendesk_create_users_bulk`
@@ -370,8 +373,8 @@ Crear múltiples usuarios en Zendesk mediante importación masiva
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Estado del trabajo de creación masiva |
| `jobStatus` | object | Objeto de estado del trabajo |
| `metadata` | object | Metadatos de la operación |
### `zendesk_update_user`
@@ -398,8 +401,8 @@ Actualizar un usuario existente en Zendesk
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Datos actualizados del usuario |
| `user` | object | Objeto del usuario actualizado |
| `metadata` | object | Metadatos de la operación |
### `zendesk_update_users_bulk`
@@ -418,8 +421,8 @@ Actualizar múltiples usuarios en Zendesk usando actualización masiva
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Estado del trabajo de actualización masiva |
| `jobStatus` | object | Objeto de estado del trabajo |
| `metadata` | object | Metadatos de la operación |
### `zendesk_delete_user`
@@ -438,8 +441,8 @@ Eliminar un usuario de Zendesk
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Datos del usuario eliminado |
| `deleted` | boolean | Éxito de eliminación |
| `metadata` | object | Metadatos de la operación |
### `zendesk_get_organizations`
@@ -459,8 +462,9 @@ Obtener una lista de organizaciones de Zendesk
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Datos y metadatos de las organizaciones |
| `organizations` | array | Array de objetos de organización |
| `paging` | object | Información de paginación |
| `metadata` | object | Metadatos de la operación |
### `zendesk_get_organization`
@@ -479,8 +483,8 @@ Obtener una única organización por ID desde Zendesk
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Datos de la organización |
| `organization` | object | Objeto de organización |
| `metadata` | object | Metadatos de la operación |
### `zendesk_autocomplete_organizations`
@@ -501,8 +505,9 @@ Autocompletar organizaciones en Zendesk por prefijo de nombre (para coincidencia
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Resultados de búsqueda de organizaciones |
| `organizations` | array | Array de objetos de organización |
| `paging` | object | Información de paginación |
| `metadata` | object | Metadatos de la operación |
### `zendesk_create_organization`
@@ -526,8 +531,8 @@ Crear una nueva organización en Zendesk
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Datos de la organización creada |
| `organization` | object | Objeto de organización creada |
| `metadata` | object | Metadatos de la operación |
### `zendesk_create_organizations_bulk`
@@ -546,8 +551,8 @@ Crear múltiples organizaciones en Zendesk mediante importación masiva
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Estado del trabajo de creación masiva |
| `jobStatus` | object | Objeto de estado del trabajo |
| `metadata` | object | Metadatos de la operación |
### `zendesk_update_organization`
@@ -572,8 +577,8 @@ Actualizar una organización existente en Zendesk
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Datos de la organización actualizada |
| `organization` | object | Objeto de organización actualizada |
| `metadata` | object | Metadatos de la operación |
### `zendesk_delete_organization`
@@ -592,8 +597,8 @@ Eliminar una organización de Zendesk
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Datos de la organización eliminada |
| `deleted` | boolean | Éxito de eliminación |
| `metadata` | object | Metadatos de la operación |
### `zendesk_search`
@@ -616,8 +621,9 @@ Búsqueda unificada a través de tickets, usuarios y organizaciones en Zendesk
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Resultados de la búsqueda |
| `results` | array | Array de objetos de resultado |
| `paging` | object | Información de paginación |
| `metadata` | object | Metadatos de la operación |
### `zendesk_search_count`
@@ -636,8 +642,8 @@ Contar el número de resultados de búsqueda que coinciden con una consulta en Z
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `success` | boolean | Estado de éxito de la operación |
| `output` | object | Resultado del recuento de búsqueda |
| `count` | number | Número de resultados coincidentes |
| `metadata` | object | Metadatos de la operación |
## Notas

View File

@@ -42,15 +42,15 @@ Recherchez sur le web en utilisant Exa AI. Renvoie des résultats de recherche p
| --------- | ---- | ---------- | ----------- |
| `query` | chaîne | Oui | La requête de recherche à exécuter |
| `numResults` | nombre | Non | Nombre de résultats à retourner \(par défaut : 10, max : 25\) |
| `useAutoprompt` | booléen | Non | Utiliser l'autoprompt pour améliorer la requête \(par défaut : false\) |
| `useAutoprompt` | booléen | Non | Utiliser ou non l'autoprompt pour améliorer la requête \(par défaut : false\) |
| `type` | chaîne | Non | Type de recherche : neural, keyword, auto ou fast \(par défaut : auto\) |
| `includeDomains` | chaîne | Non | Liste de domaines à inclure dans les résultats, séparés par des virgules |
| `excludeDomains` | chaîne | Non | Liste de domaines à exclure des résultats, séparés par des virgules |
| `category` | chaîne | Non | Filtrer par catégorie : company, research_paper, news_article, pdf, github, tweet, movie, song, personal_site |
| `category` | chaîne | Non | Filtrer par catégorie : company, research paper, news, pdf, github, tweet, personal site, linkedin profile, financial report |
| `text` | booléen | Non | Inclure le contenu textuel complet dans les résultats \(par défaut : false\) |
| `highlights` | booléen | Non | Inclure des extraits surlignés dans les résultats \(par défaut : false\) |
| `summary` | booléen | Non | Inclure des résumés générés par IA dans les résultats \(par défaut : false\) |
| `livecrawl` | chaîne | Non | Mode d'exploration en direct : always, fallback, ou never \(par défaut : never\) |
| `livecrawl` | chaîne | Non | Mode d'exploration en direct : never \(par défaut\), fallback, always, ou preferred \(toujours essayer l'exploration en direct, revenir au cache en cas d'échec\) |
| `apiKey` | chaîne | Oui | Clé API Exa AI |
#### Sortie
@@ -67,13 +67,13 @@ Récupérer le contenu des pages web en utilisant Exa AI. Renvoie le titre, le c
| Paramètre | Type | Obligatoire | Description |
| --------- | ---- | ---------- | ----------- |
| `urls` | chaîne | Oui | Liste d'URLs séparées par des virgules pour récupérer du contenu |
| `text` | booléen | Non | Si vrai, renvoie le texte complet de la page avec les paramètres par défaut. Si faux, désactive le retour de texte. |
| `urls` | chaîne | Oui | Liste d'URLs séparées par des virgules pour récupérer le contenu |
| `text` | booléen | Non | Si true, renvoie le texte complet de la page avec les paramètres par défaut. Si false, désactive le retour du texte. |
| `summaryQuery` | chaîne | Non | Requête pour guider la génération du résumé |
| `subpages` | nombre | Non | Nombre de sous-pages à explorer à partir des URLs fournies |
| `subpageTarget` | chaîne | Non | Mots-clés séparés par des virgules pour cibler des sous-pages spécifiques \(ex. : "docs,tutorial,about"\) |
| `subpageTarget` | chaîne | Non | Mots-clés séparés par des virgules pour cibler des sous-pages spécifiques \(par exemple, "docs,tutorial,about"\) |
| `highlights` | booléen | Non | Inclure des extraits surlignés dans les résultats \(par défaut : false\) |
| `livecrawl` | chaîne | Non | Mode d'exploration en direct : always, fallback, ou never \(par défaut : never\) |
| `livecrawl` | chaîne | Non | Mode d'exploration en direct : never \(par défaut\), fallback, always, ou preferred \(toujours essayer l'exploration en direct, revenir au cache en cas d'échec\) |
| `apiKey` | chaîne | Oui | Clé API Exa AI |
#### Sortie
@@ -96,10 +96,9 @@ Trouvez des pages web similaires à une URL donnée en utilisant Exa AI. Renvoie
| `includeDomains` | chaîne | Non | Liste de domaines à inclure dans les résultats, séparés par des virgules |
| `excludeDomains` | chaîne | Non | Liste de domaines à exclure des résultats, séparés par des virgules |
| `excludeSourceDomain` | booléen | Non | Exclure le domaine source des résultats \(par défaut : false\) |
| `category` | chaîne | Non | Filtrer par catégorie : company, research_paper, news_article, pdf, github, tweet, movie, song, personal_site |
| `highlights` | booléen | Non | Inclure des extraits surlignés dans les résultats \(par défaut : false\) |
| `summary` | booléen | Non | Inclure des résumés générés par IA dans les résultats \(par défaut : false\) |
| `livecrawl` | chaîne | Non | Mode d'exploration en direct : always, fallback, ou never \(par défaut : never\) |
| `livecrawl` | chaîne | Non | Mode d'exploration en direct : never \(par défaut\), fallback, always, ou preferred \(toujours essayer l'exploration en direct, revenir au cache en cas d'échec\) |
| `apiKey` | chaîne | Oui | Clé API Exa AI |
#### Sortie

View File

@@ -91,11 +91,11 @@ Menez des recherches approfondies complètes sur le web en utilisant Parallel AI
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `status` | string | Statut de la tâche (en cours, terminée, échouée) |
| `status` | string | Statut de la tâche (terminée, échouée) |
| `run_id` | string | ID unique pour cette tâche de recherche |
| `message` | string | Message de statut (pour les tâches en cours) |
| `message` | string | Message de statut |
| `content` | object | Résultats de recherche (structurés selon output_schema) |
| `basis` | array | Citations et sources avec extraits et niveaux de confiance |
| `basis` | array | Citations et sources avec raisonnement et niveaux de confiance |
## Notes

View File

@@ -51,8 +51,9 @@ Générez des compléments à l'aide des modèles de chat Perplexity AI
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Résultats du complément de chat |
| `content` | string | Contenu de texte généré |
| `model` | string | Modèle utilisé pour la génération |
| `usage` | object | Informations sur l'utilisation des tokens |
### `perplexity_search`
@@ -76,8 +77,7 @@ Obtenez des résultats de recherche classés depuis Perplexity
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Résultats de recherche |
| `results` | array | Tableau dessultats de recherche |
## Notes

View File

@@ -36,8 +36,8 @@ Traite une réflexion/instruction fournie, la rendant disponible pour les étape
#### Entrée
| Paramètre | Type | Obligatoire | Description |
| --------- | ---- | ----------- | ----------- |
| `thought` | chaîne | Oui | Le processus de réflexion ou l'instruction fournie par l'utilisateur dans le bloc Étape de Réflexion. |
| --------- | ---- | -------- | ----------- |
| `thought` | chaîne | Oui | Votre raisonnement interne, analyse ou processus de réflexion. Utilisez ceci pour réfléchir au problème étape par étape avant de répondre. |
#### Sortie

View File

@@ -31,37 +31,30 @@ Intégrez Translate dans le flux de travail. Peut traduire du texte dans n'impor
## Outils
### `openai_chat`
### `llm_chat`
Envoyez une requête de complétion de chat à n'importe quel fournisseur de LLM pris en charge
#### Entrée
| Paramètre | Type | Obligatoire | Description |
| --------- | ---- | ----------- | ----------- |
| --------- | ---- | ---------- | ----------- |
| `model` | chaîne | Oui | Le modèle à utiliser (ex. : gpt-4o, claude-sonnet-4-5, gemini-2.0-flash) |
| `systemPrompt` | chaîne | Non | Instruction système pour définir le comportement de l'assistant |
| `context` | chaîne | Oui | Le message utilisateur ou le contexte à envoyer au modèle |
| `apiKey` | chaîne | Non | Clé API pour le fournisseur (utilise la clé de plateforme si non fournie pour les modèles hébergés) |
| `temperature` | nombre | Non | Température pour la génération de réponse (0-2) |
| `maxTokens` | nombre | Non | Nombre maximum de tokens dans la réponse |
| `azureEndpoint` | chaîne | Non | URL du point de terminaison Azure OpenAI |
| `azureApiVersion` | chaîne | Non | Version de l'API Azure OpenAI |
#### Sortie
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Texte traduit |
| `model` | string | Modèle utilisé |
| `tokens` | json | Utilisation des tokens |
### `anthropic_chat`
### `google_chat`
#### Entrée
| Paramètre | Type | Obligatoire | Description |
| --------- | ---- | ----------- | ----------- |
#### Sortie
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | Texte traduit |
| `model` | string | Modèle utilisé |
| `tokens` | json | Utilisation des jetons |
| `content` | chaîne | Le contenu de la réponse générée |
| `model` | chaîne | Le modèle utilisé pour la génération |
| `tokens` | objet | Informations sur l'utilisation des tokens |
## Notes

View File

@@ -250,13 +250,13 @@ Télécharger un fichier média (image, vidéo, document) sur WordPress.com
| Paramètre | Type | Obligatoire | Description |
| --------- | ---- | -------- | ----------- |
| `siteId` | string | Oui | ID du site WordPress.com ou domaine \(ex., 12345678 ou monsite.wordpress.com\) |
| `file` | string | Oui | Données du fichier encodées en Base64 ou URL pour récupérer le fichier |
| `filename` | string | Oui | Nom du fichier avec extension \(ex., image.jpg\) |
| `title` | string | Non | Titre du média |
| `caption` | string | Non | Légende du média |
| `altText` | string | Non | Texte alternatif pour l'accessibilité |
| `description` | string | Non | Description du média |
| `siteId` | chaîne | Oui | ID du site WordPress.com ou domaine \(ex., 12345678 ou monsite.wordpress.com\) |
| `file` | fichier | Non | Fichier à télécharger \(objet UserFile\) |
| `filename` | chaîne | Non | Remplacement optionnel du nom de fichier \(ex., image.jpg\) |
| `title` | chaîne | Non | Titre du média |
| `caption` | chaîne | Non | Légende du média |
| `altText` | chaîne | Non | Texte alternatif pour l'accessibilité |
| `description` | chaîne | Non | Description du média |
#### Sortie

View File

@@ -170,25 +170,6 @@ Obtenir les vidéos d'une playlist YouTube.
| --------- | ---- | ----------- |
| `items` | tableau | Tableau de vidéos dans la playlist |
### `youtube_related_videos`
Trouver des vidéos liées à une vidéo YouTube spécifique.
#### Entrée
| Paramètre | Type | Obligatoire | Description |
| --------- | ---- | ----------- | ----------- |
| `videoId` | chaîne | Oui | ID de la vidéo YouTube pour laquelle trouver des vidéos liées |
| `maxResults` | nombre | Non | Nombre maximum de vidéos liées à retourner \(1-50\) |
| `pageToken` | chaîne | Non | Jeton de page pour la pagination |
| `apiKey` | chaîne | Oui | Clé API YouTube |
#### Sortie
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `items` | tableau | Tableau de vidéos liées |
### `youtube_comments`
Obtenir les commentaires d'une vidéo YouTube.
@@ -207,7 +188,7 @@ Obtenir les commentaires d'une vidéo YouTube.
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `items` | tableau | Tableau de commentaires de la vidéo |
| `items` | tableau | Tableau des commentaires de la vidéo |
## Notes

View File

@@ -74,8 +74,9 @@ Récupérer une liste de tickets depuis Zendesk avec filtrage optionnel
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Données et métadonnées des tickets |
| `tickets` | array | Tableau d'objets ticket |
| `paging` | object | Informations de pagination |
| `metadata` | object | Métadonnées de l'opération |
### `zendesk_get_ticket`
@@ -94,8 +95,8 @@ Obtenir un ticket unique par ID depuis Zendesk
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Données du ticket |
| `ticket` | object | Objet ticket |
| `metadata` | object | Métadonnées de l'opération |
### `zendesk_create_ticket`
@@ -123,8 +124,8 @@ Créer un nouveau ticket dans Zendesk avec prise en charge des champs personnali
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Données du ticket créé |
| `ticket` | object | Objet ticket créé |
| `metadata` | object | Métadonnées de l'opération |
### `zendesk_create_tickets_bulk`
@@ -143,8 +144,8 @@ Créer plusieurs tickets dans Zendesk en une seule fois (max 100)
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Statut de la tâche de création en masse |
| `jobStatus` | object | Objet statut de la tâche |
| `metadata` | object | Métadonnées de l'opération |
### `zendesk_update_ticket`
@@ -172,8 +173,8 @@ Mettre à jour un ticket existant dans Zendesk avec prise en charge des champs p
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Données du ticket mis à jour |
| `ticket` | object | Objet ticket mis à jour |
| `metadata` | object | Métadonnées de l'opération |
### `zendesk_update_tickets_bulk`
@@ -197,8 +198,8 @@ Mettre à jour plusieurs tickets dans Zendesk en une seule fois (max 100)
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Statut de la tâche de mise à jour groupée |
| `jobStatus` | object | Objet statut de la tâche |
| `metadata` | object | Métadonnées de l'opération |
### `zendesk_delete_ticket`
@@ -217,8 +218,8 @@ Supprimer un ticket de Zendesk
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Confirmation de suppression |
| `deleted` | boolean | Succès de la suppression |
| `metadata` | object | Métadonnées de l'opération |
### `zendesk_merge_tickets`
@@ -239,8 +240,8 @@ Fusionner plusieurs tickets dans un ticket cible
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Statut de la tâche de fusion |
| `jobStatus` | object | Objet statut de la tâche |
| `metadata` | object | Métadonnées de l'opération |
### `zendesk_get_users`
@@ -262,8 +263,9 @@ Récupérer une liste d'utilisateurs de Zendesk avec filtrage optionnel
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Données utilisateurs et métadonnées |
| `users` | array | Tableau d'objets utilisateur |
| `paging` | object | Informations de pagination |
| `metadata` | object | Métadonnées de l'opération |
### `zendesk_get_user`
@@ -282,8 +284,8 @@ Obtenir un utilisateur unique par ID depuis Zendesk
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Données de l'utilisateur |
| `user` | object | Objet utilisateur |
| `metadata` | object | Métadonnées de l'opération |
### `zendesk_get_current_user`
@@ -301,8 +303,8 @@ Obtenir l'utilisateur actuellement authentifié depuis Zendesk
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Données de l'utilisateur actuel |
| `user` | object | Objet de l'utilisateur actuel |
| `metadata` | object | Métadonnées de l'opération |
### `zendesk_search_users`
@@ -324,8 +326,9 @@ Rechercher des utilisateurs dans Zendesk à l'aide d'une chaîne de requête
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | booléen | Statut de réussite de l'opération |
| `output` | objet | Résultats de recherche d'utilisateurs |
| `users` | array | Tableau d'objets utilisateur |
| `paging` | object | Informations de pagination |
| `metadata` | object | Métadonnées de l'opération |
### `zendesk_create_user`
@@ -351,8 +354,8 @@ Créer un nouvel utilisateur dans Zendesk
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Données de l'utilisateur créé |
| `user` | object | Objet utilisateur créé |
| `metadata` | object | Métadonnées de l'opération |
### `zendesk_create_users_bulk`
@@ -371,8 +374,8 @@ Créer plusieurs utilisateurs dans Zendesk en utilisant l'importation en masse
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Statut de la tâche de création en masse |
| `jobStatus` | object | Objet d'état de la tâche |
| `metadata` | object | Métadonnées de l'opération |
### `zendesk_update_user`
@@ -399,8 +402,8 @@ Mettre à jour un utilisateur existant dans Zendesk
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Données utilisateur mises à jour |
| `user` | object | Objet utilisateur mis à jour |
| `metadata` | object | Métadonnées de l'opération |
### `zendesk_update_users_bulk`
@@ -419,8 +422,8 @@ Mettre à jour plusieurs utilisateurs dans Zendesk en utilisant la mise à jour
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Statut de la tâche de mise à jour en masse |
| `jobStatus` | object | Objet d'état de la tâche |
| `metadata` | object | Métadonnées de l'opération |
### `zendesk_delete_user`
@@ -439,8 +442,8 @@ Supprimer un utilisateur de Zendesk
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Données de l'utilisateur supprimé |
| `deleted` | boolean | Succès de la suppression |
| `metadata` | object | Métadonnées de l'opération |
### `zendesk_get_organizations`
@@ -460,8 +463,9 @@ Récupérer une liste d'organisations depuis Zendesk
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | booléen | Statut de réussite de l'opération |
| `output` | objet | Données et métadonnées des organisations |
| `organizations` | array | Tableau d'objets d'organisation |
| `paging` | object | Informations de pagination |
| `metadata` | object | Métadonnées de l'opération |
### `zendesk_get_organization`
@@ -480,8 +484,8 @@ Obtenir une organisation unique par ID depuis Zendesk
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | booléen | Statut de réussite de l'opération |
| `output` | objet | Données de l'organisation |
| `organization` | object | Objet organisation |
| `metadata` | object | Métadonnées de l'opération |
### `zendesk_autocomplete_organizations`
@@ -502,8 +506,9 @@ Autocomplétion des organisations dans Zendesk par préfixe de nom (pour corresp
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Résultats de recherche des organisations |
| `organizations` | array | Tableau d'objets d'organisation |
| `paging` | object | Informations de pagination |
| `metadata` | object | Métadonnées de l'opération |
### `zendesk_create_organization`
@@ -527,8 +532,8 @@ Créer une nouvelle organisation dans Zendesk
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Données de l'organisation créée |
| `organization` | object | Objet organisation créé |
| `metadata` | object | Métadonnées de l'opération |
### `zendesk_create_organizations_bulk`
@@ -547,8 +552,8 @@ Créer plusieurs organisations dans Zendesk en utilisant l'importation en masse
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Statut de la tâche de création en masse |
| `jobStatus` | object | Objet statut de la tâche |
| `metadata` | object | Métadonnées de l'opération |
### `zendesk_update_organization`
@@ -573,8 +578,8 @@ Mettre à jour une organisation existante dans Zendesk
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Données de l'organisation mise à jour |
| `organization` | object | Objet organisation mis à jour |
| `metadata` | object | Métadonnées de l'opération |
### `zendesk_delete_organization`
@@ -593,8 +598,8 @@ Supprimer une organisation de Zendesk
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Données de l'organisation supprimée |
| `deleted` | boolean | Succès de la suppression |
| `metadata` | object | Métadonnées de l'opération |
### `zendesk_search`
@@ -617,8 +622,9 @@ Recherche unifiée à travers les tickets, utilisateurs et organisations dans Ze
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Résultats de recherche |
| `results` | array | Tableau d'objets résultats |
| `paging` | object | Informations de pagination |
| `metadata` | object | Métadonnées de l'opération |
### `zendesk_search_count`
@@ -637,8 +643,8 @@ Compter le nombre de résultats de recherche correspondant à une requête dans
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Statut de réussite de l'opération |
| `output` | object | Résultat du comptage de recherche |
| `count` | number | Nombre de résultats correspondants |
| `metadata` | object | Métadonnées de l'opération |
## Notes

View File

@@ -46,11 +46,11 @@ Exa AIを使用してウェブを検索します。タイトル、URL、テキ
| `type` | string | いいえ | 検索タイプneural、keyword、auto、またはfastデフォルトauto |
| `includeDomains` | string | いいえ | 結果に含めるドメインのカンマ区切りリスト |
| `excludeDomains` | string | いいえ | 結果から除外するドメインのカンマ区切りリスト |
| `category` | string | いいえ | カテゴリによるフィルタリングcompany、research_paper、news_article、pdf、github、tweet、movie、song、personal_site |
| `category` | string | いいえ | カテゴリによるフィルタリングcompany、research paper、news、pdf、github、tweet、personal site、linkedin profile、financial report |
| `text` | boolean | いいえ | 結果に全文コンテンツを含めるデフォルトfalse |
| `highlights` | boolean | いいえ | 結果にハイライトされたスニペットを含めるデフォルトfalse |
| `summary` | boolean | いいえ | 結果にAI生成の要約を含めるデフォルトfalse |
| `livecrawl` | string | いいえ | ライブクロールモード:always、fallback、またはneverデフォルトnever |
| `livecrawl` | string | いいえ | ライブクロールモード:neverデフォルト、fallback、always、またはpreferred常にライブクロールを試み、失敗した場合はキャッシュにフォールバック |
| `apiKey` | string | はい | Exa AI APIキー |
#### 出力
@@ -68,12 +68,12 @@ Exa AIを使用してウェブページのコンテンツを取得します。
| パラメータ | 型 | 必須 | 説明 |
| --------- | ---- | -------- | ----------- |
| `urls` | string | はい | コンテンツを取得するURLのカンマ区切りリスト |
| `text` | boolean | いいえ | trueの場合、デフォルト設定でページ全文を返します。falseの場合、テキスト返却を無効にします。 |
| `text` | boolean | いいえ | trueの場合、デフォルト設定で完全なページテキストを返します。falseの場合、テキスト返却を無効にします。 |
| `summaryQuery` | string | いいえ | 要約生成をガイドするクエリ |
| `subpages` | number | いいえ | 提供されたURLからクロールするサブページの数 |
| `subpageTarget` | string | いいえ | 特定のサブページを対象とするカンマ区切りキーワード(例:"docs,tutorial,about" |
| `subpageTarget` | string | いいえ | 特定のサブページをターゲットにするためのカンマ区切りキーワード(例:"docs,tutorial,about" |
| `highlights` | boolean | いいえ | 結果にハイライトされたスニペットを含めるデフォルトfalse |
| `livecrawl` | string | いいえ | ライブクロールモード:always、fallback、またはneverデフォルトnever |
| `livecrawl` | string | いいえ | ライブクロールモード:neverデフォルト、fallback、always、またはpreferred常にライブクロールを試み、失敗した場合はキャッシュにフォールバック |
| `apiKey` | string | はい | Exa AI APIキー |
#### 出力
@@ -96,10 +96,9 @@ Exa AIを使用して、指定されたURLに類似したウェブページを
| `includeDomains` | string | いいえ | 結果に含めるドメインのカンマ区切りリスト |
| `excludeDomains` | string | いいえ | 結果から除外するドメインのカンマ区切りリスト |
| `excludeSourceDomain` | boolean | いいえ | 結果からソースドメインを除外するデフォルトfalse |
| `category` | string | いいえ | カテゴリによるフィルタリングcompany、research_paper、news_article、pdf、github、tweet、movie、song、personal_site |
| `highlights` | boolean | いいえ | 結果にハイライトされたスニペットを含めるデフォルトfalse |
| `summary` | boolean | いいえ | 結果にAI生成の要約を含めるデフォルトfalse |
| `livecrawl` | string | いいえ | ライブクロールモード:always、fallback、またはneverデフォルトnever |
| `livecrawl` | string | いいえ | ライブクロールモード:neverデフォルト、fallback、always、またはpreferred常にライブクロールを試み、失敗した場合はキャッシュにフォールバック |
| `apiKey` | string | はい | Exa AI APIキー |
#### 出力

View File

@@ -91,11 +91,11 @@ Parallel AIを使用してウェブ全体で包括的な詳細調査を実施し
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `status` | string | タスクのステータス(実行中、完了、失敗) |
| `status` | string | タスクのステータス(完了、失敗) |
| `run_id` | string | この調査タスクの一意のID |
| `message` | string | ステータスメッセージ(実行中のタスク用) |
| `message` | string | ステータスメッセージ |
| `content` | object | 調査結果output_schemaに基づいて構造化 |
| `basis` | array | 引用と情報源(抜粋と信頼度レベルを含む) |
| `basis` | array | 引用と情報源(根拠と信頼度レベルを含む) |
## 注意事項

View File

@@ -51,8 +51,9 @@ Perplexity AIチャットモデルを使用して文章を生成する
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作の成功ステータス |
| `output` | object | チャット生成結果 |
| `content` | string | 生成されたテキストコンテンツ |
| `model` | string | 生成に使用されたモデル |
| `usage` | object | トークン使用情報 |
### `perplexity_search`
@@ -76,8 +77,7 @@ Perplexityからランク付けされた検索結果を取得する
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作の成功ステータス |
| `output` | object | 検索結果 |
| `results` | array | 検索結果の配列 |
## 注意事項

View File

@@ -37,7 +37,7 @@ Simでは、思考ツールによってエージェントがこのような意
| パラメータ | 型 | 必須 | 説明 |
| --------- | ---- | -------- | ----------- |
| `thought` | string | はい | 思考ステップブロックでユーザーが提供した思考プロセスまたは指示。 |
| `thought` | string | はい | あなたの内部的な推論、分析、または思考プロセス。これを使用して、応答する前に問題を段階的に考えてください。 |
#### 出力

View File

@@ -31,39 +31,32 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
## ツール
### `openai_chat`
### `llm_chat`
サポートされている任意のLLMプロバイダーにチャット完了リクエストを送信する
#### 入力
| パラメータ | 型 | 必須 | 説明 |
| --------- | ---- | -------- | ----------- |
| `model` | string | はい | 使用するモデルgpt-4o、claude-sonnet-4-5、gemini-2.0-flash |
| `systemPrompt` | string | いいえ | アシスタントの動作を設定するシステムプロンプト |
| `context` | string | はい | モデルに送信するユーザーメッセージまたはコンテキスト |
| `apiKey` | string | いいえ | プロバイダーのAPIキーホストされたモデルの場合、提供されなければプラットフォームキーを使用 |
| `temperature` | number | いいえ | レスポンス生成の温度0-2 |
| `maxTokens` | number | いいえ | レスポンスの最大トークン数 |
| `azureEndpoint` | string | いいえ | Azure OpenAIエンドポイントURL |
| `azureApiVersion` | string | いいえ | Azure OpenAI APIバージョン |
#### 出力
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `content` | string | 翻訳されたテキスト |
| `model` | string | 使用されたモデル |
| `tokens` | json | トークン使用 |
| `content` | string | 生成されたレスポンスの内容 |
| `model` | string | 生成に使用されたモデル |
| `tokens` | object | トークン使用情報 |
### `anthropic_chat`
### `google_chat`
#### 入力
| パラメータ | 型 | 必須 | 説明 |
| --------- | ---- | -------- | ----------- |
#### 出力
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `content` | string | 翻訳されたテキスト |
| `model` | string | 使用されたモデル |
| `tokens` | json | トークン使用量 |
## メモ
## 注意事項
- カテゴリー: `tools`
- タイプ: `translate`

View File

@@ -251,10 +251,10 @@ IDによってWordPress.comから単一のページを取得する
| パラメータ | 型 | 必須 | 説明 |
| --------- | ---- | -------- | ----------- |
| `siteId` | string | はい | WordPress.comのサイトIDまたはドメイン \(例12345678またはmysite.wordpress.com\) |
| `file` | string | はい | Base64エンコードされたファイルデータまたはファイルを取得するURL |
| `filename` | string | はい | 拡張子付きのファイル名 \(image.jpg\) |
| `title` | string | いいえ | メディアタイトル |
| `caption` | string | いいえ | メディアキャプション |
| `file` | file | いいえ | アップロードするファイルUserFileオブジェクト |
| `filename` | string | いいえ | オプションのファイル名上書き(image.jpg |
| `title` | string | いいえ | メディアタイトル |
| `caption` | string | いいえ | メディアキャプション |
| `altText` | string | いいえ | アクセシビリティのための代替テキスト |
| `description` | string | いいえ | メディアの説明 |

View File

@@ -170,25 +170,6 @@ YouTube再生リストから動画を取得します。
| --------- | ---- | ----------- |
| `items` | array | プレイリスト内の動画の配列 |
### `youtube_related_videos`
特定のYouTube動画に関連する動画を検索します。
#### 入力
| パラメータ | 型 | 必須 | 説明 |
| --------- | ---- | -------- | ----------- |
| `videoId` | string | はい | 関連動画を検索するYouTube動画ID |
| `maxResults` | number | いいえ | 返す関連動画の最大数1-50 |
| `pageToken` | string | いいえ | ページネーション用のページトークン |
| `apiKey` | string | はい | YouTube APIキー |
#### 出力
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `items` | array | 関連動画の配列 |
### `youtube_comments`
YouTube動画からコメントを取得します。
@@ -199,7 +180,7 @@ YouTube動画からコメントを取得します。
| --------- | ---- | -------- | ----------- |
| `videoId` | string | はい | YouTube動画ID |
| `maxResults` | number | いいえ | 返すコメントの最大数 |
| `order` | string | いいえ | コメントの並び順timeまたはrelevance |
| `order` | string | いいえ | コメントの並び順time(時間順)またはrelevance(関連性順) |
| `pageToken` | string | いいえ | ページネーション用のページトークン |
| `apiKey` | string | はい | YouTube APIキー |
@@ -211,5 +192,5 @@ YouTube動画からコメントを取得します。
## 注意事項
- カテゴリ: `tools`
- カテゴリ: `tools`
- タイプ: `youtube`

View File

@@ -73,8 +73,9 @@ Zendeskをワークフローに統合します。チケットの取得、チケ
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功ステータス |
| `output` | object | チケットデータとメタデータ |
| `tickets` | array | チケットオブジェクトの配列 |
| `paging` | object | ページネーション情報 |
| `metadata` | object | 操作メタデータ |
### `zendesk_get_ticket`
@@ -93,8 +94,8 @@ IDによってZendeskから単一のチケットを取得
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功ステータス |
| `output` | object | チケットデータ |
| `ticket` | object | チケットオブジェクト |
| `metadata` | object | 操作メタデータ |
### `zendesk_create_ticket`
@@ -122,8 +123,8 @@ IDによってZendeskから単一のチケットを取得
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 作成功ステータス |
| `output` | object | 作成されたチケットデータ |
| `ticket` | object | 作成されたチケットオブジェクト |
| `metadata` | object | 操作メタデータ |
### `zendesk_create_tickets_bulk`
@@ -142,8 +143,8 @@ Zendeskで一度に複数のチケットを作成最大100件
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功ステータス |
| `output` | object | 一括作成ジョブステータ |
| `jobStatus` | object | ジョブステータスオブジェクト |
| `metadata` | object | 操作メタデータ |
### `zendesk_update_ticket`
@@ -171,8 +172,8 @@ Zendeskで一度に複数のチケットを作成最大100件
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功ステータス |
| `output` | object | 更新されたチケットデータ |
| `ticket` | object | 更新されたチケットオブジェクト |
| `metadata` | object | 操作メタデータ |
### `zendesk_update_tickets_bulk`
@@ -196,8 +197,8 @@ Zendeskで複数のチケットを一度に更新最大100件
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功ステータス |
| `output` | object | 一括更新ジョブのステータ |
| `jobStatus` | object | ジョブステータスオブジェクト |
| `metadata` | object | 操作メタデータ |
### `zendesk_delete_ticket`
@@ -216,8 +217,8 @@ Zendeskからチケットを削除
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功ステータス |
| `output` | object | 削除確認 |
| `deleted` | boolean | 削除成功 |
| `metadata` | object | 操作メタデータ |
### `zendesk_merge_tickets`
@@ -238,8 +239,8 @@ Zendeskからチケットを削除
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功ステータス |
| `output` | object | 統合ジョブステータ |
| `jobStatus` | object | ジョブステータスオブジェクト |
| `metadata` | object | 操作メタデータ |
### `zendesk_get_users`
@@ -261,8 +262,9 @@ Zendeskからチケットを削除
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功ステータス |
| `output` | object | ユーザーデータとメタデータ |
| `users` | array | ユーザーオブジェクトの配列 |
| `paging` | object | ページネーション情報 |
| `metadata` | object | 操作メタデータ |
### `zendesk_get_user`
@@ -281,8 +283,8 @@ ZendeskからIDで単一のユーザーを取得
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功ステータス |
| `output` | object | ユーザーデータ |
| `user` | object | ユーザーオブジェクト |
| `metadata` | object | 操作メタデータ |
### `zendesk_get_current_user`
@@ -300,8 +302,8 @@ Zendeskから現在認証されているユーザーを取得
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功ステータス |
| `output` | object | 現在のユーザーデータ |
| `user` | object | 現在のユーザーオブジェクト |
| `metadata` | object | 操作メタデータ |
### `zendesk_search_users`
@@ -323,8 +325,9 @@ Zendeskから現在認証されているユーザーを取得
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功ステータス |
| `output` | object | ユーザー検索結果 |
| `users` | array | ユーザーオブジェクトの配列 |
| `paging` | object | ページネーション情報 |
| `metadata` | object | 操作メタデータ |
### `zendesk_create_user`
@@ -350,8 +353,8 @@ Zendeskに新しいユーザーを作成する
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 作成功ステータス |
| `output` | object | 作成されたユーザーデータ |
| `user` | object | 作成されたユーザーオブジェクト |
| `metadata` | object | 操作メタデータ |
### `zendesk_create_users_bulk`
@@ -370,8 +373,8 @@ Zendeskに新しいユーザーを作成する
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功ステータス |
| `output` | object | 一括作成ジョブのステータ |
| `jobStatus` | object | ジョブステータスオブジェクト |
| `metadata` | object | 操作メタデータ |
### `zendesk_update_user`
@@ -398,8 +401,8 @@ Zendeskの既存ユーザーを更新する
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功ステータス |
| `output` | object | 更新されたユーザーデータ |
| `user` | object | 更新されたユーザーオブジェクト |
| `metadata` | object | 操作メタデータ |
### `zendesk_update_users_bulk`
@@ -418,8 +421,8 @@ Zendeskの既存ユーザーを更新する
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功ステータス |
| `output` | object | 一括更新ジョブステータ |
| `jobStatus` | object | ジョブステータスオブジェクト |
| `metadata` | object | 操作メタデータ |
### `zendesk_delete_user`
@@ -438,8 +441,8 @@ Zendeskからユーザーを削除する
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功ステータス |
| `output` | object | 削除されたユーザーデータ |
| `deleted` | boolean | 削除成功 |
| `metadata` | object | 操作メタデータ |
### `zendesk_get_organizations`
@@ -459,8 +462,9 @@ Zendeskから組織のリストを取得する
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功ステータス |
| `output` | object | 組織データとメタデータ |
| `organizations` | array | 組織オブジェクトの配列 |
| `paging` | object | ページネーション情報 |
| `metadata` | object | 操作メタデータ |
### `zendesk_get_organization`
@@ -479,8 +483,8 @@ ZendeskからIDで単一の組織を取得する
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功ステータス |
| `output` | object | 組織データ |
| `organization` | object | 組織オブジェクト |
| `metadata` | object | 操作メタデータ |
### `zendesk_autocomplete_organizations`
@@ -501,8 +505,9 @@ ZendeskからIDで単一の組織を取得する
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功ステータス |
| `output` | object | 組織検索結果 |
| `organizations` | array | 組織オブジェクトの配列 |
| `paging` | object | ページネーション情報 |
| `metadata` | object | 操作メタデータ |
### `zendesk_create_organization`
@@ -526,8 +531,8 @@ Zendeskで新しい組織を作成する
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 作成功ステータス |
| `output` | object | 作成された組織データ |
| `organization` | object | 作成された組織オブジェクト |
| `metadata` | object | 操作メタデータ |
### `zendesk_create_organizations_bulk`
@@ -546,8 +551,8 @@ Zendeskで新しい組織を作成する
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功ステータス |
| `output` | object | 一括作成ジョブステータ |
| `jobStatus` | object | ジョブステータスオブジェクト |
| `metadata` | object | 操作メタデータ |
### `zendesk_update_organization`
@@ -572,8 +577,8 @@ Zendeskで既存の組織を更新する
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功ステータス |
| `output` | object | 更新された組織データ |
| `organization` | object | 更新された組織オブジェクト |
| `metadata` | object | 操作メタデータ |
### `zendesk_delete_organization`
@@ -592,8 +597,8 @@ Zendeskから組織を削除する
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功ステータス |
| `output` | object | 削除された組織データ |
| `deleted` | boolean | 削除成功 |
| `metadata` | object | 操作メタデータ |
### `zendesk_search`
@@ -616,8 +621,9 @@ Zendeskでチケット、ユーザー、組織を横断した統合検索
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功ステータス |
| `output` | object | 検索結果 |
| `results` | array | 結果オブジェクトの配列 |
| `paging` | object | ページネーション情報 |
| `metadata` | object | 操作メタデータ |
### `zendesk_search_count`
@@ -636,8 +642,8 @@ Zendeskでクエリに一致する検索結果の数をカウント
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功ステータス |
| `output` | object | 検索カウント結果 |
| `count` | number | 一致する結果の数 |
| `metadata` | object | 操作メタデータ |
## 注意事項

View File

@@ -46,11 +46,11 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| `type` | string | 否 | 搜索类型neural、keyword、auto 或 fast \(默认值: auto\) |
| `includeDomains` | string | 否 | 用逗号分隔的域名列表,包含在结果中 |
| `excludeDomains` | string | 否 | 用逗号分隔的域名列表,从结果中排除 |
| `category` | string | 否 | 按类别筛选company、research_paper、news_article、pdf、github、tweet、movie、song、personal_site |
| `category` | string | 否 | 按类别筛选company、research paper、news、pdf、github、tweet、personal site、linkedin profile、financial report |
| `text` | boolean | 否 | 在结果中包含完整文本内容 \(默认值: false\) |
| `highlights` | boolean | 否 | 在结果中包含高亮片段 \(默认值: false\) |
| `summary` | boolean | 否 | 在结果中包含 AI 生成的摘要 \(默认值: false\) |
| `livecrawl` | string | 否 | 实时爬取模式:always、fallback 或 never \(默认值: never\) |
| `livecrawl` | string | 否 | 实时爬取模式:never \(默认值\)、fallback、always 或 preferred \(始终尝试实时爬取,失败时回退到缓存\) |
| `apiKey` | string | 是 | Exa AI API 密钥 |
#### 输出
@@ -67,13 +67,13 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 必需 | 描述 |
| --------- | ---- | -------- | ----------- |
| `urls` | string | 是 | 用逗号分隔的 URL 列表,用于获取内容 |
| `text` | boolean | 否 | 如果为 true则返回默认设置的完整页面文本。如果为 false则禁用文本返回。 |
| `urls` | string | 是 | 用逗号分隔的 URL 列表,用于检索内容 |
| `text` | boolean | 否 | 如果为 true则返回具有默认设置的完整页面文本。如果为 false则禁用文本返回。 |
| `summaryQuery` | string | 否 | 用于指导摘要生成的查询 |
| `subpages` | number | 否 | 从提供的 URL 爬取的子页面数量 |
| `subpages` | number | 否 | 从提供的 URL 爬取的子页面数量 |
| `subpageTarget` | string | 否 | 用逗号分隔的关键字,用于定位特定子页面 \(例如:"docs,tutorial,about"\) |
| `highlights` | boolean | 否 | 在结果中包含高亮片段 \(默认值: false\) |
| `livecrawl` | string | 否 | 实时爬取模式:always、fallback 或 never \(默认值: never\) |
| `livecrawl` | string | 否 | 实时爬取模式:never \(默认值\)、fallback、always 或 preferred \(始终尝试实时爬取,失败时回退到缓存\) |
| `apiKey` | string | 是 | Exa AI API 密钥 |
#### 输出
@@ -90,16 +90,15 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 必需 | 描述 |
| --------- | ---- | -------- | ----------- |
| `url` | string | 是 | 查找相似链接的 URL |
| `url` | string | 是 | 用于查找相似链接的 URL |
| `numResults` | number | 否 | 要返回的相似链接数量 \(默认值: 10最大值: 25\) |
| `text` | boolean | 否 | 是否包含相似页面的完整文本 |
| `includeDomains` | string | 否 | 用逗号分隔的域名列表,包含在结果中 |
| `excludeDomains` | string | 否 | 用逗号分隔的域名列表,从结果中排除 |
| `excludeSourceDomain` | boolean | 否 | 从结果中排除源域名 \(默认值: false\) |
| `category` | string | 否 | 按类别筛选company、research_paper、news_article、pdf、github、tweet、movie、song、personal_site |
| `highlights` | boolean | 否 | 在结果中包含高亮片段 \(默认值: false\) |
| `summary` | boolean | 否 | 在结果中包含 AI 生成的摘要 \(默认值: false\) |
| `livecrawl` | string | 否 | 实时爬取模式:always、fallback 或 never \(默认值: never\) |
| `livecrawl` | string | 否 | 实时爬取模式:never \(默认值\), fallback, always, 或 preferred \(始终尝试实时爬取,失败时回退到缓存\) |
| `apiKey` | string | 是 | Exa AI API 密钥 |
#### 输出

View File

@@ -91,11 +91,11 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `status` | string | 任务状态(运行中、已完成失败) |
| `status` | string | 任务状态(已完成失败) |
| `run_id` | string | 此研究任务的唯一 ID |
| `message` | string | 状态消息(针对运行中的任务) |
| `message` | string | 状态消息 |
| `content` | object | 研究结果(基于 output_schema 结构化) |
| `basis` | array | 引用和来源,包括摘录和置信度等级 |
| `basis` | array | 引用和来源,包括推理和置信度等级 |
## 注意

View File

@@ -51,8 +51,9 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 聊天补全结果 |
| `content` | string | 生成的文本内容 |
| `model` | string | 用于生成的模型 |
| `usage` | object | 令牌使用信息 |
### `perplexity_search`
@@ -76,8 +77,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 搜索结果 |
| `results` | array | 搜索结果数组 |
## 注意事项

View File

@@ -37,7 +37,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 必需 | 描述 |
| --------- | ---- | -------- | ----------- |
| `thought` | 字符串 | 是 | 用户在思考步骤模块中提供的思考过程或指令。 |
| `thought` | 字符串 | 是 | 您的内部推理、分析或思考过程。使用此项逐步思考问题,然后再作出回应。 |
#### 输出

View File

@@ -31,39 +31,32 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
## 工具
### `openai_chat`
### `llm_chat`
向任何支持的 LLM 提供商发送聊天完成请求
#### 输入
| 参数 | 类型 | 必需 | 描述 |
| --------- | ---- | -------- | ----------- |
| `model` | string | 是 | 要使用的模型 \(例如gpt-4o、claude-sonnet-4-5、gemini-2.0-flash\) |
| `systemPrompt` | string | 否 | 设置助手行为的系统提示 |
| `context` | string | 是 | 要发送给模型的用户消息或上下文 |
| `apiKey` | string | 否 | 提供商的 API 密钥 \(如果未为托管模型提供,则使用平台密钥\) |
| `temperature` | number | 否 | 响应生成的温度 \(0-2\) |
| `maxTokens` | number | 否 | 响应的最大令牌数 |
| `azureEndpoint` | string | 否 | Azure OpenAI 端点 URL |
| `azureApiVersion` | string | 否 | Azure OpenAI API 版本 |
#### 输出
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `content` | string | 翻译后的文本 |
| `model` | string | 使用的模型 |
| `tokens` | json | 令牌使用情况 |
| `content` | string | 生成的响应内容 |
| `model` | string | 用于生成的模型 |
| `tokens` | object | 令牌使用信息 |
### `anthropic_chat`
## 注意
### `google_chat`
#### 输入
| 参数 | 类型 | 必需 | 描述 |
| --------- | ---- | -------- | ----------- |
#### 输出
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `content` | string | 翻译后的文本 |
| `model` | string | 使用的模型 |
| `tokens` | json | Token 使用情况 |
## 注意事项
- 类别:`tools`
- 类型:`translate`
- 类别: `tools`
- 类型: `translate`

View File

@@ -250,13 +250,13 @@ Sim 与 WordPress 的集成让您的代理可以自动化处理重要的网站
| 参数 | 类型 | 必需 | 描述 |
| --------- | ---- | -------- | ----------- |
| `siteId` | 字符串 | 是 | WordPress.com 站点 ID 或域名例如12345678 或 mysite.wordpress.com |
| `file` | 字符串 | | Base64 编码的文件数据或用于获取文件的 URL |
| `filename` | 字符串 | | 带扩展名的文件名(例如image.jpg |
| `title` | 字符串 | 否 | 媒体标题 |
| `caption` | 字符串 | 否 | 媒体说明 |
| `altText` | 字符串 | 否 | 用于无障碍的替代文本 |
| `description` | 字符串 | 否 | 媒体描述 |
| `siteId` | string | 是 | WordPress.com 站点 ID 或域名 \(例如12345678 或 mysite.wordpress.com\) |
| `file` | file | | 要上传的文件 \(UserFile 对象\) |
| `filename` | string | | 可选的文件名覆盖 \(例如image.jpg\) |
| `title` | string | 否 | 媒体标题 |
| `caption` | string | 否 | 媒体说明 |
| `altText` | string | 否 | 用于无障碍的替代文本 |
| `description` | string | 否 | 媒体描述 |
#### 输出

View File

@@ -170,28 +170,9 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| --------- | ---- | ----------- |
| `items` | 数组 | 播放列表中的视频数组 |
### `youtube_related_videos`
查找与特定 YouTube 视频相关的视频。
#### 输入
| 参数 | 类型 | 必需 | 描述 |
| --------- | ---- | -------- | ----------- |
| `videoId` | 字符串 | 是 | 用于查找相关视频的 YouTube 视频 ID |
| `maxResults` | 数字 | 否 | 返回相关视频的最大数量 \(1-50\) |
| `pageToken` | 字符串 | 否 | 分页的页面令牌 |
| `apiKey` | 字符串 | 是 | YouTube API 密钥 |
#### 输出
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `items` | 数组 | 相关视频的数组 |
### `youtube_comments`
从 YouTube 视频获取评论。
从 YouTube 视频获取评论。
#### 输入
@@ -207,7 +188,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `items` | 数组 | 视频评论数组 |
| `items` | 数组 | 视频中的评论数组 |
## 注意事项

View File

@@ -73,8 +73,9 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 工单数据和元数据 |
| `tickets` | array | 工单对象数组 |
| `paging` | object | 分页信息 |
| `metadata` | object | 操作元数据 |
### `zendesk_get_ticket`
@@ -93,8 +94,8 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 工单数据 |
| `ticket` | object | 工单对象 |
| `metadata` | object | 操作元数据 |
### `zendesk_create_ticket`
@@ -122,8 +123,8 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 创建的工单数据 |
| `ticket` | object | 创建的工单对象 |
| `metadata` | object | 操作元数据 |
### `zendesk_create_tickets_bulk`
@@ -142,8 +143,8 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 批量创建任务状态 |
| `jobStatus` | object | 任务状态对象 |
| `metadata` | object | 操作元数据 |
### `zendesk_update_ticket`
@@ -171,8 +172,8 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 更新的工单数据 |
| `ticket` | object | 更新的工单对象 |
| `metadata` | object | 操作元数据 |
### `zendesk_update_tickets_bulk`
@@ -196,8 +197,8 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 批量更新任务状态 |
| `jobStatus` | object | 任务状态对象 |
| `metadata` | object | 操作元数据 |
### `zendesk_delete_ticket`
@@ -216,8 +217,8 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 删除确认 |
| `deleted` | boolean | 删除成功 |
| `metadata` | object | 操作元数据 |
### `zendesk_merge_tickets`
@@ -238,8 +239,8 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 合并任务状态 |
| `jobStatus` | object | 任务状态对象 |
| `metadata` | object | 操作元数据 |
### `zendesk_get_users`
@@ -261,8 +262,9 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 用户数据和元数据 |
| `users` | array | 用户对象数组 |
| `paging` | object | 分页信息 |
| `metadata` | object | 操作元数据 |
### `zendesk_get_user`
@@ -281,8 +283,8 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 用户数据 |
| `user` | object | 用户对象 |
| `metadata` | object | 操作元数据 |
### `zendesk_get_current_user`
@@ -300,8 +302,8 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 当前用户数据 |
| `user` | object | 当前用户对象 |
| `metadata` | object | 操作元数据 |
### `zendesk_search_users`
@@ -323,8 +325,9 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 用户搜索结果 |
| `users` | array | 用户对象数组 |
| `paging` | object | 分页信息 |
| `metadata` | object | 操作元数据 |
### `zendesk_create_user`
@@ -350,8 +353,8 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 创建的用户数据 |
| `user` | object | 创建的用户对象 |
| `metadata` | object | 操作元数据 |
### `zendesk_create_users_bulk`
@@ -370,8 +373,8 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 批量创建任务状态 |
| `jobStatus` | object | 任务状态对象 |
| `metadata` | object | 操作元数据 |
### `zendesk_update_user`
@@ -398,8 +401,8 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 更新的用户数据 |
| `user` | object | 更新的用户对象 |
| `metadata` | object | 操作元数据 |
### `zendesk_update_users_bulk`
@@ -418,8 +421,8 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 批量更新任务状态 |
| `jobStatus` | object | 任务状态对象 |
| `metadata` | object | 操作元数据 |
### `zendesk_delete_user`
@@ -438,8 +441,8 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 已删除的用户数据 |
| `deleted` | boolean | 删除成功 |
| `metadata` | object | 操作元数据 |
### `zendesk_get_organizations`
@@ -459,8 +462,9 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 组织数据和元数据 |
| `organizations` | array | 组织对象数组 |
| `paging` | object | 分页信息 |
| `metadata` | object | 操作元数据 |
### `zendesk_get_organization`
@@ -479,8 +483,8 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 组织数据 |
| `organization` | object | 组织对象 |
| `metadata` | object | 操作元数据 |
### `zendesk_autocomplete_organizations`
@@ -501,8 +505,9 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 组织搜索结果 |
| `organizations` | array | 组织对象数组 |
| `paging` | object | 分页信息 |
| `metadata` | object | 操作元数据 |
### `zendesk_create_organization`
@@ -526,8 +531,8 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 创建的组织数据 |
| `organization` | object | 创建的组织对象 |
| `metadata` | object | 操作元数据 |
### `zendesk_create_organizations_bulk`
@@ -546,8 +551,8 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 批量创建任务状态 |
| `jobStatus` | object | 任务状态对象 |
| `metadata` | object | 操作元数据 |
### `zendesk_update_organization`
@@ -572,8 +577,8 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 更新后的组织数据 |
| `organization` | object | 更新的组织对象 |
| `metadata` | object | 操作元数据 |
### `zendesk_delete_organization`
@@ -592,8 +597,8 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 已删除的组织数据 |
| `deleted` | boolean | 删除成功 |
| `metadata` | object | 操作元数据 |
### `zendesk_search`
@@ -616,8 +621,9 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 搜索结果 |
| `results` | array | 结果对象的数组 |
| `paging` | object | 分页信息 |
| `metadata` | object | 操作元数据 |
### `zendesk_search_count`
@@ -636,8 +642,8 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `success` | boolean | 操作成功状态 |
| `output` | object | 搜索计数结果 |
| `count` | number | 匹配结果的数量 |
| `metadata` | object | 操作元数据 |
## 注意事项

View File

@@ -278,20 +278,14 @@ checksums:
content/42: 135fd99536465a8de4cd18eae682128f
content/43: bcadfc362b69078beee0088e5936c98b
content/44: e4824dd7acff8ac2bba07075c1bf5555
content/45: 09cf2346cf5ce107fae6c9f05ff79340
content/46: 00c6430ff11748e620cfe9309f9b76d0
content/45: 29d730395e60843523997a5440265c2b
content/46: f40cf8dcdbca5bdabddd2ccafe510827
content/47: 371d0e46b4bd2c23f559b8bc112f6955
content/48: a57680d79a2db023cabd03d74a357037
content/48: fb04fb808287bc585c7bef1622cc5929
content/49: bcadfc362b69078beee0088e5936c98b
content/50: f189d5068833ff066248bdec22ac14da
content/51: 29d730395e60843523997a5440265c2b
content/52: f40cf8dcdbca5bdabddd2ccafe510827
content/53: 371d0e46b4bd2c23f559b8bc112f6955
content/54: fb04fb808287bc585c7bef1622cc5929
content/55: bcadfc362b69078beee0088e5936c98b
content/56: 766eb78674c8d8a40cfd13eca533b7a4
content/57: b3f310d5ef115bea5a8b75bf25d7ea9a
content/58: 9f8b3ebb53dbde8abfb139b4a8bc0396
content/50: 766eb78674c8d8a40cfd13eca533b7a4
content/51: b3f310d5ef115bea5a8b75bf25d7ea9a
content/52: 9f8b3ebb53dbde8abfb139b4a8bc0396
31b5f846eb7f4b1751b4bd9233cc3e12:
meta/title: dc96f9cb0985d77c30f55927723f0175
meta/description: 15c7d592f04655f534458cd82180ba1f
@@ -560,19 +554,14 @@ checksums:
content/5: 821e6394b0a953e2b0842b04ae8f3105
content/6: 3e7ad55e438e18098a384877ec5d62e7
content/7: 9c8aa3f09c9b2bd50ea4cdff3598ea4e
content/8: ad28bec163e3e627e8741423286840bd
content/9: 371d0e46b4bd2c23f559b8bc112f6955
content/10: d71b6bb8e2dd6ce98101aec6a1dd77f2
content/11: bcadfc362b69078beee0088e5936c98b
content/12: adefb6abb806004812f51485919b8bf5
content/13: 6f45ac035872e548e43385c22ddac507
content/14: 8e7b622079252125355db846fbde9d96
content/15: 371d0e46b4bd2c23f559b8bc112f6955
content/16: d71b6bb8e2dd6ce98101aec6a1dd77f2
content/17: bcadfc362b69078beee0088e5936c98b
content/18: adefb6abb806004812f51485919b8bf5
content/19: b3f310d5ef115bea5a8b75bf25d7ea9a
content/20: 2bcdb5144797b6a7dd2d266331c9b95e
content/8: 6325adefb6e1520835225285b18b6a45
content/9: b7fa85fce9c7476fe132df189e27dac1
content/10: 371d0e46b4bd2c23f559b8bc112f6955
content/11: 985f435f721b00df4d13fa0a5552684c
content/12: bcadfc362b69078beee0088e5936c98b
content/13: 6af66efd0da20944a87fdb8d9defa358
content/14: b3f310d5ef115bea5a8b75bf25d7ea9a
content/15: 2bcdb5144797b6a7dd2d266331c9b95e
403c3b5c22a4dfef9c7dcfe7e5668348:
meta/title: 6ea728a87df2a68e40938bba653845d2
meta/description: 19eaaa8cac2b54c15a1c3f7d61b52457
@@ -588,7 +577,7 @@ checksums:
content/9: 55d3faa55bd37eb2f4ab74379410a5a9
content/10: b52a2ab3e371e2a6eca0d16c4b628f69
content/11: 371d0e46b4bd2c23f559b8bc112f6955
content/12: 870dadb88971694d23e19c63009e16e0
content/12: f629a1a44ad4cf0d2dd3eb43e8bdf5f9
content/13: bcadfc362b69078beee0088e5936c98b
content/14: 0fd095f0d3f37eac2be2be452b3ba1dc
content/15: b3f310d5ef115bea5a8b75bf25d7ea9a
@@ -1316,13 +1305,13 @@ checksums:
content/11: 371d0e46b4bd2c23f559b8bc112f6955
content/12: 6abb969d0ca1807d6401266ab8f50975
content/13: bcadfc362b69078beee0088e5936c98b
content/14: 8e69b5b320bb75e70df03437e894c776
content/14: 57008e827f0df4dc136962228c1c74a7
content/15: 3a3c262e00210fad2ff657eaf5aac97d
content/16: 10de36438cb6e1cec1535bee5cb73699
content/17: 371d0e46b4bd2c23f559b8bc112f6955
content/18: 0bd5204d7085a5b9dcfc82e1b3ed2714
content/19: bcadfc362b69078beee0088e5936c98b
content/20: d84fb23e5dfc9d41a177acd7dfb28e72
content/20: fbab8b16c84eb1264e43e99da04006bd
content/21: b3f310d5ef115bea5a8b75bf25d7ea9a
content/22: 568b0cef19d3e81ea300a5621ad42ab5
25de8a658947e92bca61370185aeda9d:
@@ -1354,7 +1343,7 @@ checksums:
content/23: 371d0e46b4bd2c23f559b8bc112f6955
content/24: 5a56ae7f7ba1a4385da43f822782b024
content/25: bcadfc362b69078beee0088e5936c98b
content/26: 98c3ad979dfde09dc41d1b6e370b38f5
content/26: 2f75132ca9307c0a90456a238e9a742b
content/27: b3f310d5ef115bea5a8b75bf25d7ea9a
content/28: 04857d373a5405ac83582efac37e1dc1
153fdbf063881f22370488d6b1b177be:
@@ -3513,19 +3502,19 @@ checksums:
content/9: b012197e65487ff167f2cbd78da85eac
content/10: 0cc2f207ef4e2f3ac0acf4cf9b3aba2c
content/11: 371d0e46b4bd2c23f559b8bc112f6955
content/12: 10980279b69aa6c6a1464f263ca25be6
content/12: 989c1aa1d50cf098f58aa7d1589f05dd
content/13: bcadfc362b69078beee0088e5936c98b
content/14: 3d15775cf6e0105686ea02f90008b64b
content/15: 4499d456d0b0166932b1cdcc55c21396
content/16: 58f73a8f3dfd938f5d211a9ce5df6770
content/17: 371d0e46b4bd2c23f559b8bc112f6955
content/18: cbf55c968ed5f0917b7269bf1eaa6afc
content/18: f1ad77da5d66a7bae33ec46c56b95ddf
content/19: bcadfc362b69078beee0088e5936c98b
content/20: e04f86af43c8bdc3e61d3935ba9e85fb
content/21: 9c18dfa51781c1e30cb2e2741b92b091
content/22: 8f86d82c91437004767ce62918f5ede6
content/23: 371d0e46b4bd2c23f559b8bc112f6955
content/24: be02ad818111831d726d6bca32c59fe7
content/24: e73c4bd6da7dbf85f5af136d078d0500
content/25: bcadfc362b69078beee0088e5936c98b
content/26: 9176672ef538cc624ee574133f9949e7
content/27: c9afca4e81299f2f696c4e988878b18d
@@ -46278,157 +46267,157 @@ checksums:
content/14: 371d0e46b4bd2c23f559b8bc112f6955
content/15: 03790a503987168076530036c1493fe5
content/16: bcadfc362b69078beee0088e5936c98b
content/17: 54e111a2489eba7bdff7b7fe4376b8a7
content/17: ea45046a2ff2b4b0d87c76fc6b97c408
content/18: 52c12f35ed540fb4095071b8febadbd6
content/19: 8b22b32d6e35d60ce06e26b8cdde7516
content/20: 371d0e46b4bd2c23f559b8bc112f6955
content/21: dea74d6e978d7a0cb2eb9d17c0d75743
content/22: bcadfc362b69078beee0088e5936c98b
content/23: 1b166ea32dff5f8de92b256fe48200d7
content/23: b99563e729dbe27993fdb4d95a46d8d5
content/24: 2e0c17af85edfb251596a7774ebc148f
content/25: f062fa5ca051bc3f3d208c8932508fc0
content/26: 371d0e46b4bd2c23f559b8bc112f6955
content/27: 43a06769b0b005d1e7d9028fc4e53665
content/28: bcadfc362b69078beee0088e5936c98b
content/29: 573530e346d195727862b03b380f40fc
content/29: 72c013f869e9976ad77f19655a1e8c08
content/30: 720be6a9405a4d31e16d25dc262eddb6
content/31: 0e47014b2ea303913c7129b3e4da290a
content/32: 371d0e46b4bd2c23f559b8bc112f6955
content/33: 490e114043914c0dd0b41a8a9eaa4d9d
content/34: bcadfc362b69078beee0088e5936c98b
content/35: 2aa59cb206803bd55848d1dbc4f02c24
content/35: 6e083e5204c044c175986bc4de65142f
content/36: afab481f543682e3c895d06bffff104b
content/37: 8f7732c5ac7c54dced63abc5531979f5
content/38: 371d0e46b4bd2c23f559b8bc112f6955
content/39: a1b3b3d3a6ae1ab701d5a138a65268e9
content/40: bcadfc362b69078beee0088e5936c98b
content/41: 209979bf1ba673df324a3e121a11c8a3
content/41: 3cfb80c9ee50d78855f92714695fd099
content/42: 963255b39dc080d1e7293b700e84614c
content/43: 3423e945033f3bae0146a6909ceedc8f
content/44: 371d0e46b4bd2c23f559b8bc112f6955
content/45: d41d7acef728fbaaec3398c4ff7b8fbe
content/46: bcadfc362b69078beee0088e5936c98b
content/47: cf3e596e8dd672a8d8d4857f51e9ab50
content/47: 6e083e5204c044c175986bc4de65142f
content/48: 9ab9aad5aa1e726435361084e272a014
content/49: 1d3de7001c97111aeb341e2fb5ec815a
content/50: 371d0e46b4bd2c23f559b8bc112f6955
content/51: 1b1a3fbf365b1595c261fb87488cb2ec
content/52: bcadfc362b69078beee0088e5936c98b
content/53: aa539c46a35e99f7440fa5c0d394c2db
content/53: 3386c2debe2e941da427f5c1904fa767
content/54: 60112dd6b483b56f99f4c16c9884b050
content/55: 547bcbaeb75e52e529f102fd008326af
content/56: 371d0e46b4bd2c23f559b8bc112f6955
content/57: aba9802d4a2ab9b4515aa98efcabb951
content/58: bcadfc362b69078beee0088e5936c98b
content/59: 42baf3d6d8998277e787dff34d0c9205
content/59: 6e083e5204c044c175986bc4de65142f
content/60: 78b20224accffac2854e9a92729474ec
content/61: 060052f51f91b3181ec5161788f7b8ca
content/62: 371d0e46b4bd2c23f559b8bc112f6955
content/63: bd6bc4020abf3ef5f6e3e680d5b28a14
content/64: bcadfc362b69078beee0088e5936c98b
content/65: 1b12c9e7e17dc8a545788c0e59039e85
content/65: cf03036e119b398bf5187e8289f3f94f
content/66: 9f56a114c0953177aed24db130781e6a
content/67: f6cfb33fdbaa58daf786ba40f7c0a7c4
content/68: 371d0e46b4bd2c23f559b8bc112f6955
content/69: 3b3d202acc98df0cf6b79e902aae2d7e
content/70: bcadfc362b69078beee0088e5936c98b
content/71: e57935e8dc8c529b7cd6d1bbd625c571
content/71: 2665d3aed70b1659e3b6e14310ff6ac3
content/72: 57a45f869a822311c41280e85410615f
content/73: 28a8643f2c30a6e2af1209e7c04af013
content/74: 371d0e46b4bd2c23f559b8bc112f6955
content/75: 927aaa7ff75ed5d79f338dab1b65a5d4
content/76: bcadfc362b69078beee0088e5936c98b
content/77: b920a5d942663ab36af04d7588767c22
content/77: 81ad1310cb520e2d27438070e9336192
content/78: 7d4b7f84297208d69bf1e76e5261862a
content/79: f9849f3e5c1253f99871a4a306a1d989
content/80: 371d0e46b4bd2c23f559b8bc112f6955
content/81: c1b61507ca0c8558ae4b63c1e816bb47
content/82: bcadfc362b69078beee0088e5936c98b
content/83: 1a337e24a2607d14708da299915edc97
content/83: cf03036e119b398bf5187e8289f3f94f
content/84: 636045dc118e3ba21f9d39376650122e
content/85: 97b39365b3102e15acac67ca2dbdda19
content/86: 371d0e46b4bd2c23f559b8bc112f6955
content/87: e36155b52053b654d4d37e51bb4942e6
content/88: bcadfc362b69078beee0088e5936c98b
content/89: c9e060877ac3730e4402be772ac46ce8
content/89: 5ca2489579987217db1a2c875b96d9f9
content/90: 1432d33da621fc006a55d406d02b9a37
content/91: fc8958b8145693dba3f21b6927623f6e
content/92: 371d0e46b4bd2c23f559b8bc112f6955
content/93: 0ceb2a6d432abc7ae35c5d702efd6138
content/94: bcadfc362b69078beee0088e5936c98b
content/95: 74ae0903eb838fb659541a8db6e8fa03
content/95: 6e083e5204c044c175986bc4de65142f
content/96: d278aa56d3be3d2573356cc6408ba158
content/97: 99a1c61fceaa1bc28862fac36ce2f4e7
content/98: 371d0e46b4bd2c23f559b8bc112f6955
content/99: 10f26827078049449566a718edaabcde
content/100: bcadfc362b69078beee0088e5936c98b
content/101: a50beaa2074bd143cd62b7fc23554f60
content/101: 1b4d8e47d7af5392c3b2d26ff2ded123
content/102: 9375b46687d820ca8bf3f4cee903569f
content/103: 8adabd834d3e643b995fa5e0bcaaa91e
content/104: 371d0e46b4bd2c23f559b8bc112f6955
content/105: e4be303f29ceeae8431191379edf2299
content/106: bcadfc362b69078beee0088e5936c98b
content/107: cf3e596e8dd672a8d8d4857f51e9ab50
content/107: 6e083e5204c044c175986bc4de65142f
content/108: 75ebf9b0cd4b860936f60f62bba9b01d
content/109: ec54d03fd37c2acf17f0b6ecc27e0e5c
content/110: 371d0e46b4bd2c23f559b8bc112f6955
content/111: c36524c25d5e4d2180b2466c21444be3
content/112: bcadfc362b69078beee0088e5936c98b
content/113: cec912475a9662b72d3266e24ccfeabf
content/113: 3386c2debe2e941da427f5c1904fa767
content/114: 9bf8d3e3fadd33bf0d1a3b2f1c191d76
content/115: 04c526cf6db7ae45bd6b69f4fbe3cec0
content/116: 371d0e46b4bd2c23f559b8bc112f6955
content/117: fd010a6f907ab6d156bcc8942c7d8fab
content/118: bcadfc362b69078beee0088e5936c98b
content/119: c46faeb199b61577a56855249a26ed15
content/119: 28e1ea07f7ed31c219d900a239011abc
content/120: d11520ec23cfe64d97182999af637729
content/121: 90b9a292124bf143507cedaea38afac8
content/122: 371d0e46b4bd2c23f559b8bc112f6955
content/123: b674e049685fa8b815185f8b0999148b
content/124: bcadfc362b69078beee0088e5936c98b
content/125: ba0b2310309adab963b529e3beb0c1b2
content/125: 3701c897476e40630b0c21084e502926
content/126: 401e619e5f586d7a98cb40919eb4e8c0
content/127: cd2944a378bdf25f5195d0213a92e5c5
content/128: 371d0e46b4bd2c23f559b8bc112f6955
content/129: 69176bce8f2277c0aab5ae5cb26036b9
content/130: bcadfc362b69078beee0088e5936c98b
content/131: 5ccf80dc8b3aa024fc35aefcfd7cb271
content/131: 28e1ea07f7ed31c219d900a239011abc
content/132: 741bf0308b6d0a358c53114cab40393a
content/133: 248f9e5380cbdd539a3e18722d72bcd1
content/134: 371d0e46b4bd2c23f559b8bc112f6955
content/135: c9ad4274c5941afc9ec3949a42c511b2
content/136: bcadfc362b69078beee0088e5936c98b
content/137: cae631cdfc9299fd10c6ed5cf956df77
content/137: 3238a9fa1c649af888db930ff0181397
content/138: 88092a1d9ebf0b96cc2263677ab7875e
content/139: 74e37cd1cf3f31a08e874966f08618e9
content/140: 371d0e46b4bd2c23f559b8bc112f6955
content/141: 3c2f509845fd8ebd43e2469f231b5ec6
content/142: bcadfc362b69078beee0088e5936c98b
content/143: 74ae0903eb838fb659541a8db6e8fa03
content/143: 6e083e5204c044c175986bc4de65142f
content/144: 3fa05da9a789cbe306e6e37532ea5b20
content/145: beea05186900898272601f78c7b4ffbc
content/146: 371d0e46b4bd2c23f559b8bc112f6955
content/147: 1c20c4e275db53d81e1a26d5be248df1
content/148: bcadfc362b69078beee0088e5936c98b
content/149: caa918a2c7e8f3f319dc5dde1f40e064
content/149: 8ba2ff0291d55fa722dc1ce0f00bf90c
content/150: d697e1dc249684ff4628ab7d847ebeec
content/151: 6530f207edacc508f1fffed44c6e3eea
content/152: 371d0e46b4bd2c23f559b8bc112f6955
content/153: 8abbb8779b9abb22a555a7529dde3c21
content/154: bcadfc362b69078beee0088e5936c98b
content/155: c1dab1cc1a29f4fc13632d7616e8e1f6
content/155: 3386c2debe2e941da427f5c1904fa767
content/156: a6ac06461462f71313b9a69d33c4713f
content/157: b83a3e027493b33566e157eae7b067bb
content/158: 371d0e46b4bd2c23f559b8bc112f6955
content/159: 654d2d9f89b93a4f46b846930b957f81
content/160: bcadfc362b69078beee0088e5936c98b
content/161: d84fb23e5dfc9d41a177acd7dfb28e72
content/161: bb4850d44287327333b0400dd7aa6bd9
content/162: 5e43194e8a59fd28d7c47b73f4c21c0a
content/163: 862627f89b796dd70b796b7a56989f02
content/164: 371d0e46b4bd2c23f559b8bc112f6955
content/165: 0590e327298a51c83d743ac718562a6a
content/166: bcadfc362b69078beee0088e5936c98b
content/167: 9b8ed8eaf45789402878e0c9b179f4ad
content/167: 0b9e2bbe5ee526600c866f3c8305dc87
content/168: b3f310d5ef115bea5a8b75bf25d7ea9a
content/169: 47663eb519251c4ba49564d0e7c36e94
41b4c06bb56053083938bcf6d958856d:
@@ -47761,7 +47750,7 @@ checksums:
content/68: ada572e729e01d615e2bed27d33796cb
content/69: b45b536bf6f2547c9058fca134946283
content/70: 371d0e46b4bd2c23f559b8bc112f6955
content/71: a7ca3f5cb26e88ffcbb6972d13b8e0ab
content/71: 8b0984caf1aa8d793736f1c56b8c5e9a
content/72: bcadfc362b69078beee0088e5936c98b
content/73: 820449909a823294cef60f274eb7dd70
content/74: d676ef2d8dca4c76ca28bac77c6e83cb

View File

@@ -3,7 +3,6 @@ import { userStats } from '@sim/db/schema'
import { eq, sql } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { deductFromCredits } from '@/lib/billing/credits/balance'
import { checkAndBillOverageThreshold } from '@/lib/billing/threshold-billing'
import { checkInternalApiKey } from '@/lib/copilot/utils'
import { isBillingEnabled } from '@/lib/core/config/environment'
@@ -92,17 +91,11 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ error: 'User stats record not found' }, { status: 500 })
}
const { creditsUsed, overflow } = await deductFromCredits(userId, cost)
if (creditsUsed > 0) {
logger.info(`[${requestId}] Deducted cost from credits`, { userId, creditsUsed, overflow })
}
const costToStore = overflow
const updateFields = {
totalCost: sql`total_cost + ${costToStore}`,
currentPeriodCost: sql`current_period_cost + ${costToStore}`,
totalCopilotCost: sql`total_copilot_cost + ${costToStore}`,
currentPeriodCopilotCost: sql`current_period_copilot_cost + ${costToStore}`,
totalCost: sql`total_cost + ${cost}`,
currentPeriodCost: sql`current_period_cost + ${cost}`,
totalCopilotCost: sql`total_copilot_cost + ${cost}`,
currentPeriodCopilotCost: sql`current_period_copilot_cost + ${cost}`,
totalCopilotCalls: sql`total_copilot_calls + 1`,
lastActive: new Date(),
}

View File

@@ -4,7 +4,8 @@ import { eq } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { z } from 'zod'
import { renderOTPEmail } from '@/components/emails/render-email'
import { getRedisClient, markMessageAsProcessed, releaseLock } from '@/lib/core/config/redis'
import { getRedisClient } from '@/lib/core/config/redis'
import { getStorageMethod } from '@/lib/core/storage'
import { generateRequestId } from '@/lib/core/utils/request'
import { createLogger } from '@/lib/logs/console/logger'
import { sendEmail } from '@/lib/messaging/email/mailer'
@@ -17,87 +18,80 @@ function generateOTP() {
return Math.floor(100000 + Math.random() * 900000).toString()
}
// OTP storage utility functions using Redis
// We use 15 minutes (900 seconds) expiry for OTPs
const OTP_EXPIRY = 15 * 60
const OTP_EXPIRY = 15 * 60 // 15 minutes
const OTP_EXPIRY_MS = OTP_EXPIRY * 1000
/**
* In-memory OTP storage for single-instance deployments without Redis.
* Only used when REDIS_URL is not configured (determined once at startup).
*
* Warning: This does NOT work in multi-instance/serverless deployments.
*/
const inMemoryOTPStore = new Map<string, { otp: string; expiresAt: number }>()
function cleanupExpiredOTPs() {
const now = Date.now()
for (const [key, value] of inMemoryOTPStore.entries()) {
if (value.expiresAt < now) {
inMemoryOTPStore.delete(key)
}
}
}
// Store OTP in Redis
async function storeOTP(email: string, chatId: string, otp: string): Promise<void> {
const key = `otp:${email}:${chatId}`
const redis = getRedisClient()
const storageMethod = getStorageMethod()
if (redis) {
// Use Redis if available
if (storageMethod === 'redis') {
const redis = getRedisClient()
if (!redis) {
throw new Error('Redis configured but client unavailable')
}
await redis.set(key, otp, 'EX', OTP_EXPIRY)
} else {
// Use the existing function as fallback to mark that an OTP exists
await markMessageAsProcessed(key, OTP_EXPIRY)
// For the fallback case, we need to handle storing the OTP value separately
// since markMessageAsProcessed only stores "1"
const valueKey = `${key}:value`
try {
// Access the in-memory cache directly - hacky but works for fallback
const inMemoryCache = (global as any).inMemoryCache
if (inMemoryCache) {
const fullKey = `processed:${valueKey}`
const expiry = OTP_EXPIRY ? Date.now() + OTP_EXPIRY * 1000 : null
inMemoryCache.set(fullKey, { value: otp, expiry })
}
} catch (error) {
logger.error('Error storing OTP in fallback cache:', error)
}
cleanupExpiredOTPs()
inMemoryOTPStore.set(key, {
otp,
expiresAt: Date.now() + OTP_EXPIRY_MS,
})
}
}
// Get OTP from Redis
async function getOTP(email: string, chatId: string): Promise<string | null> {
const key = `otp:${email}:${chatId}`
const redis = getRedisClient()
const storageMethod = getStorageMethod()
if (redis) {
// Use Redis if available
return await redis.get(key)
}
// Use the existing function as fallback - check if it exists
const exists = await new Promise((resolve) => {
try {
// Check the in-memory cache directly - hacky but works for fallback
const inMemoryCache = (global as any).inMemoryCache
const fullKey = `processed:${key}`
const cacheEntry = inMemoryCache?.get(fullKey)
resolve(!!cacheEntry)
} catch {
resolve(false)
if (storageMethod === 'redis') {
const redis = getRedisClient()
if (!redis) {
throw new Error('Redis configured but client unavailable')
}
})
return redis.get(key)
}
if (!exists) return null
const entry = inMemoryOTPStore.get(key)
if (!entry) return null
// Try to get the value key
const valueKey = `${key}:value`
try {
const inMemoryCache = (global as any).inMemoryCache
const fullKey = `processed:${valueKey}`
const cacheEntry = inMemoryCache?.get(fullKey)
return cacheEntry?.value || null
} catch {
if (entry.expiresAt < Date.now()) {
inMemoryOTPStore.delete(key)
return null
}
return entry.otp
}
// Delete OTP from Redis
async function deleteOTP(email: string, chatId: string): Promise<void> {
const key = `otp:${email}:${chatId}`
const redis = getRedisClient()
const storageMethod = getStorageMethod()
if (redis) {
// Use Redis if available
if (storageMethod === 'redis') {
const redis = getRedisClient()
if (!redis) {
throw new Error('Redis configured but client unavailable')
}
await redis.del(key)
} else {
// Use the existing function as fallback
await releaseLock(`processed:${key}`)
await releaseLock(`processed:${key}:value`)
inMemoryOTPStore.delete(key)
}
}
@@ -110,7 +104,6 @@ const otpVerifySchema = z.object({
otp: z.string().length(6, 'OTP must be 6 digits'),
})
// Send OTP endpoint
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ identifier: string }> }
@@ -121,101 +114,82 @@ export async function POST(
try {
logger.debug(`[${requestId}] Processing OTP request for identifier: ${identifier}`)
// Parse request body
let body
try {
body = await request.json()
const { email } = otpRequestSchema.parse(body)
const body = await request.json()
const { email } = otpRequestSchema.parse(body)
// Find the chat deployment
const deploymentResult = await db
.select({
id: chat.id,
authType: chat.authType,
allowedEmails: chat.allowedEmails,
title: chat.title,
})
.from(chat)
.where(eq(chat.identifier, identifier))
.limit(1)
const deploymentResult = await db
.select({
id: chat.id,
authType: chat.authType,
allowedEmails: chat.allowedEmails,
title: chat.title,
})
.from(chat)
.where(eq(chat.identifier, identifier))
.limit(1)
if (deploymentResult.length === 0) {
logger.warn(`[${requestId}] Chat not found for identifier: ${identifier}`)
return addCorsHeaders(createErrorResponse('Chat not found', 404), request)
}
if (deploymentResult.length === 0) {
logger.warn(`[${requestId}] Chat not found for identifier: ${identifier}`)
return addCorsHeaders(createErrorResponse('Chat not found', 404), request)
}
const deployment = deploymentResult[0]
const deployment = deploymentResult[0]
// Verify this is an email-protected chat
if (deployment.authType !== 'email') {
return addCorsHeaders(
createErrorResponse('This chat does not use email authentication', 400),
request
)
}
const allowedEmails: string[] = Array.isArray(deployment.allowedEmails)
? deployment.allowedEmails
: []
const isEmailAllowed =
allowedEmails.includes(email) ||
allowedEmails.some((allowed: string) => {
if (allowed.startsWith('@')) {
const domain = email.split('@')[1]
return domain && allowed === `@${domain}`
}
return false
})
if (!isEmailAllowed) {
return addCorsHeaders(
createErrorResponse('Email not authorized for this chat', 403),
request
)
}
const otp = generateOTP()
await storeOTP(email, deployment.id, otp)
const emailHtml = await renderOTPEmail(
otp,
email,
'email-verification',
deployment.title || 'Chat'
if (deployment.authType !== 'email') {
return addCorsHeaders(
createErrorResponse('This chat does not use email authentication', 400),
request
)
}
const emailResult = await sendEmail({
to: email,
subject: `Verification code for ${deployment.title || 'Chat'}`,
html: emailHtml,
const allowedEmails: string[] = Array.isArray(deployment.allowedEmails)
? deployment.allowedEmails
: []
const isEmailAllowed =
allowedEmails.includes(email) ||
allowedEmails.some((allowed: string) => {
if (allowed.startsWith('@')) {
const domain = email.split('@')[1]
return domain && allowed === `@${domain}`
}
return false
})
if (!emailResult.success) {
logger.error(`[${requestId}] Failed to send OTP email:`, emailResult.message)
return addCorsHeaders(
createErrorResponse('Failed to send verification email', 500),
request
)
}
// Add a small delay to ensure Redis has fully processed the operation
// This helps with eventual consistency in distributed systems
await new Promise((resolve) => setTimeout(resolve, 500))
logger.info(`[${requestId}] OTP sent to ${email} for chat ${deployment.id}`)
return addCorsHeaders(createSuccessResponse({ message: 'Verification code sent' }), request)
} catch (error: any) {
if (error instanceof z.ZodError) {
return addCorsHeaders(
createErrorResponse(error.errors[0]?.message || 'Invalid request', 400),
request
)
}
throw error
if (!isEmailAllowed) {
return addCorsHeaders(createErrorResponse('Email not authorized for this chat', 403), request)
}
const otp = generateOTP()
await storeOTP(email, deployment.id, otp)
const emailHtml = await renderOTPEmail(
otp,
email,
'email-verification',
deployment.title || 'Chat'
)
const emailResult = await sendEmail({
to: email,
subject: `Verification code for ${deployment.title || 'Chat'}`,
html: emailHtml,
})
if (!emailResult.success) {
logger.error(`[${requestId}] Failed to send OTP email:`, emailResult.message)
return addCorsHeaders(createErrorResponse('Failed to send verification email', 500), request)
}
logger.info(`[${requestId}] OTP sent to ${email} for chat ${deployment.id}`)
return addCorsHeaders(createSuccessResponse({ message: 'Verification code sent' }), request)
} catch (error: any) {
if (error instanceof z.ZodError) {
return addCorsHeaders(
createErrorResponse(error.errors[0]?.message || 'Invalid request', 400),
request
)
}
logger.error(`[${requestId}] Error processing OTP request:`, error)
return addCorsHeaders(
createErrorResponse(error.message || 'Failed to process request', 500),
@@ -224,7 +198,6 @@ export async function POST(
}
}
// Verify OTP endpoint
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ identifier: string }> }
@@ -235,63 +208,50 @@ export async function PUT(
try {
logger.debug(`[${requestId}] Verifying OTP for identifier: ${identifier}`)
// Parse request body
let body
try {
body = await request.json()
const { email, otp } = otpVerifySchema.parse(body)
const body = await request.json()
const { email, otp } = otpVerifySchema.parse(body)
// Find the chat deployment
const deploymentResult = await db
.select({
id: chat.id,
authType: chat.authType,
})
.from(chat)
.where(eq(chat.identifier, identifier))
.limit(1)
const deploymentResult = await db
.select({
id: chat.id,
authType: chat.authType,
})
.from(chat)
.where(eq(chat.identifier, identifier))
.limit(1)
if (deploymentResult.length === 0) {
logger.warn(`[${requestId}] Chat not found for identifier: ${identifier}`)
return addCorsHeaders(createErrorResponse('Chat not found', 404), request)
}
const deployment = deploymentResult[0]
// Check if OTP exists and is valid
const storedOTP = await getOTP(email, deployment.id)
if (!storedOTP) {
return addCorsHeaders(
createErrorResponse('No verification code found, request a new one', 400),
request
)
}
// Check if OTP matches
if (storedOTP !== otp) {
return addCorsHeaders(createErrorResponse('Invalid verification code', 400), request)
}
// OTP is valid, clean up
await deleteOTP(email, deployment.id)
// Create success response with auth cookie
const response = addCorsHeaders(createSuccessResponse({ authenticated: true }), request)
// Set authentication cookie
setChatAuthCookie(response, deployment.id, deployment.authType)
return response
} catch (error: any) {
if (error instanceof z.ZodError) {
return addCorsHeaders(
createErrorResponse(error.errors[0]?.message || 'Invalid request', 400),
request
)
}
throw error
if (deploymentResult.length === 0) {
logger.warn(`[${requestId}] Chat not found for identifier: ${identifier}`)
return addCorsHeaders(createErrorResponse('Chat not found', 404), request)
}
const deployment = deploymentResult[0]
const storedOTP = await getOTP(email, deployment.id)
if (!storedOTP) {
return addCorsHeaders(
createErrorResponse('No verification code found, request a new one', 400),
request
)
}
if (storedOTP !== otp) {
return addCorsHeaders(createErrorResponse('Invalid verification code', 400), request)
}
await deleteOTP(email, deployment.id)
const response = addCorsHeaders(createSuccessResponse({ authenticated: true }), request)
setChatAuthCookie(response, deployment.id, deployment.authType)
return response
} catch (error: any) {
if (error instanceof z.ZodError) {
return addCorsHeaders(
createErrorResponse(error.errors[0]?.message || 'Invalid request', 400),
request
)
}
logger.error(`[${requestId}] Error verifying OTP:`, error)
return addCorsHeaders(
createErrorResponse(error.message || 'Failed to process request', 500),

View File

@@ -5,7 +5,7 @@ import path from 'path'
import binaryExtensionsList from 'binary-extensions'
import { type NextRequest, NextResponse } from 'next/server'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { validateExternalUrl } from '@/lib/core/security/input-validation'
import { createPinnedUrl, validateUrlWithDNS } from '@/lib/core/security/input-validation'
import { isSupportedFileType, parseFile } from '@/lib/file-parsers'
import { createLogger } from '@/lib/logs/console/logger'
import { isUsingCloudStorage, type StorageContext, StorageService } from '@/lib/uploads'
@@ -270,7 +270,7 @@ async function handleExternalUrl(
logger.info('Fetching external URL:', url)
logger.info('WorkspaceId for URL save:', workspaceId)
const urlValidation = validateExternalUrl(url, 'fileUrl')
const urlValidation = await validateUrlWithDNS(url, 'fileUrl')
if (!urlValidation.isValid) {
logger.warn(`Blocked external URL request: ${urlValidation.error}`)
return {
@@ -346,8 +346,12 @@ async function handleExternalUrl(
}
}
const response = await fetch(url, {
const pinnedUrl = createPinnedUrl(url, urlValidation.resolvedIP!)
const response = await fetch(pinnedUrl, {
signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS),
headers: {
Host: urlValidation.originalHostname!,
},
})
if (!response.ok) {
throw new Error(`Failed to fetch URL: ${response.status} ${response.statusText}`)

View File

@@ -1,5 +1,10 @@
import { db } from '@sim/db'
import { permissions, workflow, workflowExecutionLogs } from '@sim/db/schema'
import {
permissions,
workflow,
workflowDeploymentVersion,
workflowExecutionLogs,
} from '@sim/db/schema'
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
@@ -29,6 +34,7 @@ export async function GET(_request: NextRequest, { params }: { params: Promise<{
workflowId: workflowExecutionLogs.workflowId,
executionId: workflowExecutionLogs.executionId,
stateSnapshotId: workflowExecutionLogs.stateSnapshotId,
deploymentVersionId: workflowExecutionLogs.deploymentVersionId,
level: workflowExecutionLogs.level,
trigger: workflowExecutionLogs.trigger,
startedAt: workflowExecutionLogs.startedAt,
@@ -46,9 +52,15 @@ export async function GET(_request: NextRequest, { params }: { params: Promise<{
workflowWorkspaceId: workflow.workspaceId,
workflowCreatedAt: workflow.createdAt,
workflowUpdatedAt: workflow.updatedAt,
deploymentVersion: workflowDeploymentVersion.version,
deploymentVersionName: workflowDeploymentVersion.name,
})
.from(workflowExecutionLogs)
.innerJoin(workflow, eq(workflowExecutionLogs.workflowId, workflow.id))
.leftJoin(
workflowDeploymentVersion,
eq(workflowDeploymentVersion.id, workflowExecutionLogs.deploymentVersionId)
)
.innerJoin(
permissions,
and(
@@ -81,6 +93,9 @@ export async function GET(_request: NextRequest, { params }: { params: Promise<{
id: log.id,
workflowId: log.workflowId,
executionId: log.executionId,
deploymentVersionId: log.deploymentVersionId,
deploymentVersion: log.deploymentVersion ?? null,
deploymentVersionName: log.deploymentVersionName ?? null,
level: log.level,
duration: log.totalDurationMs ? `${log.totalDurationMs}ms` : null,
trigger: log.trigger,

View File

@@ -1,5 +1,11 @@
import { db } from '@sim/db'
import { pausedExecutions, permissions, workflow, workflowExecutionLogs } from '@sim/db/schema'
import {
pausedExecutions,
permissions,
workflow,
workflowDeploymentVersion,
workflowExecutionLogs,
} from '@sim/db/schema'
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'
@@ -51,6 +57,7 @@ export async function GET(request: NextRequest) {
workflowId: workflowExecutionLogs.workflowId,
executionId: workflowExecutionLogs.executionId,
stateSnapshotId: workflowExecutionLogs.stateSnapshotId,
deploymentVersionId: workflowExecutionLogs.deploymentVersionId,
level: workflowExecutionLogs.level,
trigger: workflowExecutionLogs.trigger,
startedAt: workflowExecutionLogs.startedAt,
@@ -71,6 +78,8 @@ export async function GET(request: NextRequest) {
pausedStatus: pausedExecutions.status,
pausedTotalPauseCount: pausedExecutions.totalPauseCount,
pausedResumedCount: pausedExecutions.resumedCount,
deploymentVersion: workflowDeploymentVersion.version,
deploymentVersionName: workflowDeploymentVersion.name,
}
: {
// Basic mode - exclude large fields for better performance
@@ -78,6 +87,7 @@ export async function GET(request: NextRequest) {
workflowId: workflowExecutionLogs.workflowId,
executionId: workflowExecutionLogs.executionId,
stateSnapshotId: workflowExecutionLogs.stateSnapshotId,
deploymentVersionId: workflowExecutionLogs.deploymentVersionId,
level: workflowExecutionLogs.level,
trigger: workflowExecutionLogs.trigger,
startedAt: workflowExecutionLogs.startedAt,
@@ -98,6 +108,8 @@ export async function GET(request: NextRequest) {
pausedStatus: pausedExecutions.status,
pausedTotalPauseCount: pausedExecutions.totalPauseCount,
pausedResumedCount: pausedExecutions.resumedCount,
deploymentVersion: workflowDeploymentVersion.version,
deploymentVersionName: sql<null>`NULL`, // Only needed in full mode for details panel
}
const baseQuery = db
@@ -107,6 +119,10 @@ export async function GET(request: NextRequest) {
pausedExecutions,
eq(pausedExecutions.executionId, workflowExecutionLogs.executionId)
)
.leftJoin(
workflowDeploymentVersion,
eq(workflowDeploymentVersion.id, workflowExecutionLogs.deploymentVersionId)
)
.innerJoin(
workflow,
and(
@@ -397,6 +413,9 @@ export async function GET(request: NextRequest) {
id: log.id,
workflowId: log.workflowId,
executionId: log.executionId,
deploymentVersionId: log.deploymentVersionId,
deploymentVersion: log.deploymentVersion ?? null,
deploymentVersionName: log.deploymentVersionName ?? null,
level: log.level,
duration: log.totalDurationMs ? `${log.totalDurationMs}ms` : null,
trigger: log.trigger,

View File

@@ -16,15 +16,17 @@ export async function GET(request: NextRequest) {
const requestId = nanoid()
logger.info(`Inactivity alert polling triggered (${requestId})`)
let lockAcquired = false
try {
const authError = verifyCronAuth(request, 'Inactivity alert polling')
if (authError) {
return authError
}
const locked = await acquireLock(LOCK_KEY, requestId, LOCK_TTL_SECONDS)
lockAcquired = await acquireLock(LOCK_KEY, requestId, LOCK_TTL_SECONDS)
if (!locked) {
if (!lockAcquired) {
return NextResponse.json(
{
success: true,
@@ -57,6 +59,8 @@ export async function GET(request: NextRequest) {
{ status: 500 }
)
} finally {
await releaseLock(LOCK_KEY).catch(() => {})
if (lockAcquired) {
await releaseLock(LOCK_KEY, requestId).catch(() => {})
}
}
}

View File

@@ -4,7 +4,7 @@ import { z } from 'zod'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateInternalToken } from '@/lib/auth/internal'
import { isDev } from '@/lib/core/config/environment'
import { validateProxyUrl } from '@/lib/core/security/input-validation'
import { createPinnedUrl, validateUrlWithDNS } from '@/lib/core/security/input-validation'
import { generateRequestId } from '@/lib/core/utils/request'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { createLogger } from '@/lib/logs/console/logger'
@@ -173,7 +173,7 @@ export async function GET(request: Request) {
return createErrorResponse("Missing 'url' parameter", 400)
}
const urlValidation = validateProxyUrl(targetUrl)
const urlValidation = await validateUrlWithDNS(targetUrl)
if (!urlValidation.isValid) {
logger.warn(`[${requestId}] Blocked proxy request`, {
url: targetUrl.substring(0, 100),
@@ -211,11 +211,13 @@ export async function GET(request: Request) {
logger.info(`[${requestId}] Proxying ${method} request to: ${targetUrl}`)
try {
const response = await fetch(targetUrl, {
const pinnedUrl = createPinnedUrl(targetUrl, urlValidation.resolvedIP!)
const response = await fetch(pinnedUrl, {
method: method,
headers: {
...getProxyHeaders(),
...customHeaders,
Host: urlValidation.originalHostname!,
},
body: body || undefined,
})

View File

@@ -227,16 +227,16 @@ export async function POST(request: NextRequest) {
logger.info(`[${requestId}] Transcription completed successfully`)
return NextResponse.json({
transcript,
segments,
language: detectedLanguage,
duration,
confidence,
sentiment: sentimentResults,
entities,
summary,
})
const response: Record<string, any> = { transcript }
if (segments !== undefined) response.segments = segments
if (detectedLanguage !== undefined) response.language = detectedLanguage
if (duration !== undefined) response.duration = duration
if (confidence !== undefined) response.confidence = confidence
if (sentimentResults !== undefined) response.sentiment = sentimentResults
if (entities !== undefined) response.entities = entities
if (summary !== undefined) response.summary = summary
return NextResponse.json(response)
} catch (error) {
logger.error(`[${requestId}] STT proxy error:`, error)
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
@@ -277,11 +277,11 @@ async function transcribeWithWhisper(
formData.append('temperature', temperature.toString())
}
formData.append('response_format', 'verbose_json')
if (timestamps === 'word') {
formData.append('response_format', 'verbose_json')
formData.append('timestamp_granularities', 'word')
} else if (timestamps === 'sentence') {
formData.append('response_format', 'verbose_json')
formData.append('timestamp_granularities', 'segment')
}
@@ -302,17 +302,14 @@ async function transcribeWithWhisper(
const data = await response.json()
if (timestamps === 'none') {
return {
transcript: data.text,
language: data.language,
}
let segments: TranscriptSegment[] | undefined
if (timestamps !== 'none') {
segments = (data.segments || data.words || []).map((seg: any) => ({
text: seg.text,
start: seg.start,
end: seg.end,
}))
}
const segments: TranscriptSegment[] = (data.segments || data.words || []).map((seg: any) => ({
text: seg.text,
start: seg.start,
end: seg.end,
}))
return {
transcript: data.text,

View File

@@ -19,7 +19,6 @@ export async function createMySQLConnection(config: MySQLConnectionConfig) {
}
if (config.ssl === 'disabled') {
// Don't set ssl property at all to disable SSL
} else if (config.ssl === 'required') {
connectionConfig.ssl = { rejectUnauthorized: true }
} else if (config.ssl === 'preferred') {
@@ -54,42 +53,6 @@ export async function executeQuery(
export function validateQuery(query: string): { isValid: boolean; error?: string } {
const trimmedQuery = query.trim().toLowerCase()
const dangerousPatterns = [
/drop\s+database/i,
/drop\s+schema/i,
/drop\s+user/i,
/create\s+user/i,
/grant\s+/i,
/revoke\s+/i,
/alter\s+user/i,
/set\s+global/i,
/set\s+session/i,
/load\s+data/i,
/into\s+outfile/i,
/into\s+dumpfile/i,
/load_file\s*\(/i,
/system\s+/i,
/exec\s+/i,
/execute\s+immediate/i,
/xp_cmdshell/i,
/sp_configure/i,
/information_schema\.tables/i,
/mysql\.user/i,
/mysql\.db/i,
/mysql\.host/i,
/performance_schema/i,
/sys\./i,
]
for (const pattern of dangerousPatterns) {
if (pattern.test(query)) {
return {
isValid: false,
error: `Query contains potentially dangerous operation: ${pattern.source}`,
}
}
}
const allowedStatements = /^(select|insert|update|delete|with|show|describe|explain)\s+/i
if (!allowedStatements.test(trimmedQuery)) {
return {

View File

@@ -30,10 +30,7 @@ export async function createNeo4jDriver(config: Neo4jConnectionConfig) {
return driver
}
export function validateCypherQuery(
query: string,
allowDangerousOps = false
): { isValid: boolean; error?: string } {
export function validateCypherQuery(query: string): { isValid: boolean; error?: string } {
if (!query || typeof query !== 'string') {
return {
isValid: false,
@@ -41,33 +38,6 @@ export function validateCypherQuery(
}
}
if (!allowDangerousOps) {
const dangerousPatterns = [
/DROP\s+DATABASE/i,
/DROP\s+CONSTRAINT/i,
/DROP\s+INDEX/i,
/CREATE\s+DATABASE/i,
/CREATE\s+CONSTRAINT/i,
/CREATE\s+INDEX/i,
/CALL\s+dbms\./i,
/CALL\s+db\./i,
/LOAD\s+CSV/i,
/apoc\.cypher\.run/i,
/apoc\.load/i,
/apoc\.periodic/i,
]
for (const pattern of dangerousPatterns) {
if (pattern.test(query)) {
return {
isValid: false,
error:
'Query contains potentially dangerous operations (schema changes, system procedures, or external data loading)',
}
}
}
}
const trimmedQuery = query.trim()
if (trimmedQuery.length === 0) {
return {

View File

@@ -33,55 +33,16 @@ export async function executeQuery(
params: unknown[] = []
): Promise<{ rows: unknown[]; rowCount: number }> {
const result = await sql.unsafe(query, params)
const rowCount = result.count ?? result.length ?? 0
return {
rows: Array.isArray(result) ? result : [result],
rowCount: Array.isArray(result) ? result.length : result ? 1 : 0,
rowCount,
}
}
export function validateQuery(query: string): { isValid: boolean; error?: string } {
const trimmedQuery = query.trim().toLowerCase()
// Block dangerous SQL operations
const dangerousPatterns = [
/drop\s+database/i,
/drop\s+schema/i,
/drop\s+user/i,
/create\s+user/i,
/create\s+role/i,
/grant\s+/i,
/revoke\s+/i,
/alter\s+user/i,
/alter\s+role/i,
/set\s+role/i,
/reset\s+role/i,
/copy\s+.*from/i,
/copy\s+.*to/i,
/lo_import/i,
/lo_export/i,
/pg_read_file/i,
/pg_write_file/i,
/pg_ls_dir/i,
/information_schema\.tables/i,
/pg_catalog/i,
/pg_user/i,
/pg_shadow/i,
/pg_roles/i,
/pg_authid/i,
/pg_stat_activity/i,
/dblink/i,
/\\\\copy/i,
]
for (const pattern of dangerousPatterns) {
if (pattern.test(query)) {
return {
isValid: false,
error: `Query contains potentially dangerous operation: ${pattern.source}`,
}
}
}
const allowedStatements = /^(select|insert|update|delete|with|explain|analyze|show)\s+/i
if (!allowedStatements.test(trimmedQuery)) {
return {
@@ -147,9 +108,10 @@ export async function executeInsert(
const query = `INSERT INTO ${sanitizedTable} (${sanitizedColumns.join(', ')}) VALUES (${placeholders.join(', ')}) RETURNING *`
const result = await sql.unsafe(query, values)
const rowCount = result.count ?? result.length ?? 0
return {
rows: Array.isArray(result) ? result : [result],
rowCount: Array.isArray(result) ? result.length : result ? 1 : 0,
rowCount,
}
}
@@ -170,9 +132,10 @@ export async function executeUpdate(
const query = `UPDATE ${sanitizedTable} SET ${setClause} WHERE ${where} RETURNING *`
const result = await sql.unsafe(query, values)
const rowCount = result.count ?? result.length ?? 0
return {
rows: Array.isArray(result) ? result : [result],
rowCount: Array.isArray(result) ? result.length : result ? 1 : 0,
rowCount,
}
}
@@ -187,8 +150,9 @@ export async function executeDelete(
const query = `DELETE FROM ${sanitizedTable} WHERE ${where} RETURNING *`
const result = await sql.unsafe(query, [])
const rowCount = result.count ?? result.length ?? 0
return {
rows: Array.isArray(result) ? result : [result],
rowCount: Array.isArray(result) ? result.length : result ? 1 : 0,
rowCount,
}
}

View File

@@ -25,7 +25,6 @@ export async function POST(request: NextRequest) {
logger.info(`[${requestId}] Executing RDS query on ${params.database}`)
// Validate the query
const validation = validateQuery(params.query)
if (!validation.isValid) {
logger.warn(`[${requestId}] Query validation failed: ${validation.error}`)

View File

@@ -82,29 +82,6 @@ function parseFieldValue(field: Field): unknown {
export function validateQuery(query: string): { isValid: boolean; error?: string } {
const trimmedQuery = query.trim().toLowerCase()
const dangerousPatterns = [
/drop\s+database/i,
/drop\s+schema/i,
/drop\s+user/i,
/create\s+user/i,
/create\s+role/i,
/grant\s+/i,
/revoke\s+/i,
/alter\s+user/i,
/alter\s+role/i,
/set\s+role/i,
/reset\s+role/i,
]
for (const pattern of dangerousPatterns) {
if (pattern.test(query)) {
return {
isValid: false,
error: `Query contains potentially dangerous operation: ${pattern.source}`,
}
}
}
const allowedStatements = /^(select|insert|update|delete|with|explain|show)\s+/i
if (!allowedStatements.test(trimmedQuery)) {
return {

View File

@@ -7,7 +7,6 @@ import { ensureZodObject, normalizeUrl } from '@/app/api/tools/stagehand/utils'
const logger = createLogger('StagehandAgentAPI')
// Environment variables for Browserbase
const BROWSERBASE_API_KEY = env.BROWSERBASE_API_KEY
const BROWSERBASE_PROJECT_ID = env.BROWSERBASE_PROJECT_ID
@@ -44,9 +43,7 @@ function extractActionDirectives(task: string): {
let match
let processedTask = task
// Find all action directives in the task
while ((match = actionRegex.exec(task)) !== null) {
const _fullMatch = match[0]
const actionText = match[1].trim()
const index = match.index
@@ -56,8 +53,6 @@ function extractActionDirectives(task: string): {
})
}
// Replace action directives with placeholders for the agent
// We'll number them to make it clear to the agent what's happening
if (actionDirectives.length > 0) {
let offset = 0
for (let i = 0; i < actionDirectives.length; i++) {
@@ -65,22 +60,18 @@ function extractActionDirectives(task: string): {
const originalIndex = directive.index
const placeholder = `[SECURE ACTION ${i + 1}]`
// Calculate position considering previous replacements
const adjustedIndex = originalIndex - offset
// Get text to replace
const fullMatch = task.substring(
originalIndex,
originalIndex + task.substring(originalIndex).indexOf(']]') + 2
)
// Replace in processed task
processedTask =
processedTask.substring(0, adjustedIndex) +
placeholder +
processedTask.substring(adjustedIndex + fullMatch.length)
// Update offset for next replacement
offset += fullMatch.length - placeholder.length
}
}
@@ -88,7 +79,6 @@ function extractActionDirectives(task: string): {
return { processedTask, actionDirectives }
}
// Function to process secure actions in a given message
async function processSecureActions(
message: string,
stagehand: Stagehand,
@@ -98,21 +88,17 @@ async function processSecureActions(
modifiedMessage: string
executedActions: Array<{ action: string; result: { success: boolean; message: string } }>
}> {
// Track executed actions and modified message
const executedActions: Array<{ action: string; result: { success: boolean; message: string } }> =
[]
let modifiedMessage = message
// Look for secure action execute directives
const secureActionMatches = [...message.matchAll(/EXECUTE SECURE ACTION (\d+)/gi)]
// Process each secure action request
for (const match of secureActionMatches) {
const fullMatch = match[0]
const actionIndex = Number.parseInt(match[1], 10) - 1 // Convert to 0-based index
const actionIndex = Number.parseInt(match[1], 10) - 1
if (actionDirectives[actionIndex]) {
// Found a secure action directive to execute
const actionDirective = actionDirectives[actionIndex]
let resultMessage = ''
@@ -121,14 +107,10 @@ async function processSecureActions(
action: actionDirective.action,
})
// Perform the action with variable substitution at runtime
// This uses act() directly, which handles variables securely
const result = await stagehand.page.act({
action: actionDirective.action,
const result = await stagehand.act(actionDirective.action, {
variables: variables || {},
})
// Store the result for later reporting
executedActions.push({
action: actionDirective.action,
result: {
@@ -137,7 +119,6 @@ async function processSecureActions(
},
})
// Success message to replace the execution request
resultMessage = `\nSecure action ${actionIndex + 1} executed successfully.\n`
} catch (error) {
logger.error(`Error executing secure action ${actionIndex + 1}`, {
@@ -145,7 +126,6 @@ async function processSecureActions(
action: actionDirective.action,
})
// Store the failed result
executedActions.push({
action: actionDirective.action,
result: {
@@ -154,14 +134,11 @@ async function processSecureActions(
},
})
// Error message to replace the execution request
resultMessage = `\nError executing secure action ${actionIndex + 1}: ${error instanceof Error ? error.message : 'Unknown error'}\n`
}
// Replace the execution directive with the result message
modifiedMessage = modifiedMessage.replace(fullMatch, resultMessage)
} else {
// Invalid action index - replace with error message
const errorMessage = `\nError: Secure action ${actionIndex + 1} does not exist.\n`
modifiedMessage = modifiedMessage.replace(fullMatch, errorMessage)
}
@@ -170,7 +147,6 @@ async function processSecureActions(
return { modifiedMessage, executedActions }
}
// Helper function for direct login attempt
async function attemptDirectLogin(
stagehand: Stagehand,
variables: Record<string, string> | undefined
@@ -187,11 +163,9 @@ async function attemptDirectLogin(
}
}
// Define common variable keys for credentials
const usernameKeys = ['username', 'email', 'user']
const passwordKeys = ['password', 'pass', 'secret']
// Find the actual keys used in the variables
const usernameKey = usernameKeys.find((key) => variables[key] !== undefined)
const passwordKey = passwordKeys.find((key) => variables[key] !== undefined)
@@ -210,9 +184,8 @@ async function attemptDirectLogin(
logger.info('Attempting direct login with provided variables.')
try {
const page = stagehand.page
const page = stagehand.context.pages()[0]
// Common selectors for username/email fields
const usernameSelectors = [
'input[type="text"][name*="user"]',
'input[type="email"]',
@@ -225,7 +198,6 @@ async function attemptDirectLogin(
'input[aria-label*="email" i]',
]
// Common selectors for password fields
const passwordSelectors = [
'input[type="password"]',
'input[name*="pass"]',
@@ -234,7 +206,6 @@ async function attemptDirectLogin(
'input[aria-label*="pass" i]',
]
// Common selectors for submit buttons
const submitSelectors = [
'button[type="submit"]',
'input[type="submit"]',
@@ -246,12 +217,10 @@ async function attemptDirectLogin(
'button[name*="submit"]',
]
// Find and fill username
let usernameFilled = false
for (const selector of usernameSelectors) {
const input = page.locator(selector).first()
if ((await input.count()) > 0 && (await input.isVisible({ timeout: 1000 }))) {
// Short timeout
if ((await input.count()) > 0 && (await input.isVisible())) {
logger.info(`Found username field: ${selector}`)
await input.fill(usernameValue)
usernameFilled = true
@@ -268,11 +237,10 @@ async function attemptDirectLogin(
}
}
// Find and fill password
let passwordFilled = false
for (const selector of passwordSelectors) {
const input = page.locator(selector).first()
if ((await input.count()) > 0 && (await input.isVisible({ timeout: 1000 }))) {
if ((await input.count()) > 0 && (await input.isVisible())) {
logger.info(`Found password field: ${selector}`)
await input.fill(passwordValue)
passwordFilled = true
@@ -282,7 +250,6 @@ async function attemptDirectLogin(
if (!passwordFilled) {
logger.warn('Could not find a visible password field for direct login.')
// Even if password field not found, maybe username submit works? Unlikely but possible.
return {
attempted: true,
success: false,
@@ -291,20 +258,13 @@ async function attemptDirectLogin(
}
}
// Find and click submit button
let submitClicked = false
for (const selector of submitSelectors) {
const button = page.locator(selector).first()
// Check if button exists and is visible/enabled
if (
(await button.count()) > 0 &&
(await button.isVisible({ timeout: 1000 })) &&
(await button.isEnabled({ timeout: 1000 }))
) {
if ((await button.count()) > 0 && (await button.isVisible())) {
logger.info(`Found submit button: ${selector}`)
await button.click()
// Wait longer for login processing
await page.waitForTimeout(3000)
await new Promise((resolve) => setTimeout(resolve, 3000))
submitClicked = true
break
}
@@ -324,8 +284,6 @@ async function attemptDirectLogin(
'Direct login attempt completed (fields filled, submit clicked). Verifying result...'
)
// Verify if login was successful by checking for common success indicators
// 1. Check if we're redirected away from login page
const currentUrl = page.url()
const isStillOnLoginPage =
currentUrl.includes('login') ||
@@ -334,9 +292,7 @@ async function attemptDirectLogin(
currentUrl.includes('signup') ||
currentUrl.includes('register')
// 2. Check for login error messages
const hasLoginError = await page.evaluate(() => {
// Look for common error message elements
const errorSelectors = [
'[class*="error" i]',
'[id*="error" i]',
@@ -367,9 +323,7 @@ async function attemptDirectLogin(
return false
})
// 3. Check for common success indicators
const hasSuccessIndicators = await page.evaluate(() => {
// Check for user menu elements, profile info, etc.
const userMenuSelectors = [
'[class*="avatar" i]',
'[class*="profile" i]',
@@ -454,26 +408,19 @@ export async function POST(request: NextRequest) {
}
const params = validationResult.data
// Simplify variable handling - safely convert any format to the object we need
let variablesObject: Record<string, string> | undefined
// Handle different formats of variables that might come from the UI
if (params.variables) {
if (Array.isArray(params.variables)) {
// For array format (from table input)
variablesObject = {}
params.variables.forEach((item: any) => {
// Check if item and item.cells exist, and Key is a string
if (item?.cells?.Key && typeof item.cells.Key === 'string') {
// Access Key and Value within the 'cells' object
variablesObject![item.cells.Key] = item.cells.Value || ''
}
})
} else if (typeof params.variables === 'object' && params.variables !== null) {
// For object format (already in correct format)
variablesObject = { ...params.variables }
} else if (typeof params.variables === 'string') {
// Handle string format (sometimes comes as JSON string)
try {
variablesObject = JSON.parse(params.variables)
} catch (_e) {
@@ -481,14 +428,12 @@ export async function POST(request: NextRequest) {
}
}
// Verify we have non-empty variables
if (!variablesObject || Object.keys(variablesObject).length === 0) {
logger.warn('Variables object is empty after processing', {
originalVariables: params.variables,
variablesType: typeof params.variables,
})
// Try to recover username/password from the raw variables if we can
if (typeof params.variables === 'object' && params.variables !== null) {
variablesObject = {}
for (const key in params.variables) {
@@ -502,7 +447,6 @@ export async function POST(request: NextRequest) {
}
}
// Log the collected variables (careful not to log actual passwords)
if (variablesObject) {
const safeVarKeys = Object.keys(variablesObject).map((key) => {
return key.toLowerCase().includes('password')
@@ -519,10 +463,8 @@ export async function POST(request: NextRequest) {
const { task, startUrl: rawStartUrl, outputSchema, apiKey } = params
// Normalize the starting URL - only add https:// if needed
let startUrl = rawStartUrl
// Add https:// if no protocol is specified
startUrl = normalizeUrl(startUrl)
logger.info('Starting Stagehand agent process', {
@@ -532,7 +474,6 @@ export async function POST(request: NextRequest) {
hasVariables: !!variablesObject && Object.keys(variablesObject).length > 0,
})
// Check for required environment variables
if (!BROWSERBASE_API_KEY || !BROWSERBASE_PROJECT_ID) {
logger.error('Missing required environment variables', {
hasBrowserbaseApiKey: !!BROWSERBASE_API_KEY,
@@ -546,68 +487,41 @@ export async function POST(request: NextRequest) {
}
try {
// Initialize Stagehand with Browserbase
logger.info('Initializing Stagehand with Browserbase')
logger.info('Initializing Stagehand with Browserbase (v3)')
stagehand = new Stagehand({
env: 'BROWSERBASE',
apiKey: BROWSERBASE_API_KEY,
projectId: BROWSERBASE_PROJECT_ID,
verbose: 1,
// Use a custom logger wrapper that adapts our logger to Stagehand's expected format
logger: (msg) => logger.info(typeof msg === 'string' ? msg : JSON.stringify(msg)),
disablePino: true,
modelName: 'claude-3-7-sonnet-20250219',
modelClientOptions: {
apiKey: apiKey, // User's OpenAI API key
model: {
modelName: 'anthropic/claude-3-7-sonnet-latest',
apiKey: apiKey,
},
})
// Initialize Stagehand
logger.info('Starting stagehand.init()')
await stagehand.init()
logger.info('Stagehand initialized successfully')
// Monkey patch the page.act method to automatically apply variables to all actions
if (variablesObject && Object.keys(variablesObject).length > 0) {
logger.info('Setting up automatic variable substitution for all actions')
const originalAct = stagehand.page.act.bind(stagehand.page)
stagehand.page.act = async (options: any) => {
// If options is a string, convert it to object
if (typeof options === 'string') {
options = { action: options }
}
const page = stagehand.context.pages()[0]
// Ensure variables are included
options.variables = { ...(options.variables || {}), ...variablesObject }
logger.info('Executing act with variables', {
action: options.action,
hasVariables: true,
variableCount: Object.keys(options.variables).length,
})
// Call original method
return originalAct(options)
}
}
// Navigate to the start URL
logger.info(`Navigating to ${startUrl}`)
await stagehand.page.goto(startUrl, { waitUntil: 'networkidle' })
await page.goto(startUrl, { waitUntil: 'networkidle' })
logger.info('Navigation complete')
// Helper function to detect and navigate to login page if needed
const ensureLoginPage = async (): Promise<boolean> => {
if (!stagehand) {
logger.error('Stagehand instance is null')
return false
}
const currentPage = stagehand.context.pages()[0]
logger.info('Checking if we need to navigate to login page')
try {
// Check if we're already on a page with login form
const loginFormExists = await stagehand.page.evaluate(() => {
const loginFormExists = await currentPage.evaluate(() => {
const usernameInput = document.querySelector(
'input[type="text"], input[type="email"], input[name="username"], input[id="username"]'
)
@@ -620,10 +534,7 @@ export async function POST(request: NextRequest) {
return true
}
// Look for common login buttons/links
const loginElements = await stagehand.page.observe({
instruction: 'Find login buttons or links on this page',
})
const loginElements = await stagehand.observe('Find login buttons or links on this page')
if (loginElements && loginElements.length > 0) {
for (const element of loginElements) {
@@ -633,18 +544,13 @@ export async function POST(request: NextRequest) {
) {
logger.info(`Found login element: ${element.description}`)
// Click the login button/link
if (element.selector) {
logger.info(`Clicking login element: ${element.selector}`)
await stagehand.page.act({
action: `Click on the ${element.description}`,
})
await stagehand.act(`Click on the ${element.description}`)
// Wait for navigation or DOM changes
await stagehand.page.waitForTimeout(2000)
await new Promise((resolve) => setTimeout(resolve, 2000))
// Check if we're now on login page
const loginPageAfterClick = await stagehand.page.evaluate(() => {
const loginPageAfterClick = await currentPage.evaluate(() => {
const usernameInput = document.querySelector(
'input[type="text"], input[type="email"], input[name="username"], input[id="username"]'
)
@@ -661,15 +567,13 @@ export async function POST(request: NextRequest) {
}
}
// Try direct navigation to /login if we couldn't find login elements
logger.info('Trying direct navigation to /login path')
const currentUrl = await stagehand.page.url()
const currentUrl = currentPage.url()
const loginUrl = new URL('/login', currentUrl).toString()
await stagehand.page.goto(loginUrl, { waitUntil: 'networkidle' })
await currentPage.goto(loginUrl, { waitUntil: 'networkidle' })
// Check if we're now on login page
const loginPageAfterDirectNav = await stagehand.page.evaluate(() => {
const loginPageAfterDirectNav = await currentPage.evaluate(() => {
const usernameInput = document.querySelector(
'input[type="text"], input[type="email"], input[name="username"], input[id="username"]'
)
@@ -690,14 +594,12 @@ export async function POST(request: NextRequest) {
}
}
// --- Direct Login Logic ---
let directLoginAttempted = false
let directLoginSuccess = false
let loginMessage = ''
let taskForAgent = task // Use original task by default
let agentInstructions = '' // Will be generated below
let taskForAgent = task
let agentInstructions = ''
// Only attempt direct login if relevant variables exist
const hasLoginVars =
variablesObject &&
Object.keys(variablesObject).some((k) =>
@@ -725,7 +627,6 @@ export async function POST(request: NextRequest) {
})
if (directLoginAttempted) {
// Modify task for agent regardless of login success/failure
if (directLoginSuccess) {
taskForAgent = `Login has been completed programmatically and was successful. Please verify that you are logged in and then proceed with the original task: ${task}`
} else {
@@ -741,19 +642,15 @@ export async function POST(request: NextRequest) {
} else {
logger.info('Skipping direct login: No relevant username/password variables found.')
}
// --- End Direct Login Logic ---
// Extract action directives with variable placeholders (from original task)
const { processedTask, actionDirectives } = extractActionDirectives(task) // Use original task for extraction
const { processedTask, actionDirectives } = extractActionDirectives(task)
logger.info('Extracted action directives', {
actionCount: actionDirectives.length,
hasActionDirectives: actionDirectives.length > 0,
})
// Generate instructions based on whether direct login was attempted
if (directLoginAttempted) {
// Construct specific instructions based on login attempt outcome
const loginInstructions = directLoginSuccess
? 'Login was completed programmatically and appears successful. Please VERIFY if the login was successful by checking for elements that only appear when logged in.'
: `Login was attempted programmatically but appears to have FAILED (${loginMessage}).
@@ -767,7 +664,6 @@ Once you've verified the login state, proceed with the following task: ${task}
${actionDirectives.length > 0 ? `\n\nNote on Secure Actions: You might see [SECURE ACTION X] placeholders. Handle these by outputting "EXECUTE SECURE ACTION X" when appropriate.` : ''}
${outputSchema && typeof outputSchema === 'object' && outputSchema !== null ? `\n\nIMPORTANT: You MUST return your final result in the following JSON format exactly:\n${formatSchemaForInstructions(getSchemaObject(outputSchema))}\n\nYour response should consist of valid JSON only, with no additional text.` : ''}`
} else {
// Original detailed instructions if agent needs to handle login/placeholders
agentInstructions = `You are a helpful web browsing assistant that will complete tasks on websites. Your goal is to accomplish the following task: ${processedTask}\n
${actionDirectives.length > 0 ? `\n\nYou'll see [SECURE ACTION X] placeholders in the task. These represent secure actions that will be handled automatically when you navigate to the appropriate page. When you reach a point where a secure action should be performed, output a line with exactly: "EXECUTE SECURE ACTION X" (where X is the action number). Then wait for confirmation before proceeding.` : ''}\n
IMPORTANT: For any form fields that require sensitive information like usernames or passwords:
@@ -790,40 +686,37 @@ WEBSITE NAVIGATION GUIDANCE:
${outputSchema && typeof outputSchema === 'object' && outputSchema !== null ? `\n\nIMPORTANT: You MUST return your final result in the following JSON format exactly:\n${formatSchemaForInstructions(getSchemaObject(outputSchema))}\n\nYour response should consist of valid JSON only, with no additional text. Ensure the data in your response adheres strictly to the schema provided.` : ''}`
}
// Create agent to execute the task
logger.info('Creating Stagehand agent', {
directLoginAttempted,
directLoginSuccess,
loginMessage,
})
const agent = stagehand.agent({
provider: 'anthropic',
model: 'claude-3-7-sonnet-20250219',
instructions: agentInstructions, // Use the generated instructions
options: {
apiKey: apiKey,
// Conditional additional instructions based on direct login attempt
additionalInstructions: directLoginAttempted
? `Login was ${directLoginSuccess ? 'successfully completed' : 'attempted but failed'}.
${loginMessage}
First check the current state of the page.
If login failed, you may need to click the login button again after ensuring fields are properly filled.`
: `
const additionalContext = directLoginAttempted
? `Login was ${directLoginSuccess ? 'successfully completed' : 'attempted but failed'}.
${loginMessage}
First check the current state of the page.
If login failed, you may need to click the login button again after ensuring fields are properly filled.`
: `
This task may contain placeholder variables like %username% and %password%.
When you need to fill form fields, use these placeholders directly (e.g., type "%username%").
The system will substitute actual values when these placeholders are used, keeping sensitive data secure.
`.trim(),
`.trim()
const agent = stagehand.agent({
model: {
modelName: 'anthropic/claude-3-7-sonnet-latest',
apiKey: apiKey,
},
executionModel: {
modelName: 'anthropic/claude-3-7-sonnet-latest',
apiKey: apiKey,
},
systemPrompt: `${agentInstructions}\n\n${additionalContext}`,
})
// Since we can't use events directly, we'll need to handle secure actions
// by running the agent and then processing any EXECUTE SECURE ACTION directives
// in its output, then decide if we need to continue the conversation
// Sequence to execute agent with secure action processing
const runAgentWithSecureActions = async (): Promise<any> => {
// Use taskForAgent which might have been modified if direct login occurred
let currentResult = await agent.execute(taskForAgent)
let currentResult = await agent.execute({ instruction: taskForAgent })
let allExecutedActions: Array<{
action: string
result: { success: boolean; message: string }
@@ -833,17 +726,13 @@ The system will substitute actual values when these placeholders are used, keepi
while (iterationCount < maxIterations && stagehand !== null) {
if (!currentResult.message) {
// No message to process, we're done
break
}
// Check if there are secure action directives in the message
if (!/EXECUTE SECURE ACTION \d+/i.test(currentResult.message)) {
// No secure actions to execute, we're done
break
}
// Process secure actions in the message
const { modifiedMessage, executedActions } = await processSecureActions(
currentResult.message,
stagehand,
@@ -851,39 +740,28 @@ The system will substitute actual values when these placeholders are used, keepi
variablesObject
)
// Add executed actions to our collection
allExecutedActions = [...allExecutedActions, ...executedActions]
if (executedActions.length === 0) {
// No actions were executed, we can stop
break
}
// Continue conversation with the agent using the modified message as context
iterationCount++
// Only continue if we need to - if we reached the final state
// with structured output, don't keep going
const hasStructuredOutput = /```json|^\s*{/.test(modifiedMessage)
if (hasStructuredOutput) {
// Already has structured output, let's not continue and risk losing it
currentResult.message = modifiedMessage
break
}
// Continue the conversation with the agent
logger.info(
`Continuing agent execution with processed actions, iteration ${iterationCount}`
)
try {
// Here we'd continue the agent conversation, but since agent.execute
// doesn't support continuation easily, we have to create a new prompt
// that synthesizes what's happened so far
const continuationPrompt = `${modifiedMessage}\n\nPlease continue with the task.`
const nextResult = await agent.execute(continuationPrompt)
const nextResult = await agent.execute({ instruction: continuationPrompt })
// Merge results - keep actions from both iterations but update message
currentResult = {
...nextResult,
actions: [...currentResult.actions, ...nextResult.actions],
@@ -894,16 +772,14 @@ The system will substitute actual values when these placeholders are used, keepi
}
}
// Return the final result and all executed secure actions
return {
...currentResult,
secureActions: allExecutedActions,
}
}
// Execute the agent with secure action handling
logger.info('Executing agent task', {
task: taskForAgent, // Log the task actually given to the agent
task: taskForAgent,
actionDirectiveCount: actionDirectives.length,
directLoginAttempted,
directLoginSuccess,
@@ -925,59 +801,51 @@ The system will substitute actual values when these placeholders are used, keepi
executedActionCount: agentExecutionResult.secureActions?.length || 0,
})
// Parse the structured data from the agent's message if possible
let structuredOutput = null
const hasOutputSchema =
outputSchema && typeof outputSchema === 'object' && outputSchema !== null
if (agentResult.message) {
try {
// Try to parse JSON from the message
// First, clean up the message to extract just the JSON
let jsonContent = agentResult.message
// Look for JSON block markers in case the model wrapped it in ```json blocks
const jsonBlockMatch = jsonContent.match(/```(?:json)?\s*([\s\S]*?)\s*```/)
if (jsonBlockMatch?.[1]) {
jsonContent = jsonBlockMatch[1]
}
// Try to parse the content as JSON
structuredOutput = JSON.parse(jsonContent)
logger.info('Successfully parsed structured output from agent response')
} catch (parseError) {
logger.error('Failed to parse JSON from agent message', {
error: parseError,
message: agentResult.message,
})
if (hasOutputSchema) {
logger.warn('Failed to parse JSON from agent message, attempting fallback extraction', {
error: parseError,
})
// If we have a schema, try one more approach with extract
if (
outputSchema &&
typeof outputSchema === 'object' &&
outputSchema !== null &&
stagehand
) {
try {
logger.info('Attempting to extract structured data using Stagehand extract')
const schemaObj = getSchemaObject(outputSchema)
const zodSchema = ensureZodObject(logger, schemaObj)
if (stagehand) {
try {
logger.info('Attempting to extract structured data using Stagehand extract')
const schemaObj = getSchemaObject(outputSchema)
const zodSchema = ensureZodObject(logger, schemaObj)
// Use the extract API to get structured data from whatever page we ended up on
structuredOutput = await stagehand.page.extract({
instruction:
structuredOutput = await stagehand.extract(
'Extract the requested information from this page according to the schema',
schema: zodSchema,
} as any)
zodSchema
)
logger.info('Successfully extracted structured data as fallback', {
keys: structuredOutput ? Object.keys(structuredOutput) : [],
})
} catch (extractError) {
logger.error('Fallback extraction also failed', { error: extractError })
logger.info('Successfully extracted structured data as fallback', {
keys: structuredOutput ? Object.keys(structuredOutput) : [],
})
} catch (extractError) {
logger.error('Fallback extraction also failed', { error: extractError })
}
}
} else {
logger.info('Agent returned plain text response (no schema provided)')
}
}
}
// Return agent result, structured output, and secure action results
return NextResponse.json({
agentResult,
structuredOutput,
@@ -990,7 +858,6 @@ The system will substitute actual values when these placeholders are used, keepi
stack: error instanceof Error ? error.stack : undefined,
})
// Provide detailed error information
let errorMessage = 'Unknown error during agent execution'
let errorDetails: Record<string, any> = {}
@@ -1001,7 +868,6 @@ The system will substitute actual values when these placeholders are used, keepi
stack: error.stack,
}
// Log additional properties for context
const errorObj = error as any
if (typeof errorObj.code !== 'undefined') {
errorDetails.code = errorObj.code
@@ -1036,7 +902,6 @@ The system will substitute actual values when these placeholders are used, keepi
{ status: 500 }
)
} finally {
// Clean up Stagehand resources
if (stagehand) {
try {
logger.info('Closing Stagehand instance')

View File

@@ -7,7 +7,6 @@ import { ensureZodObject, normalizeUrl } from '@/app/api/tools/stagehand/utils'
const logger = createLogger('StagehandExtractAPI')
// Environment variables for Browserbase
const BROWSERBASE_API_KEY = env.BROWSERBASE_API_KEY
const BROWSERBASE_PROJECT_ID = env.BROWSERBASE_PROJECT_ID
@@ -21,7 +20,7 @@ const requestSchema = z.object({
})
export async function POST(request: NextRequest) {
let stagehand = null
let stagehand: Stagehand | null = null
try {
const body = await request.json()
@@ -42,14 +41,13 @@ export async function POST(request: NextRequest) {
}
const params = validationResult.data
const { url: rawUrl, instruction, selector, useTextExtract, apiKey, schema } = params
const { url: rawUrl, instruction, selector, apiKey, schema } = params
const url = normalizeUrl(rawUrl)
logger.info('Starting Stagehand extraction process', {
rawUrl,
url,
hasInstruction: !!instruction,
useTextExtract: !!useTextExtract,
schemaType: typeof schema,
})
@@ -79,16 +77,16 @@ export async function POST(request: NextRequest) {
}
try {
logger.info('Initializing Stagehand with Browserbase')
logger.info('Initializing Stagehand with Browserbase (v3)')
stagehand = new Stagehand({
env: 'BROWSERBASE',
apiKey: BROWSERBASE_API_KEY,
projectId: BROWSERBASE_PROJECT_ID,
verbose: 1,
logger: (msg) => logger.info(typeof msg === 'string' ? msg : JSON.stringify(msg)),
disablePino: true,
modelName: 'gpt-4o',
modelClientOptions: {
model: {
modelName: 'openai/gpt-4o',
apiKey: apiKey,
},
})
@@ -97,8 +95,10 @@ export async function POST(request: NextRequest) {
await stagehand.init()
logger.info('Stagehand initialized successfully')
const page = stagehand.context.pages()[0]
logger.info(`Navigating to ${url}`)
await stagehand.page.goto(url, { waitUntil: 'networkidle' })
await page.goto(url, { waitUntil: 'networkidle' })
logger.info('Navigation complete')
logger.info('Preparing extraction schema', {
@@ -130,32 +130,19 @@ export async function POST(request: NextRequest) {
zodSchema = undefined
}
const extractOptions: any = {
instruction,
useTextExtract: !!useTextExtract,
}
if (zodSchema) {
extractOptions.schema = zodSchema
}
if (selector) {
logger.info(`Using selector: ${selector}`)
extractOptions.selector = selector
}
logger.info('Calling stagehand.page.extract with options', {
hasInstruction: !!extractOptions.instruction,
hasSchema: !!extractOptions.schema,
hasSelector: !!extractOptions.selector,
useTextExtract: extractOptions.useTextExtract,
logger.info('Calling stagehand.extract with options', {
hasInstruction: !!instruction,
hasSchema: !!zodSchema,
hasSelector: !!selector,
})
let extractedData
if (zodSchema) {
extractedData = await stagehand.page.extract(extractOptions)
extractedData = await stagehand.extract(instruction, zodSchema, {
selector: selector || undefined,
})
} else {
extractedData = await stagehand.page.extract(extractOptions.instruction)
extractedData = await stagehand.extract(instruction)
}
logger.info('Extraction successful', {

View File

@@ -0,0 +1,224 @@
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 {
getFileExtension,
getMimeTypeFromExtension,
processSingleFileToUserFile,
} from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
export const dynamic = 'force-dynamic'
const logger = createLogger('WordPressUploadAPI')
const WORDPRESS_COM_API_BASE = 'https://public-api.wordpress.com/wp/v2/sites'
const WordPressUploadSchema = z.object({
accessToken: z.string().min(1, 'Access token is required'),
siteId: z.string().min(1, 'Site ID is required'),
file: z.any().optional().nullable(),
filename: z.string().optional().nullable(),
title: z.string().optional().nullable(),
caption: z.string().optional().nullable(),
altText: z.string().optional().nullable(),
description: z.string().optional().nullable(),
})
export async function POST(request: NextRequest) {
const requestId = generateRequestId()
try {
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized WordPress upload attempt: ${authResult.error}`)
return NextResponse.json(
{
success: false,
error: authResult.error || 'Authentication required',
},
{ status: 401 }
)
}
logger.info(
`[${requestId}] Authenticated WordPress upload request via ${authResult.authType}`,
{
userId: authResult.userId,
}
)
const body = await request.json()
const validatedData = WordPressUploadSchema.parse(body)
logger.info(`[${requestId}] Uploading file to WordPress`, {
siteId: validatedData.siteId,
filename: validatedData.filename,
hasFile: !!validatedData.file,
})
if (!validatedData.file) {
return NextResponse.json(
{
success: false,
error: 'No file provided. Please upload a file.',
},
{ status: 400 }
)
}
// Process file - convert to UserFile format if needed
const fileData = validatedData.file
let userFile
try {
userFile = processSingleFileToUserFile(fileData, requestId, logger)
} catch (error) {
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Failed to process file',
},
{ status: 400 }
)
}
logger.info(`[${requestId}] Downloading file from storage`, {
fileName: userFile.name,
key: userFile.key,
size: userFile.size,
})
let fileBuffer: Buffer
try {
fileBuffer = await downloadFileFromStorage(userFile, requestId, logger)
} catch (error) {
logger.error(`[${requestId}] Failed to download file:`, error)
return NextResponse.json(
{
success: false,
error: `Failed to download file: ${error instanceof Error ? error.message : 'Unknown error'}`,
},
{ status: 500 }
)
}
// Use provided filename or fall back to the original file name
const filename = validatedData.filename || userFile.name
const mimeType = userFile.type || getMimeTypeFromExtension(getFileExtension(filename))
logger.info(`[${requestId}] Uploading to WordPress`, {
siteId: validatedData.siteId,
filename,
mimeType,
size: fileBuffer.length,
})
// Upload to WordPress using multipart form data
const formData = new FormData()
// Convert Buffer to Uint8Array for Blob compatibility
const uint8Array = new Uint8Array(fileBuffer)
const blob = new Blob([uint8Array], { type: mimeType })
formData.append('file', blob, filename)
// Add optional metadata
if (validatedData.title) {
formData.append('title', validatedData.title)
}
if (validatedData.caption) {
formData.append('caption', validatedData.caption)
}
if (validatedData.altText) {
formData.append('alt_text', validatedData.altText)
}
if (validatedData.description) {
formData.append('description', validatedData.description)
}
const uploadResponse = await fetch(`${WORDPRESS_COM_API_BASE}/${validatedData.siteId}/media`, {
method: 'POST',
headers: {
Authorization: `Bearer ${validatedData.accessToken}`,
},
body: formData,
})
if (!uploadResponse.ok) {
const errorText = await uploadResponse.text()
let errorMessage = `WordPress API error: ${uploadResponse.statusText}`
try {
const errorJson = JSON.parse(errorText)
errorMessage = errorJson.message || errorJson.error || errorMessage
} catch {
// Use default error message
}
logger.error(`[${requestId}] WordPress API error:`, {
status: uploadResponse.status,
statusText: uploadResponse.statusText,
error: errorText,
})
return NextResponse.json(
{
success: false,
error: errorMessage,
},
{ status: uploadResponse.status }
)
}
const uploadData = await uploadResponse.json()
logger.info(`[${requestId}] File uploaded successfully`, {
mediaId: uploadData.id,
sourceUrl: uploadData.source_url,
})
return NextResponse.json({
success: true,
output: {
media: {
id: uploadData.id,
date: uploadData.date,
slug: uploadData.slug,
type: uploadData.type,
link: uploadData.link,
title: uploadData.title,
caption: uploadData.caption,
alt_text: uploadData.alt_text,
media_type: uploadData.media_type,
mime_type: uploadData.mime_type,
source_url: uploadData.source_url,
media_details: uploadData.media_details,
},
},
})
} 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 uploading file to WordPress:`, error)
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Internal server error',
},
{ status: 500 }
)
}
}

View File

@@ -111,6 +111,7 @@ export async function GET(request: NextRequest) {
id: workflowExecutionLogs.id,
workflowId: workflowExecutionLogs.workflowId,
executionId: workflowExecutionLogs.executionId,
deploymentVersionId: workflowExecutionLogs.deploymentVersionId,
level: workflowExecutionLogs.level,
trigger: workflowExecutionLogs.trigger,
startedAt: workflowExecutionLogs.startedAt,
@@ -161,6 +162,7 @@ export async function GET(request: NextRequest) {
id: log.id,
workflowId: log.workflowId,
executionId: log.executionId,
deploymentVersionId: log.deploymentVersionId,
level: log.level,
trigger: log.trigger,
startedAt: log.startedAt.toISOString(),

View File

@@ -61,6 +61,8 @@ export async function GET(request: NextRequest) {
{ status: 500 }
)
} finally {
await releaseLock(LOCK_KEY).catch(() => {})
if (lockValue) {
await releaseLock(LOCK_KEY, lockValue).catch(() => {})
}
}
}

View File

@@ -61,6 +61,8 @@ export async function GET(request: NextRequest) {
{ status: 500 }
)
} finally {
await releaseLock(LOCK_KEY).catch(() => {})
if (lockValue) {
await releaseLock(LOCK_KEY, lockValue).catch(() => {})
}
}
}

View File

@@ -61,6 +61,8 @@ export async function GET(request: NextRequest) {
{ status: 500 }
)
} finally {
await releaseLock(LOCK_KEY).catch(() => {})
if (lockValue) {
await releaseLock(LOCK_KEY, lockValue).catch(() => {})
}
}
}

View File

@@ -430,6 +430,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
edges: any[]
loops: Record<string, any>
parallels: Record<string, any>
deploymentVersionId?: string
} | null = null
let processedInput = input
@@ -444,6 +445,10 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
edges: workflowData.edges,
loops: workflowData.loops || {},
parallels: workflowData.parallels || {},
deploymentVersionId:
!shouldUseDraftState && 'deploymentVersionId' in workflowData
? (workflowData.deploymentVersionId as string)
: undefined,
}
const serializedWorkflow = new Serializer().serializeWorkflow(

View File

@@ -29,6 +29,7 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { Skeleton } from '@/components/ui/skeleton'
import { VerifiedBadge } from '@/components/ui/verified-badge'
import { useSession } from '@/lib/auth/auth-client'
import { cn } from '@/lib/core/utils/cn'
@@ -41,6 +42,95 @@ import { useStarTemplate, useTemplate } from '@/hooks/queries/templates'
const logger = createLogger('TemplateDetails')
interface TemplateDetailsLoadingProps {
isWorkspaceContext?: boolean
workspaceId?: string | null
}
function TemplateDetailsLoading({ isWorkspaceContext, workspaceId }: TemplateDetailsLoadingProps) {
const breadcrumbItems = [
{
label: 'Templates',
href:
isWorkspaceContext && workspaceId ? `/workspace/${workspaceId}/templates` : '/templates',
},
{ label: 'Template' },
]
return (
<div
className={cn(
'flex flex-col',
isWorkspaceContext ? 'h-full flex-1 overflow-hidden' : 'min-h-screen'
)}
>
<div className={cn('flex flex-1', isWorkspaceContext && 'overflow-hidden')}>
<div
className={cn(
'flex flex-1 flex-col px-[24px] pt-[24px] pb-[24px]',
isWorkspaceContext ? 'overflow-auto' : 'overflow-visible'
)}
>
{/* Breadcrumb navigation */}
<Breadcrumb items={breadcrumbItems} />
{/* Template name and action buttons */}
<div className='mt-[14px] flex items-center justify-between'>
<Skeleton className='h-[27px] w-[250px] rounded-[4px]' />
<div className='flex items-center gap-[8px]'>
<Skeleton className='h-[32px] w-[80px] rounded-[6px]' />
</div>
</div>
{/* Template tagline */}
<div className='mt-[4px]'>
<Skeleton className='h-[21px] w-[400px] rounded-[4px]' />
</div>
{/* Creator and stats row */}
<div className='mt-[16px] flex items-center gap-[8px]'>
{/* Star icon and count */}
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
<Skeleton className='h-[21px] w-[24px] rounded-[4px]' />
{/* Views icon and count */}
<Skeleton className='h-[16px] w-[16px] rounded-[2px]' />
<Skeleton className='h-[21px] w-[32px] rounded-[4px]' />
{/* Vertical divider */}
<div className='mx-[4px] mb-[-1.5px] h-[18px] w-[1.25px] rounded-full bg-[var(--border)]' />
{/* Creator profile pic */}
<Skeleton className='h-[16px] w-[16px] rounded-full' />
{/* Creator name */}
<Skeleton className='h-[21px] w-[100px] rounded-[4px]' />
</div>
{/* Credentials needed */}
<div className='mt-[12px]'>
<Skeleton className='h-[18px] w-[280px] rounded-[4px]' />
</div>
{/* Canvas preview */}
<div className='relative mt-[24px] h-[450px] w-full flex-shrink-0 overflow-hidden rounded-[8px] border border-[var(--border)]'>
<Skeleton className='h-full w-full rounded-none' />
</div>
{/* About this Workflow */}
<div className='mt-8'>
<Skeleton className='mb-4 h-[24px] w-[180px] rounded-[4px]' />
<div className='space-y-2'>
<Skeleton className='h-[18px] w-full rounded-[4px]' />
<Skeleton className='h-[18px] w-[90%] rounded-[4px]' />
<Skeleton className='h-[18px] w-[75%] rounded-[4px]' />
</div>
</div>
</div>
</div>
</div>
)
}
interface TemplateDetailsProps {
isWorkspaceContext?: boolean
}
@@ -207,11 +297,7 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
if (loading) {
return (
<div className='flex h-screen items-center justify-center'>
<div className='text-center'>
<p className='font-sans text-muted-foreground text-sm'>Loading template...</p>
</div>
</div>
<TemplateDetailsLoading isWorkspaceContext={isWorkspaceContext} workspaceId={workspaceId} />
)
}
@@ -542,9 +628,19 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
}
return (
<div className={cn('flex flex-col', isWorkspaceContext ? 'h-full flex-1' : 'min-h-screen')}>
<div className='flex flex-1 overflow-hidden'>
<div className='flex flex-1 flex-col overflow-auto px-[24px] pt-[24px] pb-[24px]'>
<div
className={cn(
'flex flex-col',
isWorkspaceContext ? 'h-full flex-1 overflow-hidden' : 'min-h-screen'
)}
>
<div className={cn('flex flex-1', isWorkspaceContext && 'overflow-hidden')}>
<div
className={cn(
'flex flex-1 flex-col px-[24px] pt-[24px] pb-[24px]',
isWorkspaceContext ? 'overflow-auto' : 'overflow-visible'
)}
>
{/* Breadcrumb navigation */}
<Breadcrumb items={breadcrumbItems} />
@@ -697,7 +793,7 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
{/* Template tagline */}
{template.details?.tagline && (
<p className='mt-[4px] font-medium text-[14px] text-[var(--text-tertiary)]'>
<p className='mt-[4px] line-clamp-2 max-w-[40vw] font-medium text-[14px] text-[var(--text-tertiary)]'>
{template.details.tagline}
</p>
)}
@@ -770,7 +866,7 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
{/* Canvas preview */}
<div
className='relative mt-[24px] h-[450px] w-full overflow-hidden rounded-[8px] border border-[var(--border)]'
className='relative mt-[24px] h-[450px] w-full flex-shrink-0 overflow-hidden rounded-[8px] border border-[var(--border)]'
onWheelCapture={handleCanvasWheelCapture}
>
{renderWorkflowPreview()}

View File

@@ -6,7 +6,7 @@ import { season } from '@/app/_styles/fonts/season/season'
export default function TemplatesLayoutClient({ children }: { children: React.ReactNode }) {
return (
<Tooltip.Provider delayDuration={600} skipDelayDuration={0}>
<div className={`${season.variable} font-season`}>{children}</div>
<div className={`${season.variable} flex min-h-screen flex-col font-season`}>{children}</div>
</Tooltip.Provider>
)
}

View File

@@ -12,7 +12,6 @@ import {
ModalFooter,
ModalHeader,
} from '@/components/emcn'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { cn } from '@/lib/core/utils/cn'
import { createLogger } from '@/lib/logs/console/logger'
import { formatFileSize, validateKnowledgeBaseFile } from '@/lib/uploads/utils/file-utils'
@@ -53,7 +52,7 @@ export function AddDocumentsModal({
const [dragCounter, setDragCounter] = useState(0)
const [retryingIndexes, setRetryingIndexes] = useState<Set<number>>(new Set())
const { isUploading, uploadProgress, uploadFiles, clearError } = useKnowledgeUpload({
const { isUploading, uploadProgress, uploadFiles, uploadError, clearError } = useKnowledgeUpload({
workspaceId,
onUploadComplete: () => {
logger.info(`Successfully uploaded ${files.length} files`)
@@ -234,11 +233,7 @@ export function AddDocumentsModal({
<div className='min-h-0 flex-1 overflow-y-auto'>
<div className='space-y-[12px]'>
{fileError && (
<Alert variant='destructive'>
<AlertCircle className='h-4 w-4' />
<AlertTitle>Error</AlertTitle>
<AlertDescription>{fileError}</AlertDescription>
</Alert>
<p className='text-[11px] text-[var(--text-error)] leading-tight'>{fileError}</p>
)}
<div className='flex flex-col gap-[8px]'>
@@ -341,24 +336,31 @@ export function AddDocumentsModal({
</div>
</ModalBody>
<ModalFooter>
<Button variant='default' onClick={handleClose} type='button' disabled={isUploading}>
Cancel
</Button>
<Button
variant='primary'
type='button'
onClick={handleUpload}
disabled={files.length === 0 || isUploading}
>
{isUploading
? uploadProgress.stage === 'uploading'
? `Uploading ${uploadProgress.filesCompleted}/${uploadProgress.totalFiles}...`
: uploadProgress.stage === 'processing'
? 'Processing...'
: 'Uploading...'
: 'Upload'}
</Button>
<ModalFooter className='flex-col items-stretch gap-[12px]'>
{uploadError && (
<p className='text-[11px] text-[var(--text-error)] leading-tight'>
{uploadError.message}
</p>
)}
<div className='flex justify-end gap-[8px]'>
<Button variant='default' onClick={handleClose} type='button' disabled={isUploading}>
Cancel
</Button>
<Button
variant='primary'
type='button'
onClick={handleUpload}
disabled={files.length === 0 || isUploading}
>
{isUploading
? uploadProgress.stage === 'uploading'
? `Uploading ${uploadProgress.filesCompleted}/${uploadProgress.totalFiles}...`
: uploadProgress.stage === 'processing'
? 'Processing...'
: 'Uploading...'
: 'Upload'}
</Button>
</div>
</ModalFooter>
</ModalContent>
</Modal>

View File

@@ -17,7 +17,6 @@ import {
ModalHeader,
Textarea,
} from '@/components/emcn'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { cn } from '@/lib/core/utils/cn'
import { createLogger } from '@/lib/logs/console/logger'
import { formatFileSize, validateKnowledgeBaseFile } from '@/lib/uploads/utils/file-utils'
@@ -89,7 +88,7 @@ export function CreateBaseModal({
const scrollContainerRef = useRef<HTMLDivElement>(null)
const { uploadFiles, isUploading, uploadProgress, clearError } = useKnowledgeUpload({
const { uploadFiles, isUploading, uploadProgress, uploadError, clearError } = useKnowledgeUpload({
workspaceId,
onUploadComplete: (uploadedFiles) => {
logger.info(`Successfully uploaded ${uploadedFiles.length} files`)
@@ -280,21 +279,35 @@ export function CreateBaseModal({
const newKnowledgeBase = result.data
if (files.length > 0) {
newKnowledgeBase.docCount = files.length
try {
const uploadedFiles = await uploadFiles(files, newKnowledgeBase.id, {
chunkSize: data.maxChunkSize,
minCharactersPerChunk: data.minChunkSize,
chunkOverlap: data.overlapSize,
recipe: 'default',
})
if (onKnowledgeBaseCreated) {
onKnowledgeBaseCreated(newKnowledgeBase)
logger.info(`Successfully uploaded ${uploadedFiles.length} files`)
logger.info(`Started processing ${uploadedFiles.length} documents in the background`)
newKnowledgeBase.docCount = uploadedFiles.length
if (onKnowledgeBaseCreated) {
onKnowledgeBaseCreated(newKnowledgeBase)
}
} catch (uploadError) {
// If file upload fails completely, delete the knowledge base to avoid orphaned empty KB
logger.error('File upload failed, deleting knowledge base:', uploadError)
try {
await fetch(`/api/knowledge/${newKnowledgeBase.id}`, {
method: 'DELETE',
})
logger.info(`Deleted orphaned knowledge base: ${newKnowledgeBase.id}`)
} catch (deleteError) {
logger.error('Failed to delete orphaned knowledge base:', deleteError)
}
throw uploadError
}
const uploadedFiles = await uploadFiles(files, newKnowledgeBase.id, {
chunkSize: data.maxChunkSize,
minCharactersPerChunk: data.minChunkSize,
chunkOverlap: data.overlapSize,
recipe: 'default',
})
logger.info(`Successfully uploaded ${uploadedFiles.length} files`)
logger.info(`Started processing ${uploadedFiles.length} documents in the background`)
} else {
if (onKnowledgeBaseCreated) {
onKnowledgeBaseCreated(newKnowledgeBase)
@@ -325,14 +338,6 @@ export function CreateBaseModal({
<ModalBody className='!pb-[16px]'>
<div ref={scrollContainerRef} className='min-h-0 flex-1 overflow-y-auto'>
<div className='space-y-[12px]'>
{submitStatus && submitStatus.type === 'error' && (
<Alert variant='destructive'>
<AlertCircle className='h-4 w-4' />
<AlertTitle>Error</AlertTitle>
<AlertDescription>{submitStatus.message}</AlertDescription>
</Alert>
)}
<div className='flex flex-col gap-[8px]'>
<Label htmlFor='name'>Name</Label>
<Input
@@ -498,36 +503,39 @@ export function CreateBaseModal({
)}
{fileError && (
<Alert variant='destructive'>
<AlertCircle className='h-4 w-4' />
<AlertTitle>Error</AlertTitle>
<AlertDescription>{fileError}</AlertDescription>
</Alert>
<p className='text-[11px] text-[var(--text-error)] leading-tight'>{fileError}</p>
)}
</div>
</div>
</ModalBody>
<ModalFooter>
<Button
variant='default'
onClick={() => handleClose(false)}
type='button'
disabled={isSubmitting}
>
Cancel
</Button>
<Button variant='primary' type='submit' disabled={isSubmitting || !nameValue?.trim()}>
{isSubmitting
? isUploading
? uploadProgress.stage === 'uploading'
? `Uploading ${uploadProgress.filesCompleted}/${uploadProgress.totalFiles}...`
: uploadProgress.stage === 'processing'
? 'Processing...'
: 'Creating...'
: 'Creating...'
: 'Create'}
</Button>
<ModalFooter className='flex-col items-stretch gap-[12px]'>
{(submitStatus?.type === 'error' || uploadError) && (
<p className='text-[11px] text-[var(--text-error)] leading-tight'>
{uploadError?.message || submitStatus?.message}
</p>
)}
<div className='flex justify-end gap-[8px]'>
<Button
variant='default'
onClick={() => handleClose(false)}
type='button'
disabled={isSubmitting}
>
Cancel
</Button>
<Button variant='primary' type='submit' disabled={isSubmitting || !nameValue?.trim()}>
{isSubmitting
? isUploading
? uploadProgress.stage === 'uploading'
? `Uploading ${uploadProgress.filesCompleted}/${uploadProgress.totalFiles}...`
: uploadProgress.stage === 'processing'
? 'Processing...'
: 'Creating...'
: 'Creating...'
: 'Create'}
</Button>
</div>
</ModalFooter>
</form>
</ModalContent>

View File

@@ -1,5 +1,6 @@
import { useCallback, useState } from 'react'
import { createLogger } from '@/lib/logs/console/logger'
import { getFileExtension, getMimeTypeFromExtension } from '@/lib/uploads/utils/file-utils'
const logger = createLogger('KnowledgeUpload')
@@ -143,6 +144,17 @@ const calculateThroughputMbps = (bytes: number, durationMs: number) => {
*/
const formatDurationSeconds = (durationMs: number) => Number((durationMs / 1000).toFixed(2))
/**
* Gets the content type for a file, falling back to extension-based lookup if browser doesn't provide one
*/
const getFileContentType = (file: File): string => {
if (file.type?.trim()) {
return file.type
}
const extension = getFileExtension(file.name)
return getMimeTypeFromExtension(extension)
}
/**
* Runs async operations with concurrency limit
*/
@@ -280,7 +292,7 @@ const getPresignedData = async (
},
body: JSON.stringify({
fileName: file.name,
contentType: file.type,
contentType: getFileContentType(file),
fileSize: file.size,
}),
signal: localController.signal,
@@ -529,7 +541,9 @@ export function useKnowledgeUpload(options: UseKnowledgeUploadOptions = {}) {
throughputMbps: calculateThroughputMbps(file.size, durationMs),
status: xhr.status,
})
resolve(createUploadedFile(file.name, fullFileUrl, file.size, file.type, file))
resolve(
createUploadedFile(file.name, fullFileUrl, file.size, getFileContentType(file), file)
)
} else {
logger.error('S3 PUT request failed', {
status: xhr.status,
@@ -597,7 +611,7 @@ export function useKnowledgeUpload(options: UseKnowledgeUploadOptions = {}) {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
fileName: file.name,
contentType: file.type,
contentType: getFileContentType(file),
fileSize: file.size,
}),
})
@@ -736,7 +750,7 @@ export function useKnowledgeUpload(options: UseKnowledgeUploadOptions = {}) {
const fullFileUrl = path.startsWith('http') ? path : `${window.location.origin}${path}`
return createUploadedFile(file.name, fullFileUrl, file.size, file.type, file)
return createUploadedFile(file.name, fullFileUrl, file.size, getFileContentType(file), file)
} catch (error) {
logger.error(`Multipart upload failed for ${file.name}:`, error)
const durationMs = getHighResTime() - startTime
@@ -800,7 +814,7 @@ export function useKnowledgeUpload(options: UseKnowledgeUploadOptions = {}) {
file.name,
filePath.startsWith('http') ? filePath : `${window.location.origin}${filePath}`,
file.size,
file.type,
getFileContentType(file),
file
)
} finally {
@@ -855,7 +869,7 @@ export function useKnowledgeUpload(options: UseKnowledgeUploadOptions = {}) {
const batchRequest = {
files: batchFiles.map((file) => ({
fileName: file.name,
contentType: file.type,
contentType: getFileContentType(file),
fileSize: file.size,
})),
}

View File

@@ -199,7 +199,7 @@ export function Knowledge() {
</div>
</div>
<div className='mt-[24px] grid grid-cols-1 gap-x-[20px] gap-y-[40px] md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4'>
<div className='mt-[24px] grid grid-cols-1 gap-[20px] md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4'>
{isLoading ? (
<BaseCardSkeletonGrid count={8} />
) : filteredAndSortedKnowledgeBases.length === 0 ? (

View File

@@ -2,5 +2,5 @@
* Knowledge Base layout - applies sidebar padding for all knowledge routes.
*/
export default function KnowledgeLayout({ children }: { children: React.ReactNode }) {
return <div className='flex h-full flex-1 flex-col pl-60'>{children}</div>
return <div className='flex h-full flex-1 flex-col overflow-hidden pl-60'>{children}</div>
}

View File

@@ -30,8 +30,8 @@ export function LineChart({
}) {
const containerRef = useRef<HTMLDivElement | null>(null)
const uniqueId = useRef(`chart-${Math.random().toString(36).substring(2, 9)}`).current
const [containerWidth, setContainerWidth] = useState<number>(420)
const width = containerWidth
const [containerWidth, setContainerWidth] = useState<number | null>(null)
const width = containerWidth ?? 0
const height = 166
const padding = { top: 16, right: 28, bottom: 26, left: 26 }
useEffect(() => {
@@ -39,14 +39,14 @@ export function LineChart({
const element = containerRef.current
const ro = new ResizeObserver((entries) => {
const entry = entries[0]
if (entry?.contentRect) {
if (entry?.contentRect && entry.contentRect.width > 0) {
const w = Math.max(280, Math.floor(entry.contentRect.width))
setContainerWidth(w)
}
})
ro.observe(element)
const rect = element.getBoundingClientRect()
if (rect?.width) setContainerWidth(Math.max(280, Math.floor(rect.width)))
if (rect?.width && rect.width > 0) setContainerWidth(Math.max(280, Math.floor(rect.width)))
return () => ro.disconnect()
}, [])
const chartWidth = width - padding.left - padding.right
@@ -95,6 +95,16 @@ export function LineChart({
const hasExternalWrapper = !label || label === ''
if (containerWidth === null) {
return (
<div
ref={containerRef}
className={cn('w-full', !hasExternalWrapper && 'rounded-lg border bg-card p-4')}
style={{ height }}
/>
)
}
if (data.length === 0) {
return (
<div

View File

@@ -40,6 +40,14 @@ interface WorkflowExecution {
const DEFAULT_SEGMENTS = 72
const MIN_SEGMENT_PX = 10
/**
* Predetermined heights for skeleton bars to avoid hydration mismatch.
* Using static values instead of Math.random() ensures server/client consistency.
*/
const SKELETON_BAR_HEIGHTS = [
45, 72, 38, 85, 52, 68, 30, 90, 55, 42, 78, 35, 88, 48, 65, 28, 82, 58, 40, 75, 32, 95, 50, 70,
]
/**
* Skeleton loader for a single graph card
*/
@@ -56,12 +64,12 @@ function GraphCardSkeleton({ title }: { title: string }) {
<div className='flex h-[166px] flex-col justify-end gap-[4px]'>
{/* Skeleton bars simulating chart */}
<div className='flex items-end gap-[2px]'>
{Array.from({ length: 24 }).map((_, i) => (
{SKELETON_BAR_HEIGHTS.map((height, i) => (
<Skeleton
key={i}
className='flex-1'
style={{
height: `${Math.random() * 80 + 20}%`,
height: `${height}%`,
}}
/>
))}
@@ -131,7 +139,7 @@ function WorkflowsListSkeleton({ rowCount = 5 }: { rowCount?: number }) {
*/
function DashboardSkeleton() {
return (
<div className='mt-[24px] flex min-h-0 flex-1 flex-col'>
<div className='mt-[24px] flex min-h-0 flex-1 flex-col pb-[24px]'>
{/* Graphs Section */}
<div className='mb-[16px] flex-shrink-0'>
<div className='grid grid-cols-1 gap-[16px] md:grid-cols-3'>
@@ -289,9 +297,19 @@ export default function Dashboard({
const executions = metricsQuery.data?.workflows ?? []
const aggregateSegments = metricsQuery.data?.aggregateSegments ?? []
const loading = metricsQuery.isLoading
const error = metricsQuery.error?.message ?? null
/**
* Loading state logic using TanStack Query best practices:
* - isPending: true when there's no cached data (initial load only)
* - isFetching: true when any fetch is in progress
* - isPlaceholderData: true when showing stale data from keepPreviousData
*
* We only show skeleton on initial load (isPending + no data).
* For subsequent fetches, keepPreviousData shows stale content while fetching.
*/
const showSkeleton = metricsQuery.isPending && !metricsQuery.data
// Check if any filters are actually applied
const hasActiveFilters = useMemo(
() =>
@@ -747,7 +765,7 @@ export default function Dashboard({
}
}, [refreshTrigger])
if (loading) {
if (showSkeleton) {
return <DashboardSkeleton />
}
@@ -774,7 +792,7 @@ export default function Dashboard({
}
return (
<div className='mt-[24px] flex min-h-0 flex-1 flex-col'>
<div className='mt-[24px] flex min-h-0 flex-1 flex-col pb-[24px]'>
{/* Graphs Section */}
<div className='mb-[16px] flex-shrink-0'>
<div className='grid grid-cols-1 gap-[16px] md:grid-cols-3'>
@@ -793,7 +811,6 @@ export default function Dashboard({
<div className='flex-1 overflow-y-auto rounded-t-[6px] bg-[var(--surface-1)] px-[14px] py-[10px]'>
{globalDetails ? (
<LineChart
key={`runs-${expandedWorkflowId || 'all'}-${Object.keys(selectedSegments).length}-${filteredExecutions.length}`}
data={globalDetails.executionCounts}
label=''
color='var(--brand-tertiary)'
@@ -822,7 +839,6 @@ export default function Dashboard({
<div className='flex-1 overflow-y-auto rounded-t-[6px] bg-[var(--surface-1)] px-[14px] py-[10px]'>
{globalDetails ? (
<LineChart
key={`errors-${expandedWorkflowId || 'all'}-${Object.keys(selectedSegments).length}-${filteredExecutions.length}`}
data={globalDetails.failureCounts}
label=''
color='var(--text-error)'
@@ -851,7 +867,6 @@ export default function Dashboard({
<div className='flex-1 overflow-y-auto rounded-t-[6px] bg-[var(--surface-1)] px-[14px] py-[10px]'>
{globalDetails ? (
<LineChart
key={`latency-${expandedWorkflowId || 'all'}-${Object.keys(selectedSegments).length}-${filteredExecutions.length}`}
data={globalDetails.latencies}
label=''
color='var(--c-F59E0B)'

View File

@@ -161,9 +161,9 @@ export function LogDetails({
<ScrollArea className='mt-[20px] h-full w-full overflow-y-auto' ref={scrollAreaRef}>
<div className='flex flex-col gap-[10px] pb-[16px]'>
{/* Timestamp & Workflow Row */}
<div className='flex items-center gap-[16px] px-[1px]'>
<div className='flex min-w-0 items-center gap-[16px] px-[1px]'>
{/* Timestamp Card */}
<div className='flex w-[140px] flex-col gap-[8px]'>
<div className='flex w-[140px] flex-shrink-0 flex-col gap-[8px]'>
<div className='font-medium text-[12px] text-[var(--text-tertiary)]'>
Timestamp
</div>
@@ -179,16 +179,16 @@ export function LogDetails({
{/* Workflow Card */}
{log.workflow && (
<div className='flex flex-col gap-[8px]'>
<div className='flex w-0 min-w-0 flex-1 flex-col gap-[8px]'>
<div className='font-medium text-[12px] text-[var(--text-tertiary)]'>
Workflow
</div>
<div className='flex items-center gap-[8px]'>
<div className='flex min-w-0 items-center gap-[8px]'>
<div
className='h-[10px] w-[10px] flex-shrink-0 rounded-[3px]'
style={{ backgroundColor: log.workflow?.color }}
/>
<span className='font-medium text-[14px] text-[var(--text-secondary)]'>
<span className='min-w-0 flex-1 truncate font-medium text-[14px] text-[var(--text-secondary)]'>
{log.workflow.name}
</span>
</div>
@@ -209,7 +209,7 @@ export function LogDetails({
)}
{/* Details Section */}
<div className='flex flex-col'>
<div className='flex min-w-0 flex-col overflow-hidden'>
{/* Level */}
<div className='flex h-[48px] items-center justify-between border-[var(--border)] border-b p-[8px]'>
<span className='font-medium text-[12px] text-[var(--text-tertiary)]'>
@@ -233,14 +233,30 @@ export function LogDetails({
</div>
{/* Duration */}
<div className='flex h-[48px] items-center justify-between p-[8px]'>
<div
className={`flex h-[48px] items-center justify-between border-b p-[8px] ${log.deploymentVersion ? 'border-[var(--border)]' : 'border-transparent'}`}
>
<span className='font-medium text-[12px] text-[var(--text-tertiary)]'>
Duration
</span>
<span className='font-medium text-[14px] text-[var(--text-secondary)]'>
<span className='font-medium text-[13px] text-[var(--text-secondary)]'>
{log.duration || '—'}
</span>
</div>
{/* Version */}
{log.deploymentVersion && (
<div className='flex h-[48px] items-center gap-[8px] p-[8px]'>
<span className='flex-shrink-0 font-medium text-[12px] text-[var(--text-tertiary)]'>
Version
</span>
<div className='flex w-0 flex-1 justify-end'>
<span className='max-w-full truncate rounded-[6px] bg-[#14291B] px-[9px] py-[2px] font-medium text-[#86EFAC] text-[12px]'>
{log.deploymentVersionName || `v${log.deploymentVersion}`}
</span>
</div>
</div>
)}
</div>
{/* Workflow State */}

View File

@@ -343,17 +343,18 @@ export function LogsToolbar({
</Button>
{/* View mode toggle */}
<div className='flex h-[32px] items-center rounded-[6px] border border-[var(--border)] bg-[var(--surface-elevated)] p-[2px]'>
<div
className='flex h-[32px] cursor-pointer items-center rounded-[6px] border border-[var(--border)] bg-[var(--surface-elevated)] p-[2px]'
onClick={() => onViewModeChange(isDashboardView ? 'logs' : 'dashboard')}
>
<Button
variant={!isDashboardView ? 'active' : 'ghost'}
onClick={() => onViewModeChange('logs')}
className='h-[26px] rounded-[4px] px-[10px]'
>
Logs
</Button>
<Button
variant={isDashboardView ? 'active' : 'ghost'}
onClick={() => onViewModeChange('dashboard')}
className='h-[26px] rounded-[4px] px-[10px]'
>
Dashboard

View File

@@ -1,9 +1,5 @@
import { useCallback, useEffect, useState } from 'react'
import {
MAX_LOG_DETAILS_WIDTH,
MIN_LOG_DETAILS_WIDTH,
useLogDetailsUIStore,
} from '@/stores/logs/store'
import { MIN_LOG_DETAILS_WIDTH, useLogDetailsUIStore } from '@/stores/logs/store'
/**
* Hook for handling log details panel resize via mouse drag.
@@ -29,10 +25,8 @@ export function useLogDetailsResize() {
const handleMouseMove = (e: MouseEvent) => {
// Calculate new width from right edge of window
const newWidth = window.innerWidth - e.clientX
const clampedWidth = Math.max(
MIN_LOG_DETAILS_WIDTH,
Math.min(newWidth, MAX_LOG_DETAILS_WIDTH)
)
const maxWidth = window.innerWidth * 0.5 // 50vw
const clampedWidth = Math.max(MIN_LOG_DETAILS_WIDTH, Math.min(newWidth, maxWidth))
setPanelWidth(clampedWidth)
}

View File

@@ -2,5 +2,5 @@
* Logs layout - applies sidebar padding for all logs routes.
*/
export default function LogsLayout({ children }: { children: React.ReactNode }) {
return <div className='flex h-full flex-1 flex-col pl-60'>{children}</div>
return <div className='flex h-full flex-1 flex-col overflow-hidden pl-60'>{children}</div>
}

View File

@@ -107,21 +107,42 @@ export default function Logs() {
}
}, [debouncedSearchQuery, setStoreSearchQuery])
// Track previous log state for efficient change detection
const prevSelectedLogRef = useRef<WorkflowLog | null>(null)
// Sync selected log with refreshed data from logs list
useEffect(() => {
if (!selectedLog?.id || logs.length === 0) return
const updatedLog = logs.find((l) => l.id === selectedLog.id)
if (updatedLog) {
// Update selectedLog with fresh data from the list
if (!updatedLog) return
const prevLog = prevSelectedLogRef.current
// Check if status-related fields have changed (e.g., running -> done)
const hasStatusChange =
prevLog?.id === updatedLog.id &&
(updatedLog.duration !== prevLog.duration ||
updatedLog.level !== prevLog.level ||
updatedLog.hasPendingPause !== prevLog.hasPendingPause)
// Only update if the log data actually changed
if (updatedLog !== selectedLog) {
setSelectedLog(updatedLog)
// Update index in case position changed
const newIndex = logs.findIndex((l) => l.id === selectedLog.id)
if (newIndex !== selectedLogIndex) {
setSelectedLogIndex(newIndex)
}
prevSelectedLogRef.current = updatedLog
}
}, [logs, selectedLog?.id, selectedLogIndex])
// Update index in case position changed
const newIndex = logs.findIndex((l) => l.id === selectedLog.id)
if (newIndex !== selectedLogIndex) {
setSelectedLogIndex(newIndex)
}
// Refetch log details if status changed to keep details panel in sync
if (hasStatusChange) {
logDetailQuery.refetch()
}
}, [logs, selectedLog?.id, selectedLogIndex, logDetailQuery.refetch])
// Refetch log details during live mode
useEffect(() => {
@@ -143,6 +164,7 @@ export default function Logs() {
// Otherwise, select the log and open the sidebar
setSelectedLog(log)
prevSelectedLogRef.current = log
const index = logs.findIndex((l) => l.id === log.id)
setSelectedLogIndex(index)
setIsSidebarOpen(true)
@@ -154,6 +176,7 @@ export default function Logs() {
setSelectedLogIndex(nextIndex)
const nextLog = logs[nextIndex]
setSelectedLog(nextLog)
prevSelectedLogRef.current = nextLog
}
}, [selectedLogIndex, logs])
@@ -163,6 +186,7 @@ export default function Logs() {
setSelectedLogIndex(prevIndex)
const prevLog = logs[prevIndex]
setSelectedLog(prevLog)
prevSelectedLogRef.current = prevLog
}
}, [selectedLogIndex, logs])
@@ -170,6 +194,7 @@ export default function Logs() {
setIsSidebarOpen(false)
setSelectedLog(null)
setSelectedLogIndex(-1)
prevSelectedLogRef.current = null
}
useEffect(() => {
@@ -332,6 +357,7 @@ export default function Logs() {
e.preventDefault()
setSelectedLogIndex(0)
setSelectedLog(logs[0])
prevSelectedLogRef.current = logs[0]
return
}
@@ -382,215 +408,216 @@ export default function Logs() {
/>
</div>
{/* Dashboard view */}
{isDashboardView && (
<div className='pr-[24px] pb-[24px]'>
<Dashboard isLive={isLive} refreshTrigger={dashboardRefreshTrigger} />
</div>
)}
{/* Dashboard view - always mounted to preserve state and query cache */}
<div
className={cn('flex min-h-0 flex-1 flex-col pr-[24px]', !isDashboardView && 'hidden')}
>
<Dashboard isLive={isLive} refreshTrigger={dashboardRefreshTrigger} />
</div>
{/* Main content area with table - only show in logs view */}
{!isDashboardView && (
<div className='relative mt-[24px] flex min-h-0 flex-1 overflow-hidden rounded-[6px]'>
{/* Table container */}
<div className='relative flex min-h-0 flex-1 flex-col overflow-hidden rounded-[6px] bg-[var(--surface-1)]'>
{/* Table header */}
<div className='flex-shrink-0 rounded-t-[6px] bg-[var(--surface-3)] px-[24px] py-[10px]'>
<div className='flex items-center'>
<span className='w-[8%] min-w-[70px] font-medium text-[12px] text-[var(--text-tertiary)]'>
Date
</span>
<span className='w-[12%] min-w-[90px] font-medium text-[12px] text-[var(--text-tertiary)]'>
Time
</span>
<span className='w-[12%] min-w-[100px] font-medium text-[12px] text-[var(--text-tertiary)]'>
Status
</span>
<span className='w-[22%] min-w-[140px] font-medium text-[12px] text-[var(--text-tertiary)]'>
Workflow
</span>
<span className='w-[12%] min-w-[90px] font-medium text-[12px] text-[var(--text-tertiary)]'>
Cost
</span>
<span className='w-[14%] min-w-[110px] font-medium text-[12px] text-[var(--text-tertiary)]'>
Trigger
</span>
<span className='w-[20%] min-w-[100px] font-medium text-[12px] text-[var(--text-tertiary)]'>
Duration
</span>
</div>
</div>
{/* Table body - scrollable */}
<div
className='min-h-0 flex-1 overflow-y-auto overflow-x-hidden'
ref={scrollContainerRef}
>
{logsQuery.isLoading && !logsQuery.data ? (
<div className='flex h-full items-center justify-center'>
<div className='flex items-center gap-[8px] text-[var(--text-secondary)]'>
<Loader2 className='h-[16px] w-[16px] animate-spin' />
<span className='text-[13px]'>Loading logs...</span>
</div>
</div>
) : logsQuery.isError ? (
<div className='flex h-full items-center justify-center'>
<div className='flex items-center gap-[8px] text-[var(--text-error)]'>
<AlertCircle className='h-[16px] w-[16px]' />
<span className='text-[13px]'>
Error: {logsQuery.error?.message || 'Failed to load logs'}
</span>
</div>
</div>
) : logs.length === 0 ? (
<div className='flex h-full items-center justify-center'>
<div className='flex items-center gap-[8px] text-[var(--text-secondary)]'>
<span className='text-[13px]'>No logs found</span>
</div>
</div>
) : (
<div>
{logs.map((log) => {
const formattedDate = formatDate(log.createdAt)
const isSelected = selectedLog?.id === log.id
const baseLevel = (log.level || 'info').toLowerCase()
const isError = baseLevel === 'error'
const isPending = !isError && log.hasPendingPause === true
const isRunning = !isError && !isPending && log.duration === null
return (
<div
key={log.id}
ref={isSelected ? selectedRowRef : null}
className={cn(
'relative flex h-[44px] cursor-pointer items-center px-[24px] hover:bg-[var(--c-2A2A2A)]',
isSelected && 'bg-[var(--c-2A2A2A)]'
)}
onClick={() => handleLogClick(log)}
>
<div className='flex flex-1 items-center'>
{/* Date */}
<span className='w-[8%] min-w-[70px] font-medium text-[12px] text-[var(--text-primary)]'>
{formattedDate.compactDate}
</span>
{/* Time */}
<span className='w-[12%] min-w-[90px] font-medium text-[12px] text-[var(--text-primary)]'>
{formattedDate.compactTime}
</span>
{/* Status */}
<div className='w-[12%] min-w-[100px]'>
<StatusBadge
status={
isError
? 'error'
: isPending
? 'pending'
: isRunning
? 'running'
: 'info'
}
/>
</div>
{/* Workflow */}
<div className='flex w-[22%] min-w-[140px] items-center gap-[8px] pr-[8px]'>
<div
className='h-[10px] w-[10px] flex-shrink-0 rounded-[3px]'
style={{ backgroundColor: log.workflow?.color }}
/>
<span className='min-w-0 truncate font-medium text-[12px] text-[var(--text-primary)]'>
{log.workflow?.name || 'Unknown'}
</span>
</div>
{/* Cost */}
<span className='w-[12%] min-w-[90px] font-medium text-[12px] text-[var(--text-primary)]'>
{typeof log.cost?.total === 'number'
? `$${log.cost.total.toFixed(4)}`
: '—'}
</span>
{/* Trigger */}
<div className='w-[14%] min-w-[110px]'>
{log.trigger ? (
<TriggerBadge trigger={log.trigger} />
) : (
<span className='font-medium text-[12px] text-[var(--text-primary)]'>
</span>
)}
</div>
{/* Duration */}
<div className='w-[20%] min-w-[100px]'>
<Badge
variant='default'
className='rounded-[6px] px-[9px] py-[2px] text-[12px]'
>
{formatDuration(log.duration) || '—'}
</Badge>
</div>
</div>
{/* Resume Link */}
{isPending &&
log.executionId &&
(log.workflow?.id || log.workflowId) && (
<Link
href={`/resume/${log.workflow?.id || log.workflowId}/${log.executionId}`}
target='_blank'
rel='noopener noreferrer'
className={cn(
buttonVariants({ variant: 'active' }),
'absolute right-[24px] h-[26px] w-[26px] rounded-[6px] p-0'
)}
aria-label='Open resume console'
onClick={(e) => e.stopPropagation()}
>
<ArrowUpRight className='h-[14px] w-[14px]' />
</Link>
)}
</div>
)
})}
{/* Infinite scroll loader */}
{logsQuery.hasNextPage && (
<div className='flex items-center justify-center py-[16px]'>
<div
ref={loaderRef}
className='flex items-center gap-[8px] text-[var(--text-secondary)]'
>
{logsQuery.isFetchingNextPage ? (
<>
<Loader2 className='h-[16px] w-[16px] animate-spin' />
<span className='text-[13px]'>Loading more...</span>
</>
) : (
<span className='text-[13px]'>Scroll to load more</span>
)}
</div>
</div>
)}
</div>
)}
<div
className={cn(
'relative mt-[24px] flex min-h-0 flex-1 flex-col overflow-hidden rounded-[6px]',
isDashboardView && 'hidden'
)}
>
{/* Table container */}
<div className='relative flex min-h-0 flex-1 flex-col overflow-hidden rounded-[6px] bg-[var(--surface-1)]'>
{/* Table header */}
<div className='flex-shrink-0 rounded-t-[6px] bg-[var(--surface-3)] px-[24px] py-[10px]'>
<div className='flex items-center'>
<span className='w-[8%] min-w-[70px] font-medium text-[12px] text-[var(--text-tertiary)]'>
Date
</span>
<span className='w-[12%] min-w-[90px] font-medium text-[12px] text-[var(--text-tertiary)]'>
Time
</span>
<span className='w-[12%] min-w-[100px] font-medium text-[12px] text-[var(--text-tertiary)]'>
Status
</span>
<span className='w-[22%] min-w-[140px] font-medium text-[12px] text-[var(--text-tertiary)]'>
Workflow
</span>
<span className='w-[12%] min-w-[90px] font-medium text-[12px] text-[var(--text-tertiary)]'>
Cost
</span>
<span className='w-[14%] min-w-[110px] font-medium text-[12px] text-[var(--text-tertiary)]'>
Trigger
</span>
<span className='w-[20%] min-w-[100px] font-medium text-[12px] text-[var(--text-tertiary)]'>
Duration
</span>
</div>
</div>
{/* Log Details - rendered inside table container */}
<LogDetails
log={logDetailQuery.data || selectedLog}
isOpen={isSidebarOpen}
onClose={handleCloseSidebar}
onNavigateNext={handleNavigateNext}
onNavigatePrev={handleNavigatePrev}
hasNext={selectedLogIndex < logs.length - 1}
hasPrev={selectedLogIndex > 0}
/>
{/* Table body - scrollable */}
<div
className='min-h-0 flex-1 overflow-y-auto overflow-x-hidden'
ref={scrollContainerRef}
>
{logsQuery.isLoading && !logsQuery.data ? (
<div className='flex h-full items-center justify-center'>
<div className='flex items-center gap-[8px] text-[var(--text-secondary)]'>
<Loader2 className='h-[16px] w-[16px] animate-spin' />
<span className='text-[13px]'>Loading logs...</span>
</div>
</div>
) : logsQuery.isError ? (
<div className='flex h-full items-center justify-center'>
<div className='flex items-center gap-[8px] text-[var(--text-error)]'>
<AlertCircle className='h-[16px] w-[16px]' />
<span className='text-[13px]'>
Error: {logsQuery.error?.message || 'Failed to load logs'}
</span>
</div>
</div>
) : logs.length === 0 ? (
<div className='flex h-full items-center justify-center'>
<div className='flex items-center gap-[8px] text-[var(--text-secondary)]'>
<span className='text-[13px]'>No logs found</span>
</div>
</div>
) : (
<div>
{logs.map((log) => {
const formattedDate = formatDate(log.createdAt)
const isSelected = selectedLog?.id === log.id
const baseLevel = (log.level || 'info').toLowerCase()
const isError = baseLevel === 'error'
const isPending = !isError && log.hasPendingPause === true
const isRunning = !isError && !isPending && log.duration === null
return (
<div
key={log.id}
ref={isSelected ? selectedRowRef : null}
className={cn(
'relative flex h-[44px] cursor-pointer items-center px-[24px] hover:bg-[var(--c-2A2A2A)]',
isSelected && 'bg-[var(--c-2A2A2A)]'
)}
onClick={() => handleLogClick(log)}
>
<div className='flex flex-1 items-center'>
{/* Date */}
<span className='w-[8%] min-w-[70px] font-medium text-[12px] text-[var(--text-primary)]'>
{formattedDate.compactDate}
</span>
{/* Time */}
<span className='w-[12%] min-w-[90px] font-medium text-[12px] text-[var(--text-primary)]'>
{formattedDate.compactTime}
</span>
{/* Status */}
<div className='w-[12%] min-w-[100px]'>
<StatusBadge
status={
isError
? 'error'
: isPending
? 'pending'
: isRunning
? 'running'
: 'info'
}
/>
</div>
{/* Workflow */}
<div className='flex w-[22%] min-w-[140px] items-center gap-[8px] pr-[8px]'>
<div
className='h-[10px] w-[10px] flex-shrink-0 rounded-[3px]'
style={{ backgroundColor: log.workflow?.color }}
/>
<span className='min-w-0 truncate font-medium text-[12px] text-[var(--text-primary)]'>
{log.workflow?.name || 'Unknown'}
</span>
</div>
{/* Cost */}
<span className='w-[12%] min-w-[90px] font-medium text-[12px] text-[var(--text-primary)]'>
{typeof log.cost?.total === 'number'
? `$${log.cost.total.toFixed(4)}`
: '—'}
</span>
{/* Trigger */}
<div className='w-[14%] min-w-[110px]'>
{log.trigger ? (
<TriggerBadge trigger={log.trigger} />
) : (
<span className='font-medium text-[12px] text-[var(--text-primary)]'>
</span>
)}
</div>
{/* Duration */}
<div className='w-[20%] min-w-[100px]'>
<Badge
variant='default'
className='rounded-[6px] px-[9px] py-[2px] text-[12px]'
>
{formatDuration(log.duration) || '—'}
</Badge>
</div>
</div>
{/* Resume Link */}
{isPending && log.executionId && (log.workflow?.id || log.workflowId) && (
<Link
href={`/resume/${log.workflow?.id || log.workflowId}/${log.executionId}`}
target='_blank'
rel='noopener noreferrer'
className={cn(
buttonVariants({ variant: 'active' }),
'absolute right-[24px] h-[26px] w-[26px] rounded-[6px] p-0'
)}
aria-label='Open resume console'
onClick={(e) => e.stopPropagation()}
>
<ArrowUpRight className='h-[14px] w-[14px]' />
</Link>
)}
</div>
)
})}
{/* Infinite scroll loader */}
{logsQuery.hasNextPage && (
<div className='flex items-center justify-center py-[16px]'>
<div
ref={loaderRef}
className='flex items-center gap-[8px] text-[var(--text-secondary)]'
>
{logsQuery.isFetchingNextPage ? (
<>
<Loader2 className='h-[16px] w-[16px] animate-spin' />
<span className='text-[13px]'>Loading more...</span>
</>
) : (
<span className='text-[13px]'>Scroll to load more</span>
)}
</div>
</div>
)}
</div>
)}
</div>
</div>
)}
{/* Log Details - rendered inside table container */}
<LogDetails
log={logDetailQuery.data ? { ...selectedLog, ...logDetailQuery.data } : selectedLog}
isOpen={isSidebarOpen}
onClose={handleCloseSidebar}
onNavigateNext={handleNavigateNext}
onNavigatePrev={handleNavigatePrev}
hasNext={selectedLogIndex < logs.length - 1}
hasPrev={selectedLogIndex > 0}
/>
</div>
</div>
</div>

View File

@@ -206,7 +206,10 @@ function TemplateCardInner({
className
)}
>
<div ref={previewRef} className='relative h-[180px] w-full overflow-hidden rounded-[6px]'>
<div
ref={previewRef}
className='pointer-events-none h-[180px] w-full overflow-hidden rounded-[6px]'
>
{normalizedState && isInView ? (
<WorkflowPreview
workflowState={normalizedState}
@@ -222,8 +225,6 @@ function TemplateCardInner({
) : (
<div className='h-full w-full bg-[#2A2A2A]' />
)}
{/* Transparent overlay to block all pointer events from the preview */}
<div className='pointer-events-none absolute inset-0' />
</div>
<div className='mt-[10px] flex items-center justify-between'>

View File

@@ -2,9 +2,5 @@
* Templates layout - applies sidebar padding for all template routes.
*/
export default function TemplatesLayout({ children }: { children: React.ReactNode }) {
return (
<main className='flex h-full flex-1 flex-col overflow-hidden pl-60'>
<div>{children}</div>
</main>
)
return <main className='flex h-full flex-1 flex-col overflow-hidden pl-60'>{children}</main>
}

View File

@@ -175,8 +175,8 @@ export default function Templates({
<div className='flex flex-1 flex-col overflow-auto px-[24px] pt-[28px] pb-[24px]'>
<div>
<div className='flex items-start gap-[12px]'>
<div className='flex h-[26px] w-[26px] items-center justify-center rounded-[6px] border border-[#1E3A5A] bg-[#0F2A3D]'>
<Layout className='h-[14px] w-[14px] text-[#60A5FA]' />
<div className='flex h-[26px] w-[26px] items-center justify-center rounded-[6px] border border-[#1A5070] bg-[#153347]'>
<Layout className='h-[14px] w-[14px] text-[#33b4ff]' />
</div>
<h1 className='font-medium text-[18px]'>Templates</h1>
</div>

View File

@@ -398,8 +398,8 @@ export const Copilot = forwardRef<CopilotRef, CopilotProps>(({ panelWidth }, ref
className='flex h-full flex-col overflow-hidden'
>
{/* Header */}
<div className='flex flex-shrink-0 items-center justify-between rounded-[4px] bg-[var(--surface-5)] px-[12px] py-[8px]'>
<h2 className='font-medium text-[14px] text-[var(--text-primary)]'>
<div className='flex flex-shrink-0 items-center justify-between gap-[8px] rounded-[4px] bg-[var(--surface-5)] px-[12px] py-[8px]'>
<h2 className='min-w-0 flex-1 truncate font-medium text-[14px] text-[var(--text-primary)]'>
{currentChat?.title || 'New Chat'}
</h2>
<div className='flex items-center gap-[8px]'>

View File

@@ -16,6 +16,7 @@ import {
} from '@/components/emcn'
import { Skeleton, TagInput } from '@/components/ui'
import { useSession } from '@/lib/auth/auth-client'
import { cn } from '@/lib/core/utils/cn'
import { createLogger } from '@/lib/logs/console/logger'
import { WorkflowPreview } from '@/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview'
import {
@@ -87,7 +88,8 @@ export function TemplateDeploy({
const deleteMutation = useDeleteTemplate()
const isSubmitting = createMutation.isPending || updateMutation.isPending
const isFormValid = formData.name.trim().length > 0 && formData.name.length <= 100
const isFormValid =
formData.name.trim().length > 0 && formData.name.length <= 100 && formData.tagline.length <= 200
const updateField = <K extends keyof TemplateFormData>(field: K, value: TemplateFormData[K]) => {
setFormData((prev) => ({ ...prev, [field]: value }))
@@ -302,6 +304,7 @@ export function TemplateDeploy({
value={formData.tagline}
onChange={(e) => updateField('tagline', e.target.value)}
disabled={isSubmitting}
className={cn(formData.tagline.length > 200 && 'border-[var(--text-error)]')}
/>
</div>

View File

@@ -1,4 +1,6 @@
import type React from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { RepeatIcon, SplitIcon } from 'lucide-react'
import { useShallow } from 'zustand/react/shallow'
import {
Popover,
@@ -349,14 +351,24 @@ const getCaretViewportPosition = (
}
/**
* Renders a tag icon with background color
* Renders a tag icon with background color - can use either a React icon component or a letter
*/
const TagIcon: React.FC<{ icon: string; color: string }> = ({ icon, color }) => (
const TagIcon: React.FC<{
icon: string | React.ComponentType<{ className?: string }>
color: string
}> = ({ icon, color }) => (
<div
className='flex h-[14px] w-[14px] flex-shrink-0 items-center justify-center rounded'
style={{ backgroundColor: color }}
style={{ background: color }}
>
<span className='font-bold text-[10px] text-white'>{icon}</span>
{typeof icon === 'string' ? (
<span className='font-bold text-[10px] text-white'>{icon}</span>
) : (
(() => {
const IconComponent = icon
return <IconComponent className='h-[9px] w-[9px] text-white' />
})()
)}
</div>
)
@@ -1385,7 +1397,17 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
blockColor = BLOCK_COLORS.PARALLEL
}
const tagIcon = group.blockName.charAt(0).toUpperCase()
// Use actual block icon if available, otherwise fall back to special icons for loop/parallel or first letter
let tagIcon: string | React.ComponentType<{ className?: string }> = group.blockName
.charAt(0)
.toUpperCase()
if (blockConfig?.icon) {
tagIcon = blockConfig.icon
} else if (group.blockType === 'loop') {
tagIcon = RepeatIcon
} else if (group.blockType === 'parallel') {
tagIcon = SplitIcon
}
return (
<div key={group.blockId}>

View File

@@ -10,6 +10,7 @@ const IconComponent = ({ icon: Icon, className }: { icon: any; className?: strin
interface McpTool {
id: string
name: string
description?: string
serverId: string
serverName: string
icon: React.ComponentType<any>
@@ -76,7 +77,10 @@ export function McpToolsList({
},
isExpanded: true,
usageControl: 'auto',
schema: mcpTool.inputSchema,
schema: {
...mcpTool.inputSchema,
description: mcpTool.description,
},
}
onToolSelect(newTool)

View File

@@ -858,16 +858,22 @@ export function ToolInput({
return
}
const mcpToolsNeedingSchema = selectedTools.filter(
(tool) => tool.type === 'mcp' && !tool.schema && tool.params?.toolName
// Find MCP tools that need schema or are missing description
const mcpToolsNeedingUpdate = selectedTools.filter(
(tool) =>
tool.type === 'mcp' && tool.params?.toolName && (!tool.schema || !tool.schema.description)
)
if (mcpToolsNeedingSchema.length === 0) {
if (mcpToolsNeedingUpdate.length === 0) {
return
}
const updatedTools = selectedTools.map((tool) => {
if (tool.type !== 'mcp' || tool.schema || !tool.params?.toolName) {
if (tool.type !== 'mcp' || !tool.params?.toolName) {
return tool
}
if (tool.schema?.description) {
return tool
}
@@ -877,17 +883,27 @@ export function ToolInput({
if (mcpTool?.inputSchema) {
logger.info(`Backfilling schema for MCP tool: ${tool.params.toolName}`)
return { ...tool, schema: mcpTool.inputSchema }
return {
...tool,
schema: {
...mcpTool.inputSchema,
description: mcpTool.description,
},
}
}
return tool
})
const hasChanges = updatedTools.some((tool, i) => tool.schema && !selectedTools[i].schema)
const hasChanges = updatedTools.some(
(tool, i) =>
(tool.schema && !selectedTools[i].schema) ||
(tool.schema?.description && !selectedTools[i].schema?.description)
)
if (hasChanges) {
hasBackfilledRef.current = true
logger.info(`Backfilled schemas for ${mcpToolsNeedingSchema.length} MCP tool(s)`)
logger.info(`Backfilled schemas for ${mcpToolsNeedingUpdate.length} MCP tool(s)`)
setStoreValue(updatedTools)
}
}, [mcpTools, mcpLoading, selectedTools, isPreview, setStoreValue])

View File

@@ -298,6 +298,8 @@ export function Terminal() {
setOutputPanelWidth,
openOnRun,
setOpenOnRun,
wrapText,
setWrapText,
setHasHydrated,
} = useTerminalStore()
const isExpanded = useTerminalStore((state) => state.terminalHeight > NEAR_MIN_THRESHOLD)
@@ -312,7 +314,6 @@ export function Terminal() {
const exportConsoleCSV = useTerminalConsoleStore((state) => state.exportConsoleCSV)
const [selectedEntry, setSelectedEntry] = useState<ConsoleEntry | null>(null)
const [isToggling, setIsToggling] = useState(false)
const [wrapText, setWrapText] = useState(true)
const [showCopySuccess, setShowCopySuccess] = useState(false)
const [showInput, setShowInput] = useState(false)
const [autoSelectEnabled, setAutoSelectEnabled] = useState(true)
@@ -1528,7 +1529,7 @@ export function Terminal() {
showCheck
onClick={(e) => {
e.stopPropagation()
setWrapText((prev) => !prev)
setWrapText(!wrapText)
}}
>
<span>Wrap text</span>

View File

@@ -1,6 +1,6 @@
import { memo, useCallback } from 'react'
import { ArrowLeftRight, ArrowUpDown, Circle, CircleOff, LogOut } from 'lucide-react'
import { Button, Duplicate, Tooltip, Trash2 } from '@/components/emcn'
import { Button, Copy, Tooltip, Trash2 } from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
@@ -119,7 +119,7 @@ export const ActionBar = memo(
className='hover:!text-[var(--text-inverse)] h-[23px] w-[23px] rounded-[8px] bg-[var(--surface-9)] p-0 text-[#868686] hover:bg-[var(--brand-secondary)]'
disabled={disabled}
>
<Duplicate className='h-[11px] w-[11px]' />
<Copy className='h-[11px] w-[11px]' />
</Button>
</Tooltip.Trigger>
<Tooltip.Content side='top'>{getTooltipMessage('Duplicate Block')}</Tooltip.Content>

View File

@@ -703,12 +703,12 @@ export const WorkflowBlock = memo(function WorkflowBlock({
const colorClasses = isError ? '!bg-red-400 dark:!bg-red-500' : '!bg-[var(--surface-12)]'
const positionClasses = {
left: '!left-[-7px] !h-5 !w-[7px] !rounded-l-[2px] !rounded-r-none hover:!left-[-10px] hover:!w-[10px] hover:!rounded-l-full',
left: '!left-[-8px] !h-5 !w-[7px] !rounded-l-[2px] !rounded-r-none hover:!left-[-11px] hover:!w-[10px] hover:!rounded-l-full',
right:
'!right-[-7px] !h-5 !w-[7px] !rounded-r-[2px] !rounded-l-none hover:!right-[-10px] hover:!w-[10px] hover:!rounded-r-full',
top: '!top-[-7px] !h-[7px] !w-5 !rounded-t-[2px] !rounded-b-none hover:!top-[-10px] hover:!h-[10px] hover:!rounded-t-full',
'!right-[-8px] !h-5 !w-[7px] !rounded-r-[2px] !rounded-l-none hover:!right-[-11px] hover:!w-[10px] hover:!rounded-r-full',
top: '!top-[-8px] !h-[7px] !w-5 !rounded-t-[2px] !rounded-b-none hover:!top-[-11px] hover:!h-[10px] hover:!rounded-t-full',
bottom:
'!bottom-[-7px] !h-[7px] !w-5 !rounded-b-[2px] !rounded-t-none hover:!bottom-[-10px] hover:!h-[10px] hover:!rounded-b-full',
'!bottom-[-8px] !h-[7px] !w-5 !rounded-b-[2px] !rounded-t-none hover:!bottom-[-11px] hover:!h-[10px] hover:!rounded-b-full',
}
return cn(baseClasses, colorClasses, positionClasses[position])

View File

@@ -10,6 +10,7 @@ import { useBrandConfig } from '@/lib/branding/branding'
import { cn } from '@/lib/core/utils/cn'
import { getTriggersForSidebar, hasTriggerCapability } from '@/lib/workflows/triggers/trigger-utils'
import { searchItems } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-utils'
import { SIDEBAR_SCROLL_EVENT } from '@/app/workspace/[workspaceId]/w/components/sidebar/sidebar'
import { getAllBlocks } from '@/blocks'
interface SearchModalProps {
@@ -430,6 +431,12 @@ export function SearchModal({
window.open(item.href, '_blank', 'noopener,noreferrer')
} else {
router.push(item.href)
// Scroll to the workflow in the sidebar after navigation
if (item.type === 'workflow') {
window.dispatchEvent(
new CustomEvent(SIDEBAR_SCROLL_EVENT, { detail: { itemId: item.id } })
)
}
}
}
break

View File

@@ -28,9 +28,13 @@ interface ContextMenuProps {
*/
onRename?: () => void
/**
* Callback when create is clicked (for folders)
* Callback when create workflow is clicked (for folders)
*/
onCreate?: () => void
/**
* Callback when create folder is clicked (for folders)
*/
onCreateFolder?: () => void
/**
* Callback when duplicate is clicked
*/
@@ -54,10 +58,15 @@ interface ContextMenuProps {
*/
showRename?: boolean
/**
* Whether to show the create option (default: false)
* Whether to show the create workflow option (default: false)
* Set to true for folders to create workflows inside
*/
showCreate?: boolean
/**
* Whether to show the create folder option (default: false)
* Set to true for folders to create sub-folders inside
*/
showCreateFolder?: boolean
/**
* Whether to show the duplicate option (default: true)
* Set to false for items that cannot be duplicated
@@ -89,10 +98,15 @@ interface ContextMenuProps {
*/
disableDelete?: boolean
/**
* Whether the create option is disabled (default: false)
* Whether the create workflow option is disabled (default: false)
* Set to true when creation is in progress or user lacks permissions
*/
disableCreate?: boolean
/**
* Whether the create folder option is disabled (default: false)
* Set to true when creation is in progress or user lacks permissions
*/
disableCreateFolder?: boolean
}
/**
@@ -110,12 +124,14 @@ export function ContextMenu({
onOpenInNewTab,
onRename,
onCreate,
onCreateFolder,
onDuplicate,
onExport,
onDelete,
showOpenInNewTab = false,
showRename = true,
showCreate = false,
showCreateFolder = false,
showDuplicate = true,
showExport = false,
disableExport = false,
@@ -123,6 +139,7 @@ export function ContextMenu({
disableDuplicate = false,
disableDelete = false,
disableCreate = false,
disableCreateFolder = false,
}: ContextMenuProps) {
return (
<Popover open={isOpen} onOpenChange={onClose} variant='primary'>
@@ -168,6 +185,17 @@ export function ContextMenu({
Create workflow
</PopoverItem>
)}
{showCreateFolder && onCreateFolder && (
<PopoverItem
disabled={disableCreateFolder}
onClick={() => {
onCreateFolder()
onClose()
}}
>
Create folder
</PopoverItem>
)}
{showDuplicate && onDuplicate && (
<PopoverItem
disabled={disableDuplicate}

View File

@@ -14,8 +14,9 @@ import {
useItemDrag,
useItemRename,
} from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks'
import { SIDEBAR_SCROLL_EVENT } from '@/app/workspace/[workspaceId]/w/components/sidebar/sidebar'
import { useDeleteFolder, useDuplicateFolder } from '@/app/workspace/[workspaceId]/w/hooks'
import { useUpdateFolder } from '@/hooks/queries/folders'
import { useCreateFolder, useUpdateFolder } from '@/hooks/queries/folders'
import { useCreateWorkflow } from '@/hooks/queries/workflows'
import type { FolderTreeNode } from '@/stores/folders/store'
import {
@@ -48,6 +49,7 @@ export function FolderItem({ folder, level, hoverHandlers }: FolderItemProps) {
const workspaceId = params.workspaceId as string
const updateFolderMutation = useUpdateFolder()
const createWorkflowMutation = useCreateWorkflow()
const createFolderMutation = useCreateFolder()
const userPermissions = useUserPermissionsContext()
// Delete modal state
@@ -86,6 +88,10 @@ export function FolderItem({ folder, level, hoverHandlers }: FolderItemProps) {
if (result.id) {
router.push(`/workspace/${workspaceId}/w/${result.id}`)
// Scroll to the newly created workflow
window.dispatchEvent(
new CustomEvent(SIDEBAR_SCROLL_EVENT, { detail: { itemId: result.id } })
)
}
} catch (error) {
// Error already handled by mutation's onError callback
@@ -93,6 +99,28 @@ export function FolderItem({ folder, level, hoverHandlers }: FolderItemProps) {
}
}, [createWorkflowMutation, workspaceId, folder.id, router])
/**
* Handle create sub-folder using React Query mutation.
* Creates a new folder inside the current folder.
*/
const handleCreateFolderInFolder = useCallback(async () => {
try {
const result = await createFolderMutation.mutateAsync({
workspaceId,
name: 'New Folder',
parentId: folder.id,
})
if (result.id) {
// Scroll to the newly created folder
window.dispatchEvent(
new CustomEvent(SIDEBAR_SCROLL_EVENT, { detail: { itemId: result.id } })
)
}
} catch (error) {
logger.error('Failed to create folder:', error)
}
}, [createFolderMutation, workspaceId, folder.id])
// Folder expand hook
const {
isExpanded,
@@ -279,11 +307,14 @@ export function FolderItem({ folder, level, hoverHandlers }: FolderItemProps) {
onClose={closeMenu}
onRename={handleStartEdit}
onCreate={handleCreateWorkflowInFolder}
onCreateFolder={handleCreateFolderInFolder}
onDuplicate={handleDuplicateFolder}
onDelete={() => setIsDeleteModalOpen(true)}
showCreate={true}
showCreateFolder={true}
disableRename={!userPermissions.canEdit}
disableCreate={!userPermissions.canEdit || createWorkflowMutation.isPending}
disableCreateFolder={!userPermissions.canEdit || createFolderMutation.isPending}
disableDuplicate={!userPermissions.canEdit}
disableDelete={!userPermissions.canEdit}
/>

Some files were not shown because too many files have changed in this diff Show More