mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-11 07:58:06 -05:00
Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5ab7f21ae | ||
|
|
02229f0cb2 | ||
|
|
a2451ef3d3 | ||
|
|
6a262f3988 | ||
|
|
5145ce1684 | ||
|
|
e5bd5e4474 | ||
|
|
e9aede087d | ||
|
|
bfb6fffe38 | ||
|
|
ba2377f83b | ||
|
|
f502f984f3 | ||
|
|
74f371cc79 | ||
|
|
4fbec0a43f | ||
|
|
d248557042 | ||
|
|
8215a819e5 | ||
|
|
155f544ce8 | ||
|
|
22f949a41c | ||
|
|
f9aef6ae22 | ||
|
|
46b04a964d | ||
|
|
964b40de45 | ||
|
|
75aca00b6e | ||
|
|
d25084e05d | ||
|
|
445932c1c8 | ||
|
|
cc3f565d5e | ||
|
|
585f5e365b | ||
|
|
0977ed228f | ||
|
|
ed6b9c0c4a | ||
|
|
86bcdcf0d3 | ||
|
|
ac942416de | ||
|
|
195e0e8e3f | ||
|
|
1673ef98ac | ||
|
|
356b473dc3 | ||
|
|
3792bdd252 | ||
|
|
8d15219c12 | ||
|
|
c3adcf315b | ||
|
|
4df5d56ac5 | ||
|
|
7515809df0 | ||
|
|
385e93f4bb | ||
|
|
096af4fdfa | ||
|
|
dc3de95c39 | ||
|
|
79be435918 | ||
|
|
852562cfdd | ||
|
|
eb5d1f3e5b | ||
|
|
4da128d77c | ||
|
|
0c8d05fc98 | ||
|
|
4301342ffb | ||
|
|
56e485d13b | ||
|
|
1ed746bacf | ||
|
|
bf5d0a5573 | ||
|
|
fb148c6203 |
@@ -30,6 +30,18 @@ import { Dashboard, Sidebar } from '@/app/workspace/[workspaceId]/logs/component
|
||||
import { Dashboard } from '@/app/workspace/[workspaceId]/logs/components/dashboard/dashboard'
|
||||
```
|
||||
|
||||
## No Re-exports
|
||||
|
||||
Do not re-export from non-barrel files. Import directly from the source.
|
||||
|
||||
```typescript
|
||||
// ✓ Good - import from where it's declared
|
||||
import { CORE_TRIGGER_TYPES } from '@/stores/logs/filters/types'
|
||||
|
||||
// ✗ Bad - re-exporting in utils.ts then importing from there
|
||||
import { CORE_TRIGGER_TYPES } from '@/app/workspace/.../utils'
|
||||
```
|
||||
|
||||
## Import Order
|
||||
|
||||
1. React/core libraries
|
||||
|
||||
@@ -9,7 +9,7 @@ globs: ["apps/sim/**/*.tsx", "apps/sim/**/*.css"]
|
||||
|
||||
1. **No inline styles** - Use Tailwind classes
|
||||
2. **No duplicate dark classes** - Skip `dark:` when value matches light mode
|
||||
3. **Exact values** - `text-[14px]`, `h-[25px]`
|
||||
3. **Exact values** - `text-[14px]`, `h-[26px]`
|
||||
4. **Transitions** - `transition-colors` for interactive states
|
||||
|
||||
## Conditional Classes
|
||||
|
||||
@@ -52,7 +52,7 @@ import { useWorkflowStore } from '@/stores/workflows/store'
|
||||
import { useWorkflowStore } from '../../../stores/workflows/store'
|
||||
```
|
||||
|
||||
Use barrel exports (`index.ts`) when a folder has 3+ exports.
|
||||
Use barrel exports (`index.ts`) when a folder has 3+ exports. Do not re-export from non-barrel files; import directly from the source.
|
||||
|
||||
### Import Order
|
||||
1. React/core libraries
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -187,7 +187,7 @@
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2025 Sim Studio, Inc.
|
||||
Copyright 2026 Sim Studio, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
2
NOTICE
2
NOTICE
@@ -1,4 +1,4 @@
|
||||
Sim Studio
|
||||
Copyright 2025 Sim Studio
|
||||
Copyright 2026 Sim Studio
|
||||
|
||||
This product includes software developed for the Sim project.
|
||||
File diff suppressed because one or more lines are too long
@@ -58,6 +58,7 @@ import {
|
||||
LinkupIcon,
|
||||
MailchimpIcon,
|
||||
MailgunIcon,
|
||||
MailServerIcon,
|
||||
Mem0Icon,
|
||||
MicrosoftExcelIcon,
|
||||
MicrosoftOneDriveIcon,
|
||||
@@ -165,6 +166,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
huggingface: HuggingFaceIcon,
|
||||
hunter: HunterIOIcon,
|
||||
image_generator: ImageIcon,
|
||||
imap: MailServerIcon,
|
||||
incidentio: IncidentioIcon,
|
||||
intercom: IntercomIcon,
|
||||
jina: JinaAIIcon,
|
||||
|
||||
89
apps/docs/content/docs/de/blocks/webhook.mdx
Normal file
89
apps/docs/content/docs/de/blocks/webhook.mdx
Normal file
@@ -0,0 +1,89 @@
|
||||
---
|
||||
title: Webhook
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Image } from '@/components/ui/image'
|
||||
|
||||
Der Webhook-Block sendet HTTP-POST-Anfragen an externe Webhook-Endpunkte mit automatischen Webhook-Headern und optionaler HMAC-Signierung.
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/blocks/webhook.png"
|
||||
alt="Webhook-Block"
|
||||
width={500}
|
||||
height={400}
|
||||
className="my-6"
|
||||
/>
|
||||
</div>
|
||||
|
||||
## Konfiguration
|
||||
|
||||
### Webhook-URL
|
||||
|
||||
Der Ziel-Endpunkt für Ihre Webhook-Anfrage. Unterstützt sowohl statische URLs als auch dynamische Werte aus anderen Blöcken.
|
||||
|
||||
### Payload
|
||||
|
||||
JSON-Daten, die im Anfrage-Body gesendet werden. Verwenden Sie den KI-Zauberstab, um Payloads zu generieren oder auf Workflow-Variablen zu verweisen:
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "workflow.completed",
|
||||
"data": {
|
||||
"result": "<agent.content>",
|
||||
"timestamp": "<function.result>"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Signierungsgeheimnis
|
||||
|
||||
Optionales Geheimnis für die HMAC-SHA256-Payload-Signierung. Wenn angegeben, wird ein `X-Webhook-Signature`Header hinzugefügt:
|
||||
|
||||
```
|
||||
X-Webhook-Signature: t=1704067200000,v1=5d41402abc4b2a76b9719d911017c592...
|
||||
```
|
||||
|
||||
Um Signaturen zu verifizieren, berechnen Sie `HMAC-SHA256(secret, "${timestamp}.${body}")` und vergleichen Sie mit dem `v1`Wert.
|
||||
|
||||
### Zusätzliche Header
|
||||
|
||||
Benutzerdefinierte Schlüssel-Wert-Header, die in die Anfrage aufgenommen werden. Diese überschreiben alle automatischen Header mit demselben Namen.
|
||||
|
||||
## Automatische Header
|
||||
|
||||
Jede Anfrage enthält automatisch diese Header:
|
||||
|
||||
| Header | Beschreibung |
|
||||
|--------|-------------|
|
||||
| `Content-Type` | `application/json` |
|
||||
| `X-Webhook-Timestamp` | Unix-Zeitstempel in Millisekunden |
|
||||
| `X-Delivery-ID` | Eindeutige UUID für diese Zustellung |
|
||||
| `Idempotency-Key` | Identisch mit `X-Delivery-ID` zur Deduplizierung |
|
||||
|
||||
## Ausgaben
|
||||
|
||||
| Ausgabe | Typ | Beschreibung |
|
||||
|--------|------|-------------|
|
||||
| `data` | json | Antwort-Body vom Endpunkt |
|
||||
| `status` | number | HTTP-Statuscode |
|
||||
| `headers` | object | Antwort-Header |
|
||||
|
||||
## Beispiel-Anwendungsfälle
|
||||
|
||||
**Externe Dienste benachrichtigen** - Workflow-Ergebnisse an Slack, Discord oder benutzerdefinierte Endpunkte senden
|
||||
|
||||
```
|
||||
Agent → Function (format) → Webhook (notify)
|
||||
```
|
||||
|
||||
**Externe Workflows auslösen** - Prozesse in anderen Systemen starten, wenn Bedingungen erfüllt sind
|
||||
|
||||
```
|
||||
Condition (check) → Webhook (trigger) → Response
|
||||
```
|
||||
|
||||
<Callout>
|
||||
Der Webhook-Block verwendet immer POST. Für andere HTTP-Methoden oder mehr Kontrolle verwenden Sie den [API-Block](/blocks/api).
|
||||
</Callout>
|
||||
63
apps/docs/content/docs/de/keyboard-shortcuts/index.mdx
Normal file
63
apps/docs/content/docs/de/keyboard-shortcuts/index.mdx
Normal file
@@ -0,0 +1,63 @@
|
||||
---
|
||||
title: Tastaturkürzel
|
||||
description: Meistern Sie die Workflow-Arbeitsfläche mit Tastaturkürzeln und Maussteuerung
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
|
||||
Beschleunigen Sie die Erstellung Ihrer Workflows mit diesen Tastaturkürzeln und Maussteuerungen. Alle Tastenkombinationen funktionieren, wenn die Arbeitsfläche fokussiert ist (nicht beim Tippen in einem Eingabefeld).
|
||||
|
||||
<Callout type="info">
|
||||
**Mod** bezieht sich auf `Cmd` unter macOS und `Ctrl` unter Windows/Linux.
|
||||
</Callout>
|
||||
|
||||
## Arbeitsflächen-Steuerung
|
||||
|
||||
### Maussteuerung
|
||||
|
||||
| Aktion | Steuerung |
|
||||
|--------|---------|
|
||||
| Arbeitsfläche verschieben | Linksziehen auf leerer Fläche |
|
||||
| Arbeitsfläche verschieben | Scrollen oder Trackpad |
|
||||
| Mehrere Blöcke auswählen | Rechtsziehen zum Aufziehen eines Auswahlrahmens |
|
||||
| Block ziehen | Linksziehen auf Block-Kopfzeile |
|
||||
| Zur Auswahl hinzufügen | `Mod` + Klick auf Blöcke |
|
||||
|
||||
### Workflow-Aktionen
|
||||
|
||||
| Tastenkombination | Aktion |
|
||||
|----------|--------|
|
||||
| `Mod` + `Enter` | Workflow ausführen (oder abbrechen, falls aktiv) |
|
||||
| `Mod` + `Z` | Rückgängig |
|
||||
| `Mod` + `Shift` + `Z` | Wiederholen |
|
||||
| `Mod` + `C` | Ausgewählte Blöcke kopieren |
|
||||
| `Mod` + `V` | Blöcke einfügen |
|
||||
| `Delete` oder `Backspace` | Ausgewählte Blöcke oder Verbindungen löschen |
|
||||
| `Shift` + `L` | Arbeitsfläche automatisch anordnen |
|
||||
|
||||
## Panel-Navigation
|
||||
|
||||
Diese Tastenkombinationen wechseln zwischen den Panel-Tabs auf der rechten Seite der Arbeitsfläche.
|
||||
|
||||
| Tastenkombination | Aktion |
|
||||
|----------|--------|
|
||||
| `C` | Copilot-Tab fokussieren |
|
||||
| `T` | Toolbar-Tab fokussieren |
|
||||
| `E` | Editor-Tab fokussieren |
|
||||
| `Mod` + `F` | Toolbar-Suche fokussieren |
|
||||
|
||||
## Globale Navigation
|
||||
|
||||
| Tastenkombination | Aktion |
|
||||
|----------|--------|
|
||||
| `Mod` + `K` | Suche öffnen |
|
||||
| `Mod` + `Shift` + `A` | Neuen Agenten-Workflow hinzufügen |
|
||||
| `Mod` + `Y` | Zu Vorlagen gehen |
|
||||
| `Mod` + `L` | Zu Logs gehen |
|
||||
|
||||
## Dienstprogramm
|
||||
|
||||
| Tastenkombination | Aktion |
|
||||
|----------|--------|
|
||||
| `Mod` + `D` | Terminal-Konsole leeren |
|
||||
| `Mod` + `E` | Benachrichtigungen löschen |
|
||||
@@ -1,231 +0,0 @@
|
||||
---
|
||||
title: Webhook
|
||||
description: Empfangen Sie Webhooks von jedem Dienst durch Konfiguration eines
|
||||
benutzerdefinierten Webhooks.
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
import { Image } from '@/components/ui/image'
|
||||
|
||||
<BlockInfoCard
|
||||
type="generic_webhook"
|
||||
color="#10B981"
|
||||
/>
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/blocks/webhook.png"
|
||||
alt="Webhook-Block-Konfiguration"
|
||||
width={500}
|
||||
height={400}
|
||||
className="my-6"
|
||||
/>
|
||||
</div>
|
||||
|
||||
## Übersicht
|
||||
|
||||
Der generische Webhook-Block ermöglicht den Empfang von Webhooks von jedem externen Dienst. Dies ist ein flexibler Trigger, der jede JSON-Nutzlast verarbeiten kann und sich daher ideal für die Integration mit Diensten eignet, die keinen dedizierten Sim-Block haben.
|
||||
|
||||
## Grundlegende Verwendung
|
||||
|
||||
### Einfacher Durchleitungsmodus
|
||||
|
||||
Ohne ein definiertes Eingabeformat leitet der Webhook den gesamten Anforderungstext unverändert weiter:
|
||||
|
||||
```bash
|
||||
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Sim-Secret: your-secret" \
|
||||
-d '{
|
||||
"message": "Test webhook trigger",
|
||||
"data": {
|
||||
"key": "value"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
Greifen Sie in nachgelagerten Blöcken auf die Daten zu mit:
|
||||
- `<webhook1.message>` → "Test webhook trigger"
|
||||
- `<webhook1.data.key>` → "value"
|
||||
|
||||
### Strukturiertes Eingabeformat (optional)
|
||||
|
||||
Definieren Sie ein Eingabeschema, um typisierte Felder zu erhalten und erweiterte Funktionen wie Datei-Uploads zu aktivieren:
|
||||
|
||||
**Konfiguration des Eingabeformats:**
|
||||
|
||||
```json
|
||||
[
|
||||
{ "name": "message", "type": "string" },
|
||||
{ "name": "priority", "type": "number" },
|
||||
{ "name": "documents", "type": "files" }
|
||||
]
|
||||
```
|
||||
|
||||
**Webhook-Anfrage:**
|
||||
|
||||
```bash
|
||||
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Sim-Secret: your-secret" \
|
||||
-d '{
|
||||
"message": "Invoice submission",
|
||||
"priority": 1,
|
||||
"documents": [
|
||||
{
|
||||
"type": "file",
|
||||
"data": "data:application/pdf;base64,JVBERi0xLjQK...",
|
||||
"name": "invoice.pdf",
|
||||
"mime": "application/pdf"
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
## Datei-Uploads
|
||||
|
||||
### Unterstützte Dateiformate
|
||||
|
||||
Der Webhook unterstützt zwei Dateieingabeformate:
|
||||
|
||||
#### 1. Base64-kodierte Dateien
|
||||
Zum direkten Hochladen von Dateiinhalten:
|
||||
|
||||
```json
|
||||
{
|
||||
"documents": [
|
||||
{
|
||||
"type": "file",
|
||||
"data": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgA...",
|
||||
"name": "screenshot.png",
|
||||
"mime": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- **Maximale Größe**: 20MB pro Datei
|
||||
- **Format**: Standard-Daten-URL mit Base64-Kodierung
|
||||
- **Speicherung**: Dateien werden in sicheren Ausführungsspeicher hochgeladen
|
||||
|
||||
#### 2. URL-Referenzen
|
||||
Zum Übergeben vorhandener Datei-URLs:
|
||||
|
||||
```json
|
||||
{
|
||||
"documents": [
|
||||
{
|
||||
"type": "url",
|
||||
"data": "https://example.com/files/document.pdf",
|
||||
"name": "document.pdf",
|
||||
"mime": "application/pdf"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Zugriff auf Dateien in nachgelagerten Blöcken
|
||||
|
||||
Dateien werden in `UserFile`Objekte mit den folgenden Eigenschaften verarbeitet:
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: string, // Unique file identifier
|
||||
name: string, // Original filename
|
||||
url: string, // Presigned URL (valid for 5 minutes)
|
||||
size: number, // File size in bytes
|
||||
type: string, // MIME type
|
||||
key: string, // Storage key
|
||||
uploadedAt: string, // ISO timestamp
|
||||
expiresAt: string // ISO timestamp (5 minutes)
|
||||
}
|
||||
```
|
||||
|
||||
**Zugriff in Blöcken:**
|
||||
- `<webhook1.documents[0].url>` → Download-URL
|
||||
- `<webhook1.documents[0].name>` → "invoice.pdf"
|
||||
- `<webhook1.documents[0].size>` → 524288
|
||||
- `<webhook1.documents[0].type>` → "application/pdf"
|
||||
|
||||
### Vollständiges Datei-Upload-Beispiel
|
||||
|
||||
```bash
|
||||
# Create a base64-encoded file
|
||||
echo "Hello World" | base64
|
||||
# SGVsbG8gV29ybGQK
|
||||
|
||||
# Send webhook with file
|
||||
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Sim-Secret: your-secret" \
|
||||
-d '{
|
||||
"subject": "Document for review",
|
||||
"attachments": [
|
||||
{
|
||||
"type": "file",
|
||||
"data": "data:text/plain;base64,SGVsbG8gV29ybGQK",
|
||||
"name": "sample.txt",
|
||||
"mime": "text/plain"
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
## Authentifizierung
|
||||
|
||||
### Authentifizierung konfigurieren (Optional)
|
||||
|
||||
In der Webhook-Konfiguration:
|
||||
1. Aktiviere "Authentifizierung erforderlich"
|
||||
2. Setze einen geheimen Token
|
||||
3. Wähle den Header-Typ:
|
||||
- **Benutzerdefinierter Header**: `X-Sim-Secret: your-token`
|
||||
- **Authorization Bearer**: `Authorization: Bearer your-token`
|
||||
|
||||
### Verwendung der Authentifizierung
|
||||
|
||||
```bash
|
||||
# With custom header
|
||||
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Sim-Secret: your-secret-token" \
|
||||
-d '{"message": "Authenticated request"}'
|
||||
|
||||
# With bearer token
|
||||
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer your-secret-token" \
|
||||
-d '{"message": "Authenticated request"}'
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Eingabeformat für Struktur verwenden**: Definiere ein Eingabeformat, wenn du das erwartete Schema kennst. Dies bietet:
|
||||
- Typvalidierung
|
||||
- Bessere Autovervollständigung im Editor
|
||||
- Datei-Upload-Funktionen
|
||||
|
||||
2. **Authentifizierung**: Aktiviere immer die Authentifizierung für Produktions-Webhooks, um unbefugten Zugriff zu verhindern.
|
||||
|
||||
3. **Dateigrößenbeschränkungen**: Halte Dateien unter 20 MB. Verwende für größere Dateien URL-Referenzen.
|
||||
|
||||
4. **Dateiablauf**: Heruntergeladene Dateien haben URLs mit einer Gültigkeit von 5 Minuten. Verarbeite sie umgehend oder speichere sie an anderer Stelle, wenn sie länger benötigt werden.
|
||||
|
||||
5. **Fehlerbehandlung**: Die Webhook-Verarbeitung erfolgt asynchron. Überprüfe die Ausführungsprotokolle auf Fehler.
|
||||
|
||||
6. **Testen**: Verwende die Schaltfläche "Webhook testen" im Editor, um deine Konfiguration vor der Bereitstellung zu validieren.
|
||||
|
||||
## Anwendungsfälle
|
||||
|
||||
- **Formularübermittlungen**: Empfange Daten von benutzerdefinierten Formularen mit Datei-Uploads
|
||||
- **Drittanbieter-Integrationen**: Verbinde mit Diensten, die Webhooks senden (Stripe, GitHub usw.)
|
||||
- **Dokumentenverarbeitung**: Akzeptiere Dokumente von externen Systemen zur Verarbeitung
|
||||
- **Ereignisbenachrichtigungen**: Empfange Ereignisdaten aus verschiedenen Quellen
|
||||
- **Benutzerdefinierte APIs**: Erstelle benutzerdefinierte API-Endpunkte für deine Anwendungen
|
||||
|
||||
## Hinweise
|
||||
|
||||
- Kategorie: `triggers`
|
||||
- Typ: `generic_webhook`
|
||||
- **Dateiunterstützung**: Verfügbar über Eingabeformat-Konfiguration
|
||||
- **Maximale Dateigröße**: 20 MB pro Datei
|
||||
36
apps/docs/content/docs/de/tools/imap.mdx
Normal file
36
apps/docs/content/docs/de/tools/imap.mdx
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
title: IMAP-E-Mail
|
||||
description: Workflows auslösen, wenn neue E-Mails über IMAP eintreffen
|
||||
(funktioniert mit jedem E-Mail-Anbieter)
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="imap"
|
||||
color="#6366F1"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
Der IMAP-E-Mail-Trigger ermöglicht es Ihren Sim-Workflows, automatisch zu starten, sobald eine neue E-Mail in einem Postfach empfangen wird, das das IMAP-Protokoll unterstützt. Dies funktioniert mit Gmail, Outlook, Yahoo und den meisten anderen E-Mail-Anbietern.
|
||||
|
||||
Mit dem IMAP-Trigger können Sie:
|
||||
|
||||
- **E-Mail-Verarbeitung automatisieren**: Starten Sie Workflows in Echtzeit, wenn neue Nachrichten in Ihrem Posteingang eintreffen.
|
||||
- **Nach Absender, Betreff oder Ordner filtern**: Konfigurieren Sie Ihren Trigger so, dass er nur auf E-Mails reagiert, die bestimmte Bedingungen erfüllen.
|
||||
- **Anhänge extrahieren und verarbeiten**: Laden Sie Dateianhänge automatisch herunter und verwenden Sie sie in Ihren automatisierten Abläufen.
|
||||
- **E-Mail-Inhalte parsen und verwenden**: Greifen Sie auf Betreff, Absender, Empfänger, vollständigen Text und andere Metadaten in nachfolgenden Workflow-Schritten zu.
|
||||
- **Mit jedem E-Mail-Anbieter integrieren**: Funktioniert mit jedem Dienst, der standardmäßigen IMAP-Zugriff bietet, ohne Vendor-Lock-in.
|
||||
- **Bei ungelesenen, markierten oder benutzerdefinierten Kriterien auslösen**: Richten Sie erweiterte Filter für die Arten von E-Mails ein, die Ihre Workflows starten.
|
||||
|
||||
Mit Sim gibt Ihnen die IMAP-Integration die Möglichkeit, E-Mails in eine handlungsfähige Automatisierungsquelle zu verwandeln. Reagieren Sie auf Kundenanfragen, verarbeiten Sie Benachrichtigungen, starten Sie Daten-Pipelines und mehr – direkt aus Ihrem E-Mail-Posteingang, ohne manuelles Eingreifen.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
## Nutzungsanleitung
|
||||
|
||||
Verbinden Sie sich über das IMAP-Protokoll mit jedem E-Mail-Server, um Workflows auszulösen, wenn neue E-Mails empfangen werden. Unterstützt Gmail, Outlook, Yahoo und jeden anderen IMAP-kompatiblen E-Mail-Anbieter.
|
||||
|
||||
## Hinweise
|
||||
|
||||
- Kategorie: `triggers`
|
||||
- Typ: `imap`
|
||||
@@ -273,7 +273,7 @@ Eine neue Organisation in Jira Service Management erstellen
|
||||
| `name` | string | Name der erstellten Organisation |
|
||||
| `success` | boolean | Ob die Operation erfolgreich war |
|
||||
|
||||
### `jsm_add_organization_to_service_desk`
|
||||
### `jsm_add_organization`
|
||||
|
||||
Eine Organisation zu einem Service Desk in Jira Service Management hinzufügen
|
||||
|
||||
|
||||
@@ -123,8 +123,6 @@ Kontostand und Portfoliowert von Kalshi abrufen
|
||||
| --------- | ---- | ----------- |
|
||||
| `balance` | number | Kontostand in Cent |
|
||||
| `portfolioValue` | number | Portfoliowert in Cent |
|
||||
| `balanceDollars` | number | Kontostand in Dollar |
|
||||
| `portfolioValueDollars` | number | Portfoliowert in Dollar |
|
||||
|
||||
### `kalshi_get_positions`
|
||||
|
||||
|
||||
@@ -47,10 +47,11 @@ Daten aus einer Supabase-Tabelle abfragen
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | Ja | Ihre Supabase-Projekt-ID \(z. B. jdrkgepadsdopsntdlom\) |
|
||||
| `projectId` | string | Ja | Ihre Supabase-Projekt-ID \(z.B. jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | Ja | Der Name der abzufragenden Supabase-Tabelle |
|
||||
| `schema` | string | Nein | Datenbankschema für die Abfrage \(Standard: public\). Verwenden Sie dies, um auf Tabellen in anderen Schemas zuzugreifen. |
|
||||
| `filter` | string | Nein | PostgREST-Filter \(z. B. "id=eq.123"\) |
|
||||
| `select` | string | Nein | Zurückzugebende Spalten \(durch Komma getrennt\). Standard ist * \(alle Spalten\) |
|
||||
| `filter` | string | Nein | PostgREST-Filter \(z.B. "id=eq.123"\) |
|
||||
| `orderBy` | string | Nein | Spalte zum Sortieren \(fügen Sie DESC für absteigende Sortierung hinzu\) |
|
||||
| `limit` | number | Nein | Maximale Anzahl der zurückzugebenden Zeilen |
|
||||
| `apiKey` | string | Ja | Ihr Supabase Service Role Secret Key |
|
||||
@@ -91,10 +92,11 @@ Eine einzelne Zeile aus einer Supabase-Tabelle basierend auf Filterkriterien abr
|
||||
|
||||
| Parameter | Typ | Erforderlich | Beschreibung |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | Ja | Ihre Supabase-Projekt-ID \(z. B. jdrkgepadsdopsntdlom\) |
|
||||
| `projectId` | string | Ja | Ihre Supabase-Projekt-ID \(z.B. jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | Ja | Der Name der abzufragenden Supabase-Tabelle |
|
||||
| `schema` | string | Nein | Datenbankschema für die Abfrage \(Standard: public\). Verwenden Sie dies, um auf Tabellen in anderen Schemas zuzugreifen. |
|
||||
| `filter` | string | Ja | PostgREST-Filter zum Auffinden der spezifischen Zeile \(z. B. "id=eq.123"\) |
|
||||
| `select` | string | Nein | Zurückzugebende Spalten \(durch Komma getrennt\). Standard ist * \(alle Spalten\) |
|
||||
| `filter` | string | Ja | PostgREST-Filter zum Finden der spezifischen Zeile \(z.B. "id=eq.123"\) |
|
||||
| `apiKey` | string | Ja | Ihr Supabase Service Role Secret Key |
|
||||
|
||||
#### Ausgabe
|
||||
|
||||
@@ -15,7 +15,7 @@ Der generische Webhook-Block erstellt einen flexiblen Endpunkt, der beliebige Pa
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/blocks/webhook.png"
|
||||
src="/static/blocks/webhook-trigger.png"
|
||||
alt="Generische Webhook-Konfiguration"
|
||||
width={500}
|
||||
height={400}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"router",
|
||||
"variables",
|
||||
"wait",
|
||||
"webhook",
|
||||
"workflow"
|
||||
]
|
||||
}
|
||||
|
||||
87
apps/docs/content/docs/en/blocks/webhook.mdx
Normal file
87
apps/docs/content/docs/en/blocks/webhook.mdx
Normal file
@@ -0,0 +1,87 @@
|
||||
---
|
||||
title: Webhook
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Image } from '@/components/ui/image'
|
||||
|
||||
The Webhook block sends HTTP POST requests to external webhook endpoints with automatic webhook headers and optional HMAC signing.
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/blocks/webhook.png"
|
||||
alt="Webhook Block"
|
||||
width={500}
|
||||
height={400}
|
||||
className="my-6"
|
||||
/>
|
||||
</div>
|
||||
|
||||
## Configuration
|
||||
|
||||
### Webhook URL
|
||||
|
||||
The destination endpoint for your webhook request. Supports both static URLs and dynamic values from other blocks.
|
||||
|
||||
### Payload
|
||||
|
||||
JSON data to send in the request body. Use the AI wand to generate payloads or reference workflow variables:
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "workflow.completed",
|
||||
"data": {
|
||||
"result": "<agent.content>",
|
||||
"timestamp": "<function.result>"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Signing Secret
|
||||
|
||||
Optional secret for HMAC-SHA256 payload signing. When provided, adds an `X-Webhook-Signature` header:
|
||||
|
||||
```
|
||||
X-Webhook-Signature: t=1704067200000,v1=5d41402abc4b2a76b9719d911017c592...
|
||||
```
|
||||
|
||||
To verify signatures, compute `HMAC-SHA256(secret, "${timestamp}.${body}")` and compare with the `v1` value.
|
||||
|
||||
### Additional Headers
|
||||
|
||||
Custom key-value headers to include with the request. These override any automatic headers with the same name.
|
||||
|
||||
## Automatic Headers
|
||||
|
||||
Every request includes these headers automatically:
|
||||
|
||||
| Header | Description |
|
||||
|--------|-------------|
|
||||
| `Content-Type` | `application/json` |
|
||||
| `X-Webhook-Timestamp` | Unix timestamp in milliseconds |
|
||||
| `X-Delivery-ID` | Unique UUID for this delivery |
|
||||
| `Idempotency-Key` | Same as `X-Delivery-ID` for deduplication |
|
||||
|
||||
## Outputs
|
||||
|
||||
| Output | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `data` | json | Response body from the endpoint |
|
||||
| `status` | number | HTTP status code |
|
||||
| `headers` | object | Response headers |
|
||||
|
||||
## Example Use Cases
|
||||
|
||||
**Notify external services** - Send workflow results to Slack, Discord, or custom endpoints
|
||||
```
|
||||
Agent → Function (format) → Webhook (notify)
|
||||
```
|
||||
|
||||
**Trigger external workflows** - Start processes in other systems when conditions are met
|
||||
```
|
||||
Condition (check) → Webhook (trigger) → Response
|
||||
```
|
||||
|
||||
<Callout>
|
||||
The Webhook block always uses POST. For other HTTP methods or more control, use the [API block](/blocks/api).
|
||||
</Callout>
|
||||
64
apps/docs/content/docs/en/keyboard-shortcuts/index.mdx
Normal file
64
apps/docs/content/docs/en/keyboard-shortcuts/index.mdx
Normal file
@@ -0,0 +1,64 @@
|
||||
---
|
||||
title: Keyboard Shortcuts
|
||||
description: Master the workflow canvas with keyboard shortcuts and mouse controls
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
|
||||
Speed up your workflow building with these keyboard shortcuts and mouse controls. All shortcuts work when the canvas is focused (not when typing in an input field).
|
||||
|
||||
<Callout type="info">
|
||||
**Mod** refers to `Cmd` on macOS and `Ctrl` on Windows/Linux.
|
||||
</Callout>
|
||||
|
||||
## Canvas Controls
|
||||
|
||||
### Mouse Controls
|
||||
|
||||
| Action | Control |
|
||||
|--------|---------|
|
||||
| Pan/move canvas | Left-drag on empty space |
|
||||
| Pan/move canvas | Scroll or trackpad |
|
||||
| Select multiple blocks | Right-drag to draw selection box |
|
||||
| Drag block | Left-drag on block header |
|
||||
| Add to selection | `Mod` + click on blocks |
|
||||
|
||||
### Workflow Actions
|
||||
|
||||
| Shortcut | Action |
|
||||
|----------|--------|
|
||||
| `Mod` + `Enter` | Run workflow (or cancel if running) |
|
||||
| `Mod` + `Z` | Undo |
|
||||
| `Mod` + `Shift` + `Z` | Redo |
|
||||
| `Mod` + `C` | Copy selected blocks |
|
||||
| `Mod` + `V` | Paste blocks |
|
||||
| `Delete` or `Backspace` | Delete selected blocks or edges |
|
||||
| `Shift` + `L` | Auto-layout canvas |
|
||||
|
||||
## Panel Navigation
|
||||
|
||||
These shortcuts switch between panel tabs on the right side of the canvas.
|
||||
|
||||
| Shortcut | Action |
|
||||
|----------|--------|
|
||||
| `C` | Focus Copilot tab |
|
||||
| `T` | Focus Toolbar tab |
|
||||
| `E` | Focus Editor tab |
|
||||
| `Mod` + `F` | Focus Toolbar search |
|
||||
|
||||
## Global Navigation
|
||||
|
||||
| Shortcut | Action |
|
||||
|----------|--------|
|
||||
| `Mod` + `K` | Open search |
|
||||
| `Mod` + `Shift` + `A` | Add new agent workflow |
|
||||
| `Mod` + `Y` | Go to templates |
|
||||
| `Mod` + `L` | Go to logs |
|
||||
|
||||
## Utility
|
||||
|
||||
| Shortcut | Action |
|
||||
|----------|--------|
|
||||
| `Mod` + `D` | Clear terminal console |
|
||||
| `Mod` + `E` | Clear notifications |
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
"execution",
|
||||
"permissions",
|
||||
"sdks",
|
||||
"self-hosting"
|
||||
"self-hosting",
|
||||
"./keyboard-shortcuts/index"
|
||||
],
|
||||
"defaultOpen": false
|
||||
}
|
||||
|
||||
40
apps/docs/content/docs/en/tools/imap.mdx
Normal file
40
apps/docs/content/docs/en/tools/imap.mdx
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
title: IMAP Email
|
||||
description: Trigger workflows when new emails arrive via IMAP (works with any email provider)
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="imap"
|
||||
color="#6366F1"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
The IMAP Email trigger allows your Sim workflows to start automatically whenever a new email is received in any mailbox that supports the IMAP protocol. This works with Gmail, Outlook, Yahoo, and most other email providers.
|
||||
|
||||
With the IMAP trigger, you can:
|
||||
|
||||
- **Automate email processing**: Start workflows in real time when new messages arrive in your inbox.
|
||||
- **Filter by sender, subject, or folder**: Configure your trigger to react only to emails that match certain conditions.
|
||||
- **Extract and process attachments**: Automatically download and use file attachments in your automated flows.
|
||||
- **Parse and use email content**: Access the subject, sender, recipients, full body, and other metadata in downstream workflow steps.
|
||||
- **Integrate with any email provider**: Works with any service that provides standard IMAP access, without vendor lock-in.
|
||||
- **Trigger on unread, flagged, or custom criteria**: Set up advanced filters for the kinds of emails that start your workflows.
|
||||
|
||||
With Sim, the IMAP integration gives you the power to turn email into an actionable source of automation. Respond to customer inquiries, process notifications, kick off data pipelines, and more—directly from your email inbox, with no manual intervention.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Connect to any email server via IMAP protocol to trigger workflows when new emails are received. Supports Gmail, Outlook, Yahoo, and any other IMAP-compatible email provider.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Notes
|
||||
|
||||
- Category: `triggers`
|
||||
- Type: `imap`
|
||||
@@ -275,7 +275,7 @@ Create a new organization in Jira Service Management
|
||||
| `name` | string | Name of the created organization |
|
||||
| `success` | boolean | Whether the operation succeeded |
|
||||
|
||||
### `jsm_add_organization_to_service_desk`
|
||||
### `jsm_add_organization`
|
||||
|
||||
Add an organization to a service desk in Jira Service Management
|
||||
|
||||
|
||||
@@ -126,8 +126,6 @@ Retrieve your account balance and portfolio value from Kalshi
|
||||
| --------- | ---- | ----------- |
|
||||
| `balance` | number | Account balance in cents |
|
||||
| `portfolioValue` | number | Portfolio value in cents |
|
||||
| `balanceDollars` | number | Account balance in dollars |
|
||||
| `portfolioValueDollars` | number | Portfolio value in dollars |
|
||||
|
||||
### `kalshi_get_positions`
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
"huggingface",
|
||||
"hunter",
|
||||
"image_generator",
|
||||
"imap",
|
||||
"incidentio",
|
||||
"intercom",
|
||||
"jina",
|
||||
|
||||
@@ -53,6 +53,7 @@ Query data from a Supabase table
|
||||
| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | Yes | The name of the Supabase table to query |
|
||||
| `schema` | string | No | Database schema to query from \(default: public\). Use this to access tables in other schemas. |
|
||||
| `select` | string | No | Columns to return \(comma-separated\). Defaults to * \(all columns\) |
|
||||
| `filter` | string | No | PostgREST filter \(e.g., "id=eq.123"\) |
|
||||
| `orderBy` | string | No | Column to order by \(add DESC for descending\) |
|
||||
| `limit` | number | No | Maximum number of rows to return |
|
||||
@@ -97,6 +98,7 @@ Get a single row from a Supabase table based on filter criteria
|
||||
| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | Yes | The name of the Supabase table to query |
|
||||
| `schema` | string | No | Database schema to query from \(default: public\). Use this to access tables in other schemas. |
|
||||
| `select` | string | No | Columns to return \(comma-separated\). Defaults to * \(all columns\) |
|
||||
| `filter` | string | Yes | PostgREST filter to find the specific row \(e.g., "id=eq.123"\) |
|
||||
| `apiKey` | string | Yes | Your Supabase service role secret key |
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ The Generic Webhook block creates a flexible endpoint that can receive any paylo
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/blocks/webhook.png"
|
||||
src="/static/blocks/webhook-trigger.png"
|
||||
alt="Generic Webhook Configuration"
|
||||
width={500}
|
||||
height={400}
|
||||
|
||||
89
apps/docs/content/docs/es/blocks/webhook.mdx
Normal file
89
apps/docs/content/docs/es/blocks/webhook.mdx
Normal file
@@ -0,0 +1,89 @@
|
||||
---
|
||||
title: Webhook
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Image } from '@/components/ui/image'
|
||||
|
||||
El bloque Webhook envía solicitudes HTTP POST a endpoints de webhook externos con encabezados de webhook automáticos y firma HMAC opcional.
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/blocks/webhook.png"
|
||||
alt="Bloque Webhook"
|
||||
width={500}
|
||||
height={400}
|
||||
className="my-6"
|
||||
/>
|
||||
</div>
|
||||
|
||||
## Configuración
|
||||
|
||||
### URL del webhook
|
||||
|
||||
El endpoint de destino para tu solicitud de webhook. Admite tanto URL estáticas como valores dinámicos de otros bloques.
|
||||
|
||||
### Carga útil
|
||||
|
||||
Datos JSON para enviar en el cuerpo de la solicitud. Usa la varita de IA para generar cargas útiles o referenciar variables del flujo de trabajo:
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "workflow.completed",
|
||||
"data": {
|
||||
"result": "<agent.content>",
|
||||
"timestamp": "<function.result>"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Secreto de firma
|
||||
|
||||
Secreto opcional para la firma HMAC-SHA256 de la carga útil. Cuando se proporciona, añade un encabezado `X-Webhook-Signature`:
|
||||
|
||||
```
|
||||
X-Webhook-Signature: t=1704067200000,v1=5d41402abc4b2a76b9719d911017c592...
|
||||
```
|
||||
|
||||
Para verificar las firmas, calcula `HMAC-SHA256(secret, "${timestamp}.${body}")` y compara con el valor `v1`.
|
||||
|
||||
### Encabezados adicionales
|
||||
|
||||
Encabezados personalizados de clave-valor para incluir con la solicitud. Estos sobrescriben cualquier encabezado automático con el mismo nombre.
|
||||
|
||||
## Encabezados automáticos
|
||||
|
||||
Cada solicitud incluye estos encabezados automáticamente:
|
||||
|
||||
| Encabezado | Descripción |
|
||||
|--------|-------------|
|
||||
| `Content-Type` | `application/json` |
|
||||
| `X-Webhook-Timestamp` | Marca de tiempo Unix en milisegundos |
|
||||
| `X-Delivery-ID` | UUID único para esta entrega |
|
||||
| `Idempotency-Key` | Igual que `X-Delivery-ID` para deduplicación |
|
||||
|
||||
## Salidas
|
||||
|
||||
| Salida | Tipo | Descripción |
|
||||
|--------|------|-------------|
|
||||
| `data` | json | Cuerpo de respuesta del endpoint |
|
||||
| `status` | number | Código de estado HTTP |
|
||||
| `headers` | object | Encabezados de respuesta |
|
||||
|
||||
## Ejemplos de casos de uso
|
||||
|
||||
**Notificar servicios externos** - Envía resultados del flujo de trabajo a Slack, Discord o endpoints personalizados
|
||||
|
||||
```
|
||||
Agent → Function (format) → Webhook (notify)
|
||||
```
|
||||
|
||||
**Activar flujos de trabajo externos** - Inicia procesos en otros sistemas cuando se cumplan las condiciones
|
||||
|
||||
```
|
||||
Condition (check) → Webhook (trigger) → Response
|
||||
```
|
||||
|
||||
<Callout>
|
||||
El bloque Webhook siempre usa POST. Para otros métodos HTTP o más control, usa el [bloque API](/blocks/api).
|
||||
</Callout>
|
||||
64
apps/docs/content/docs/es/keyboard-shortcuts/index.mdx
Normal file
64
apps/docs/content/docs/es/keyboard-shortcuts/index.mdx
Normal file
@@ -0,0 +1,64 @@
|
||||
---
|
||||
title: Atajos de teclado
|
||||
description: Domina el lienzo de flujo de trabajo con atajos de teclado y
|
||||
controles del ratón
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
|
||||
Acelera la creación de tus flujos de trabajo con estos atajos de teclado y controles del ratón. Todos los atajos funcionan cuando el lienzo está enfocado (no cuando estás escribiendo en un campo de entrada).
|
||||
|
||||
<Callout type="info">
|
||||
**Mod** se refiere a `Cmd` en macOS y `Ctrl` en Windows/Linux.
|
||||
</Callout>
|
||||
|
||||
## Controles del lienzo
|
||||
|
||||
### Controles del ratón
|
||||
|
||||
| Acción | Control |
|
||||
|--------|---------|
|
||||
| Desplazar/mover lienzo | Arrastrar con botón izquierdo en espacio vacío |
|
||||
| Desplazar/mover lienzo | Desplazamiento o trackpad |
|
||||
| Seleccionar múltiples bloques | Arrastrar con botón derecho para dibujar cuadro de selección |
|
||||
| Arrastrar bloque | Arrastrar con botón izquierdo en encabezado del bloque |
|
||||
| Añadir a la selección | `Mod` + clic en bloques |
|
||||
|
||||
### Acciones de flujo de trabajo
|
||||
|
||||
| Atajo | Acción |
|
||||
|----------|--------|
|
||||
| `Mod` + `Enter` | Ejecutar flujo de trabajo (o cancelar si está en ejecución) |
|
||||
| `Mod` + `Z` | Deshacer |
|
||||
| `Mod` + `Shift` + `Z` | Rehacer |
|
||||
| `Mod` + `C` | Copiar bloques seleccionados |
|
||||
| `Mod` + `V` | Pegar bloques |
|
||||
| `Delete` o `Backspace` | Eliminar bloques o conexiones seleccionados |
|
||||
| `Shift` + `L` | Diseño automático del lienzo |
|
||||
|
||||
## Navegación de paneles
|
||||
|
||||
Estos atajos cambian entre las pestañas del panel en el lado derecho del lienzo.
|
||||
|
||||
| Atajo | Acción |
|
||||
|----------|--------|
|
||||
| `C` | Enfocar pestaña Copilot |
|
||||
| `T` | Enfocar pestaña Barra de herramientas |
|
||||
| `E` | Enfocar pestaña Editor |
|
||||
| `Mod` + `F` | Enfocar búsqueda de Barra de herramientas |
|
||||
|
||||
## Navegación global
|
||||
|
||||
| Atajo | Acción |
|
||||
|----------|--------|
|
||||
| `Mod` + `K` | Abrir búsqueda |
|
||||
| `Mod` + `Shift` + `A` | Añadir nuevo flujo de trabajo de agente |
|
||||
| `Mod` + `Y` | Ir a plantillas |
|
||||
| `Mod` + `L` | Ir a registros |
|
||||
|
||||
## Utilidad
|
||||
|
||||
| Atajo | Acción |
|
||||
|----------|--------|
|
||||
| `Mod` + `D` | Limpiar consola del terminal |
|
||||
| `Mod` + `E` | Limpiar notificaciones |
|
||||
@@ -1,230 +0,0 @@
|
||||
---
|
||||
title: Webhook
|
||||
description: Recibe webhooks de cualquier servicio configurando un webhook personalizado.
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
import { Image } from '@/components/ui/image'
|
||||
|
||||
<BlockInfoCard
|
||||
type="generic_webhook"
|
||||
color="#10B981"
|
||||
/>
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/blocks/webhook.png"
|
||||
alt="Configuración del bloque Webhook"
|
||||
width={500}
|
||||
height={400}
|
||||
className="my-6"
|
||||
/>
|
||||
</div>
|
||||
|
||||
## Descripción general
|
||||
|
||||
El bloque Webhook genérico te permite recibir webhooks desde cualquier servicio externo. Este es un disparador flexible que puede manejar cualquier carga útil JSON, lo que lo hace ideal para integrarse con servicios que no tienen un bloque Sim dedicado.
|
||||
|
||||
## Uso básico
|
||||
|
||||
### Modo de paso simple
|
||||
|
||||
Sin definir un formato de entrada, el webhook transmite todo el cuerpo de la solicitud tal como está:
|
||||
|
||||
```bash
|
||||
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Sim-Secret: your-secret" \
|
||||
-d '{
|
||||
"message": "Test webhook trigger",
|
||||
"data": {
|
||||
"key": "value"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
Accede a los datos en bloques posteriores usando:
|
||||
- `<webhook1.message>` → "Test webhook trigger"
|
||||
- `<webhook1.data.key>` → "value"
|
||||
|
||||
### Formato de entrada estructurado (opcional)
|
||||
|
||||
Define un esquema de entrada para obtener campos tipados y habilitar funciones avanzadas como cargas de archivos:
|
||||
|
||||
**Configuración del formato de entrada:**
|
||||
|
||||
```json
|
||||
[
|
||||
{ "name": "message", "type": "string" },
|
||||
{ "name": "priority", "type": "number" },
|
||||
{ "name": "documents", "type": "files" }
|
||||
]
|
||||
```
|
||||
|
||||
**Solicitud de webhook:**
|
||||
|
||||
```bash
|
||||
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Sim-Secret: your-secret" \
|
||||
-d '{
|
||||
"message": "Invoice submission",
|
||||
"priority": 1,
|
||||
"documents": [
|
||||
{
|
||||
"type": "file",
|
||||
"data": "data:application/pdf;base64,JVBERi0xLjQK...",
|
||||
"name": "invoice.pdf",
|
||||
"mime": "application/pdf"
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
## Cargas de archivos
|
||||
|
||||
### Formatos de archivo compatibles
|
||||
|
||||
El webhook admite dos formatos de entrada de archivos:
|
||||
|
||||
#### 1. Archivos codificados en Base64
|
||||
Para cargar contenido de archivos directamente:
|
||||
|
||||
```json
|
||||
{
|
||||
"documents": [
|
||||
{
|
||||
"type": "file",
|
||||
"data": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgA...",
|
||||
"name": "screenshot.png",
|
||||
"mime": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- **Tamaño máximo**: 20MB por archivo
|
||||
- **Formato**: URL de datos estándar con codificación base64
|
||||
- **Almacenamiento**: Los archivos se cargan en almacenamiento seguro de ejecución
|
||||
|
||||
#### 2. Referencias URL
|
||||
Para pasar URLs de archivos existentes:
|
||||
|
||||
```json
|
||||
{
|
||||
"documents": [
|
||||
{
|
||||
"type": "url",
|
||||
"data": "https://example.com/files/document.pdf",
|
||||
"name": "document.pdf",
|
||||
"mime": "application/pdf"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Acceso a archivos en bloques posteriores
|
||||
|
||||
Los archivos se procesan en objetos `UserFile` con las siguientes propiedades:
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: string, // Unique file identifier
|
||||
name: string, // Original filename
|
||||
url: string, // Presigned URL (valid for 5 minutes)
|
||||
size: number, // File size in bytes
|
||||
type: string, // MIME type
|
||||
key: string, // Storage key
|
||||
uploadedAt: string, // ISO timestamp
|
||||
expiresAt: string // ISO timestamp (5 minutes)
|
||||
}
|
||||
```
|
||||
|
||||
**Acceso en bloques:**
|
||||
- `<webhook1.documents[0].url>` → URL de descarga
|
||||
- `<webhook1.documents[0].name>` → "invoice.pdf"
|
||||
- `<webhook1.documents[0].size>` → 524288
|
||||
- `<webhook1.documents[0].type>` → "application/pdf"
|
||||
|
||||
### Ejemplo completo de carga de archivos
|
||||
|
||||
```bash
|
||||
# Create a base64-encoded file
|
||||
echo "Hello World" | base64
|
||||
# SGVsbG8gV29ybGQK
|
||||
|
||||
# Send webhook with file
|
||||
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Sim-Secret: your-secret" \
|
||||
-d '{
|
||||
"subject": "Document for review",
|
||||
"attachments": [
|
||||
{
|
||||
"type": "file",
|
||||
"data": "data:text/plain;base64,SGVsbG8gV29ybGQK",
|
||||
"name": "sample.txt",
|
||||
"mime": "text/plain"
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
## Autenticación
|
||||
|
||||
### Configurar autenticación (opcional)
|
||||
|
||||
En la configuración del webhook:
|
||||
1. Habilitar "Requerir autenticación"
|
||||
2. Establecer un token secreto
|
||||
3. Elegir tipo de encabezado:
|
||||
- **Encabezado personalizado**: `X-Sim-Secret: your-token`
|
||||
- **Autorización Bearer**: `Authorization: Bearer your-token`
|
||||
|
||||
### Uso de la autenticación
|
||||
|
||||
```bash
|
||||
# With custom header
|
||||
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Sim-Secret: your-secret-token" \
|
||||
-d '{"message": "Authenticated request"}'
|
||||
|
||||
# With bearer token
|
||||
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer your-secret-token" \
|
||||
-d '{"message": "Authenticated request"}'
|
||||
```
|
||||
|
||||
## Mejores prácticas
|
||||
|
||||
1. **Usar formato de entrada para estructura**: Define un formato de entrada cuando conozcas el esquema esperado. Esto proporciona:
|
||||
- Validación de tipo
|
||||
- Mejor autocompletado en el editor
|
||||
- Capacidades de carga de archivos
|
||||
|
||||
2. **Autenticación**: Habilita siempre la autenticación para webhooks en producción para prevenir accesos no autorizados.
|
||||
|
||||
3. **Límites de tamaño de archivo**: Mantén los archivos por debajo de 20MB. Para archivos más grandes, usa referencias URL en su lugar.
|
||||
|
||||
4. **Caducidad de archivos**: Los archivos descargados tienen URLs con caducidad de 5 minutos. Procésalos rápidamente o almacénalos en otro lugar si los necesitas por más tiempo.
|
||||
|
||||
5. **Manejo de errores**: El procesamiento de webhooks es asíncrono. Revisa los registros de ejecución para detectar errores.
|
||||
|
||||
6. **Pruebas**: Usa el botón "Probar webhook" en el editor para validar tu configuración antes de implementarla.
|
||||
|
||||
## Casos de uso
|
||||
|
||||
- **Envíos de formularios**: Recibe datos de formularios personalizados con cargas de archivos
|
||||
- **Integraciones con terceros**: Conéctate con servicios que envían webhooks (Stripe, GitHub, etc.)
|
||||
- **Procesamiento de documentos**: Acepta documentos de sistemas externos para procesarlos
|
||||
- **Notificaciones de eventos**: Recibe datos de eventos de varias fuentes
|
||||
- **APIs personalizadas**: Construye endpoints de API personalizados para tus aplicaciones
|
||||
|
||||
## Notas
|
||||
|
||||
- Categoría: `triggers`
|
||||
- Tipo: `generic_webhook`
|
||||
- **Soporte de archivos**: disponible a través de la configuración del formato de entrada
|
||||
- **Tamaño máximo de archivo**: 20MB por archivo
|
||||
36
apps/docs/content/docs/es/tools/imap.mdx
Normal file
36
apps/docs/content/docs/es/tools/imap.mdx
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
title: Correo electrónico IMAP
|
||||
description: Activa flujos de trabajo cuando lleguen nuevos correos electrónicos
|
||||
a través de IMAP (funciona con cualquier proveedor de correo electrónico)
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="imap"
|
||||
color="#6366F1"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
El activador de correo electrónico IMAP permite que tus flujos de trabajo de Sim se inicien automáticamente cada vez que se reciba un nuevo correo electrónico en cualquier buzón que admita el protocolo IMAP. Esto funciona con Gmail, Outlook, Yahoo y la mayoría de los demás proveedores de correo electrónico.
|
||||
|
||||
Con el activador IMAP, puedes:
|
||||
|
||||
- **Automatizar el procesamiento de correos electrónicos**: inicia flujos de trabajo en tiempo real cuando lleguen nuevos mensajes a tu bandeja de entrada.
|
||||
- **Filtrar por remitente, asunto o carpeta**: configura tu activador para que reaccione solo a los correos electrónicos que cumplan ciertas condiciones.
|
||||
- **Extraer y procesar archivos adjuntos**: descarga y utiliza automáticamente archivos adjuntos en tus flujos automatizados.
|
||||
- **Analizar y utilizar el contenido del correo electrónico**: accede al asunto, remitente, destinatarios, cuerpo completo y otros metadatos en los pasos posteriores del flujo de trabajo.
|
||||
- **Integrar con cualquier proveedor de correo electrónico**: funciona con cualquier servicio que proporcione acceso IMAP estándar, sin dependencia de proveedores.
|
||||
- **Activar según criterios de no leído, marcado o personalizados**: configura filtros avanzados para los tipos de correos electrónicos que inician tus flujos de trabajo.
|
||||
|
||||
Con Sim, la integración IMAP te brinda el poder de convertir el correo electrónico en una fuente de automatización procesable. Responde a consultas de clientes, procesa notificaciones, inicia pipelines de datos y más, directamente desde tu bandeja de entrada de correo electrónico, sin intervención manual.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
## Instrucciones de uso
|
||||
|
||||
Conéctate a cualquier servidor de correo electrónico a través del protocolo IMAP para activar flujos de trabajo cuando se reciban nuevos correos electrónicos. Compatible con Gmail, Outlook, Yahoo y cualquier otro proveedor de correo electrónico compatible con IMAP.
|
||||
|
||||
## Notas
|
||||
|
||||
- Categoría: `triggers`
|
||||
- Tipo: `imap`
|
||||
@@ -273,7 +273,7 @@ Crear una nueva organización en Jira Service Management
|
||||
| `name` | string | Nombre de la organización creada |
|
||||
| `success` | boolean | Si la operación tuvo éxito |
|
||||
|
||||
### `jsm_add_organization_to_service_desk`
|
||||
### `jsm_add_organization`
|
||||
|
||||
Añadir una organización a un service desk en Jira Service Management
|
||||
|
||||
|
||||
@@ -122,9 +122,7 @@ Recuperar el saldo de tu cuenta y el valor de la cartera desde Kalshi
|
||||
| Parámetro | Tipo | Descripción |
|
||||
| --------- | ---- | ----------- |
|
||||
| `balance` | number | Saldo de la cuenta en centavos |
|
||||
| `portfolioValue` | number | Valor de la cartera en centavos |
|
||||
| `balanceDollars` | number | Saldo de la cuenta en dólares |
|
||||
| `portfolioValueDollars` | number | Valor de la cartera en dólares |
|
||||
| `portfolioValue` | number | Valor del portafolio en centavos |
|
||||
|
||||
### `kalshi_get_positions`
|
||||
|
||||
|
||||
@@ -46,12 +46,13 @@ Consultar datos de una tabla de Supabase
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Obligatorio | Descripción |
|
||||
| --------- | ---- | ----------- | ----------- |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | Sí | ID de tu proyecto Supabase \(p. ej., jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | Sí | Nombre de la tabla Supabase a consultar |
|
||||
| `schema` | string | No | Esquema de base de datos desde donde consultar \(predeterminado: public\). Usa esto para acceder a tablas en otros esquemas. |
|
||||
| `schema` | string | No | Esquema de base de datos desde el que consultar \(predeterminado: public\). Usa esto para acceder a tablas en otros esquemas. |
|
||||
| `select` | string | No | Columnas a devolver \(separadas por comas\). Predeterminado: * \(todas las columnas\) |
|
||||
| `filter` | string | No | Filtro PostgREST \(p. ej., "id=eq.123"\) |
|
||||
| `orderBy` | string | No | Columna para ordenar \(añade DESC para descendente\) |
|
||||
| `orderBy` | string | No | Columna por la que ordenar \(añade DESC para orden descendente\) |
|
||||
| `limit` | number | No | Número máximo de filas a devolver |
|
||||
| `apiKey` | string | Sí | Tu clave secreta de rol de servicio de Supabase |
|
||||
|
||||
@@ -90,10 +91,11 @@ Obtener una sola fila de una tabla de Supabase basada en criterios de filtro
|
||||
#### Entrada
|
||||
|
||||
| Parámetro | Tipo | Obligatorio | Descripción |
|
||||
| --------- | ---- | ----------- | ----------- |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | Sí | ID de tu proyecto Supabase \(p. ej., jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | Sí | Nombre de la tabla Supabase a consultar |
|
||||
| `schema` | string | No | Esquema de base de datos desde donde consultar \(predeterminado: public\). Usa esto para acceder a tablas en otros esquemas. |
|
||||
| `schema` | string | No | Esquema de base de datos desde el que consultar \(predeterminado: public\). Usa esto para acceder a tablas en otros esquemas. |
|
||||
| `select` | string | No | Columnas a devolver \(separadas por comas\). Predeterminado: * \(todas las columnas\) |
|
||||
| `filter` | string | Sí | Filtro PostgREST para encontrar la fila específica \(p. ej., "id=eq.123"\) |
|
||||
| `apiKey` | string | Sí | Tu clave secreta de rol de servicio de Supabase |
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@ El bloque de webhook genérico crea un punto de conexión flexible que puede rec
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/blocks/webhook.png"
|
||||
alt="Configuración genérica de webhook"
|
||||
src="/static/blocks/webhook-trigger.png"
|
||||
alt="Configuración de webhook genérico"
|
||||
width={500}
|
||||
height={400}
|
||||
className="my-6"
|
||||
|
||||
89
apps/docs/content/docs/fr/blocks/webhook.mdx
Normal file
89
apps/docs/content/docs/fr/blocks/webhook.mdx
Normal file
@@ -0,0 +1,89 @@
|
||||
---
|
||||
title: Webhook
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Image } from '@/components/ui/image'
|
||||
|
||||
Le bloc Webhook envoie des requêtes HTTP POST vers des points de terminaison webhook externes avec des en-têtes webhook automatiques et une signature HMAC optionnelle.
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/blocks/webhook.png"
|
||||
alt="Bloc Webhook"
|
||||
width={500}
|
||||
height={400}
|
||||
className="my-6"
|
||||
/>
|
||||
</div>
|
||||
|
||||
## Configuration
|
||||
|
||||
### URL du webhook
|
||||
|
||||
Le point de terminaison de destination pour votre requête webhook. Prend en charge les URL statiques et les valeurs dynamiques provenant d'autres blocs.
|
||||
|
||||
### Charge utile
|
||||
|
||||
Données JSON à envoyer dans le corps de la requête. Utilisez la baguette IA pour générer des charges utiles ou référencer des variables de workflow :
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "workflow.completed",
|
||||
"data": {
|
||||
"result": "<agent.content>",
|
||||
"timestamp": "<function.result>"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Secret de signature
|
||||
|
||||
Secret optionnel pour la signature HMAC-SHA256 de la charge utile. Lorsqu'il est fourni, ajoute un en-tête `X-Webhook-Signature` :
|
||||
|
||||
```
|
||||
X-Webhook-Signature: t=1704067200000,v1=5d41402abc4b2a76b9719d911017c592...
|
||||
```
|
||||
|
||||
Pour vérifier les signatures, calculez `HMAC-SHA256(secret, "${timestamp}.${body}")` et comparez avec la valeur `v1`.
|
||||
|
||||
### En-têtes supplémentaires
|
||||
|
||||
En-têtes personnalisés clé-valeur à inclure avec la requête. Ceux-ci remplacent tous les en-têtes automatiques portant le même nom.
|
||||
|
||||
## En-têtes automatiques
|
||||
|
||||
Chaque requête inclut automatiquement ces en-têtes :
|
||||
|
||||
| En-tête | Description |
|
||||
|--------|-------------|
|
||||
| `Content-Type` | `application/json` |
|
||||
| `X-Webhook-Timestamp` | Horodatage Unix en millisecondes |
|
||||
| `X-Delivery-ID` | UUID unique pour cette livraison |
|
||||
| `Idempotency-Key` | Identique à `X-Delivery-ID` pour la déduplication |
|
||||
|
||||
## Sorties
|
||||
|
||||
| Sortie | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `data` | json | Corps de la réponse du point de terminaison |
|
||||
| `status` | number | Code de statut HTTP |
|
||||
| `headers` | object | En-têtes de réponse |
|
||||
|
||||
## Exemples de cas d'usage
|
||||
|
||||
**Notifier des services externes** - Envoyer les résultats du workflow vers Slack, Discord ou des points de terminaison personnalisés
|
||||
|
||||
```
|
||||
Agent → Function (format) → Webhook (notify)
|
||||
```
|
||||
|
||||
**Déclencher des workflows externes** - Démarrer des processus dans d'autres systèmes lorsque des conditions sont remplies
|
||||
|
||||
```
|
||||
Condition (check) → Webhook (trigger) → Response
|
||||
```
|
||||
|
||||
<Callout>
|
||||
Le bloc Webhook utilise toujours POST. Pour d'autres méthodes HTTP ou plus de contrôle, utilisez le [bloc API](/blocks/api).
|
||||
</Callout>
|
||||
64
apps/docs/content/docs/fr/keyboard-shortcuts/index.mdx
Normal file
64
apps/docs/content/docs/fr/keyboard-shortcuts/index.mdx
Normal file
@@ -0,0 +1,64 @@
|
||||
---
|
||||
title: Raccourcis clavier
|
||||
description: Maîtrisez le canevas de workflow avec les raccourcis clavier et les
|
||||
contrôles de souris
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
|
||||
Accélérez la création de vos workflows avec ces raccourcis clavier et contrôles de souris. Tous les raccourcis fonctionnent lorsque le canevas est actif (pas lors de la saisie dans un champ de texte).
|
||||
|
||||
<Callout type="info">
|
||||
**Mod** fait référence à `Cmd` sur macOS et `Ctrl` sur Windows/Linux.
|
||||
</Callout>
|
||||
|
||||
## Contrôles du canevas
|
||||
|
||||
### Contrôles de la souris
|
||||
|
||||
| Action | Contrôle |
|
||||
|--------|---------|
|
||||
| Déplacer le canevas | Glisser-gauche sur un espace vide |
|
||||
| Déplacer le canevas | Molette ou trackpad |
|
||||
| Sélectionner plusieurs blocs | Glisser-droit pour tracer une zone de sélection |
|
||||
| Déplacer un bloc | Glisser-gauche sur l'en-tête du bloc |
|
||||
| Ajouter à la sélection | `Mod` + clic sur les blocs |
|
||||
|
||||
### Actions de workflow
|
||||
|
||||
| Raccourci | Action |
|
||||
|----------|--------|
|
||||
| `Mod` + `Enter` | Exécuter le workflow (ou annuler si en cours) |
|
||||
| `Mod` + `Z` | Annuler |
|
||||
| `Mod` + `Shift` + `Z` | Rétablir |
|
||||
| `Mod` + `C` | Copier les blocs sélectionnés |
|
||||
| `Mod` + `V` | Coller les blocs |
|
||||
| `Delete` ou `Backspace` | Supprimer les blocs ou connexions sélectionnés |
|
||||
| `Shift` + `L` | Disposition automatique du canevas |
|
||||
|
||||
## Navigation dans les panneaux
|
||||
|
||||
Ces raccourcis permettent de basculer entre les onglets du panneau sur le côté droit du canevas.
|
||||
|
||||
| Raccourci | Action |
|
||||
|----------|--------|
|
||||
| `C` | Activer l'onglet Copilot |
|
||||
| `T` | Activer l'onglet Barre d'outils |
|
||||
| `E` | Activer l'onglet Éditeur |
|
||||
| `Mod` + `F` | Activer la recherche dans la barre d'outils |
|
||||
|
||||
## Navigation globale
|
||||
|
||||
| Raccourci | Action |
|
||||
|----------|--------|
|
||||
| `Mod` + `K` | Ouvrir la recherche |
|
||||
| `Mod` + `Shift` + `A` | Ajouter un nouveau workflow d'agent |
|
||||
| `Mod` + `Y` | Aller aux modèles |
|
||||
| `Mod` + `L` | Aller aux journaux |
|
||||
|
||||
## Utilitaire
|
||||
|
||||
| Raccourci | Action |
|
||||
|----------|--------|
|
||||
| `Mod` + `D` | Effacer la console du terminal |
|
||||
| `Mod` + `E` | Effacer les notifications |
|
||||
@@ -1,231 +0,0 @@
|
||||
---
|
||||
title: Webhook
|
||||
description: Recevez des webhooks de n'importe quel service en configurant un
|
||||
webhook personnalisé.
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
import { Image } from '@/components/ui/image'
|
||||
|
||||
<BlockInfoCard
|
||||
type="generic_webhook"
|
||||
color="#10B981"
|
||||
/>
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/blocks/webhook.png"
|
||||
alt="Configuration du bloc Webhook"
|
||||
width={500}
|
||||
height={400}
|
||||
className="my-6"
|
||||
/>
|
||||
</div>
|
||||
|
||||
## Aperçu
|
||||
|
||||
Le bloc Webhook générique vous permet de recevoir des webhooks depuis n'importe quel service externe. C'est un déclencheur flexible qui peut traiter n'importe quelle charge utile JSON, ce qui le rend idéal pour l'intégration avec des services qui n'ont pas de bloc Sim dédié.
|
||||
|
||||
## Utilisation de base
|
||||
|
||||
### Mode de transmission simple
|
||||
|
||||
Sans définir un format d'entrée, le webhook transmet l'intégralité du corps de la requête tel quel :
|
||||
|
||||
```bash
|
||||
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Sim-Secret: your-secret" \
|
||||
-d '{
|
||||
"message": "Test webhook trigger",
|
||||
"data": {
|
||||
"key": "value"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
Accédez aux données dans les blocs en aval en utilisant :
|
||||
- `<webhook1.message>` → "Test webhook trigger"
|
||||
- `<webhook1.data.key>` → "value"
|
||||
|
||||
### Format d'entrée structuré (optionnel)
|
||||
|
||||
Définissez un schéma d'entrée pour obtenir des champs typés et activer des fonctionnalités avancées comme les téléchargements de fichiers :
|
||||
|
||||
**Configuration du format d'entrée :**
|
||||
|
||||
```json
|
||||
[
|
||||
{ "name": "message", "type": "string" },
|
||||
{ "name": "priority", "type": "number" },
|
||||
{ "name": "documents", "type": "files" }
|
||||
]
|
||||
```
|
||||
|
||||
**Requête Webhook :**
|
||||
|
||||
```bash
|
||||
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Sim-Secret: your-secret" \
|
||||
-d '{
|
||||
"message": "Invoice submission",
|
||||
"priority": 1,
|
||||
"documents": [
|
||||
{
|
||||
"type": "file",
|
||||
"data": "data:application/pdf;base64,JVBERi0xLjQK...",
|
||||
"name": "invoice.pdf",
|
||||
"mime": "application/pdf"
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
## Téléchargements de fichiers
|
||||
|
||||
### Formats de fichiers pris en charge
|
||||
|
||||
Le webhook prend en charge deux formats d'entrée de fichiers :
|
||||
|
||||
#### 1. Fichiers encodés en Base64
|
||||
Pour télécharger directement le contenu du fichier :
|
||||
|
||||
```json
|
||||
{
|
||||
"documents": [
|
||||
{
|
||||
"type": "file",
|
||||
"data": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgA...",
|
||||
"name": "screenshot.png",
|
||||
"mime": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- **Taille maximale** : 20 Mo par fichier
|
||||
- **Format** : URL de données standard avec encodage base64
|
||||
- **Stockage** : Les fichiers sont téléchargés dans un stockage d'exécution sécurisé
|
||||
|
||||
#### 2. Références URL
|
||||
Pour transmettre des URL de fichiers existants :
|
||||
|
||||
```json
|
||||
{
|
||||
"documents": [
|
||||
{
|
||||
"type": "url",
|
||||
"data": "https://example.com/files/document.pdf",
|
||||
"name": "document.pdf",
|
||||
"mime": "application/pdf"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Accès aux fichiers dans les blocs en aval
|
||||
|
||||
Les fichiers sont traités en objets `UserFile` avec les propriétés suivantes :
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: string, // Unique file identifier
|
||||
name: string, // Original filename
|
||||
url: string, // Presigned URL (valid for 5 minutes)
|
||||
size: number, // File size in bytes
|
||||
type: string, // MIME type
|
||||
key: string, // Storage key
|
||||
uploadedAt: string, // ISO timestamp
|
||||
expiresAt: string // ISO timestamp (5 minutes)
|
||||
}
|
||||
```
|
||||
|
||||
**Accès dans les blocs :**
|
||||
- `<webhook1.documents[0].url>` → URL de téléchargement
|
||||
- `<webhook1.documents[0].name>` → "invoice.pdf"
|
||||
- `<webhook1.documents[0].size>` → 524288
|
||||
- `<webhook1.documents[0].type>` → "application/pdf"
|
||||
|
||||
### Exemple complet de téléchargement de fichier
|
||||
|
||||
```bash
|
||||
# Create a base64-encoded file
|
||||
echo "Hello World" | base64
|
||||
# SGVsbG8gV29ybGQK
|
||||
|
||||
# Send webhook with file
|
||||
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Sim-Secret: your-secret" \
|
||||
-d '{
|
||||
"subject": "Document for review",
|
||||
"attachments": [
|
||||
{
|
||||
"type": "file",
|
||||
"data": "data:text/plain;base64,SGVsbG8gV29ybGQK",
|
||||
"name": "sample.txt",
|
||||
"mime": "text/plain"
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
## Authentification
|
||||
|
||||
### Configurer l'authentification (optionnel)
|
||||
|
||||
Dans la configuration du webhook :
|
||||
1. Activez "Exiger l'authentification"
|
||||
2. Définissez un jeton secret
|
||||
3. Choisissez le type d'en-tête :
|
||||
- **En-tête personnalisé** : `X-Sim-Secret: your-token`
|
||||
- **Autorisation Bearer** : `Authorization: Bearer your-token`
|
||||
|
||||
### Utilisation de l'authentification
|
||||
|
||||
```bash
|
||||
# With custom header
|
||||
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Sim-Secret: your-secret-token" \
|
||||
-d '{"message": "Authenticated request"}'
|
||||
|
||||
# With bearer token
|
||||
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer your-secret-token" \
|
||||
-d '{"message": "Authenticated request"}'
|
||||
```
|
||||
|
||||
## Bonnes pratiques
|
||||
|
||||
1. **Utiliser le format d'entrée pour la structure** : définissez un format d'entrée lorsque vous connaissez le schéma attendu. Cela fournit :
|
||||
- Validation de type
|
||||
- Meilleure autocomplétion dans l'éditeur
|
||||
- Capacités de téléchargement de fichiers
|
||||
|
||||
2. **Authentification** : activez toujours l'authentification pour les webhooks en production afin d'empêcher les accès non autorisés.
|
||||
|
||||
3. **Limites de taille de fichier** : gardez les fichiers en dessous de 20 Mo. Pour les fichiers plus volumineux, utilisez plutôt des références URL.
|
||||
|
||||
4. **Expiration des fichiers** : les fichiers téléchargés ont des URL d'expiration de 5 minutes. Traitez-les rapidement ou stockez-les ailleurs si vous en avez besoin plus longtemps.
|
||||
|
||||
5. **Gestion des erreurs** : le traitement des webhooks est asynchrone. Vérifiez les journaux d'exécution pour les erreurs.
|
||||
|
||||
6. **Tests** : utilisez le bouton "Tester le webhook" dans l'éditeur pour valider votre configuration avant le déploiement.
|
||||
|
||||
## Cas d'utilisation
|
||||
|
||||
- **Soumissions de formulaires** : recevez des données de formulaires personnalisés avec téléchargement de fichiers
|
||||
- **Intégrations tierces** : connectez-vous avec des services qui envoient des webhooks (Stripe, GitHub, etc.)
|
||||
- **Traitement de documents** : acceptez des documents de systèmes externes pour traitement
|
||||
- **Notifications d'événements** : recevez des données d'événements de diverses sources
|
||||
- **API personnalisées** : créez des points de terminaison API personnalisés pour vos applications
|
||||
|
||||
## Remarques
|
||||
|
||||
- Catégorie : `triggers`
|
||||
- Type : `generic_webhook`
|
||||
- **Support de fichiers** : disponible via la configuration du format d'entrée
|
||||
- **Taille maximale de fichier** : 20 Mo par fichier
|
||||
36
apps/docs/content/docs/fr/tools/imap.mdx
Normal file
36
apps/docs/content/docs/fr/tools/imap.mdx
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
title: Email IMAP
|
||||
description: Déclenchez des workflows lorsque de nouveaux emails arrivent via
|
||||
IMAP (fonctionne avec n'importe quel fournisseur d'email)
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="imap"
|
||||
color="#6366F1"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
Le déclencheur Email IMAP permet à vos workflows Sim de démarrer automatiquement dès qu'un nouvel email est reçu dans n'importe quelle boîte mail prenant en charge le protocole IMAP. Cela fonctionne avec Gmail, Outlook, Yahoo et la plupart des autres fournisseurs d'email.
|
||||
|
||||
Avec le déclencheur IMAP, vous pouvez :
|
||||
|
||||
- **Automatiser le traitement des emails** : démarrez des workflows en temps réel lorsque de nouveaux messages arrivent dans votre boîte de réception.
|
||||
- **Filtrer par expéditeur, objet ou dossier** : configurez votre déclencheur pour réagir uniquement aux emails correspondant à certaines conditions.
|
||||
- **Extraire et traiter les pièces jointes** : téléchargez et utilisez automatiquement les fichiers joints dans vos flux automatisés.
|
||||
- **Analyser et utiliser le contenu des emails** : accédez à l'objet, l'expéditeur, les destinataires, le corps complet et d'autres métadonnées dans les étapes suivantes du workflow.
|
||||
- **Intégrer avec n'importe quel fournisseur d'email** : fonctionne avec tout service offrant un accès IMAP standard, sans dépendance à un fournisseur.
|
||||
- **Déclencher sur non lu, marqué ou critères personnalisés** : configurez des filtres avancés pour les types d'emails qui démarrent vos workflows.
|
||||
|
||||
Avec Sim, l'intégration IMAP vous donne le pouvoir de transformer l'email en une source d'automatisation exploitable. Répondez aux demandes clients, traitez les notifications, lancez des pipelines de données et plus encore, directement depuis votre boîte de réception email, sans intervention manuelle.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
## Instructions d'utilisation
|
||||
|
||||
Connectez-vous à n'importe quel serveur email via le protocole IMAP pour déclencher des workflows lorsque de nouveaux emails sont reçus. Prend en charge Gmail, Outlook, Yahoo et tout autre fournisseur d'email compatible IMAP.
|
||||
|
||||
## Remarques
|
||||
|
||||
- Catégorie : `triggers`
|
||||
- Type : `imap`
|
||||
@@ -273,7 +273,7 @@ Créer une nouvelle organisation dans Jira Service Management
|
||||
| `name` | string | Nom de l'organisation créée |
|
||||
| `success` | boolean | Indique si l'opération a réussi |
|
||||
|
||||
### `jsm_add_organization_to_service_desk`
|
||||
### `jsm_add_organization`
|
||||
|
||||
Ajouter une organisation à un service desk dans Jira Service Management
|
||||
|
||||
|
||||
@@ -123,8 +123,6 @@ Récupérer le solde de votre compte et la valeur de votre portefeuille depuis K
|
||||
| --------- | ---- | ----------- |
|
||||
| `balance` | number | Solde du compte en centimes |
|
||||
| `portfolioValue` | number | Valeur du portefeuille en centimes |
|
||||
| `balanceDollars` | number | Solde du compte en dollars |
|
||||
| `portfolioValueDollars` | number | Valeur du portefeuille en dollars |
|
||||
|
||||
### `kalshi_get_positions`
|
||||
|
||||
|
||||
@@ -49,7 +49,8 @@ Interroger des données d'une table Supabase
|
||||
| --------- | ---- | ----------- | ----------- |
|
||||
| `projectId` | string | Oui | L'ID de votre projet Supabase \(ex. : jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | Oui | Le nom de la table Supabase à interroger |
|
||||
| `schema` | string | Non | Schéma de base de données à interroger \(par défaut : public\). Utilisez ceci pour accéder aux tables dans d'autres schémas. |
|
||||
| `schema` | string | Non | Schéma de base de données à partir duquel interroger \(par défaut : public\). Utilisez ceci pour accéder aux tables dans d'autres schémas. |
|
||||
| `select` | string | Non | Colonnes à retourner \(séparées par des virgules\). Par défaut * \(toutes les colonnes\) |
|
||||
| `filter` | string | Non | Filtre PostgREST \(ex. : "id=eq.123"\) |
|
||||
| `orderBy` | string | Non | Colonne pour le tri \(ajoutez DESC pour l'ordre décroissant\) |
|
||||
| `limit` | number | Non | Nombre maximum de lignes à retourner |
|
||||
@@ -93,7 +94,8 @@ Obtenir une seule ligne d'une table Supabase selon des critères de filtrage
|
||||
| --------- | ---- | ----------- | ----------- |
|
||||
| `projectId` | string | Oui | L'ID de votre projet Supabase \(ex. : jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | Oui | Le nom de la table Supabase à interroger |
|
||||
| `schema` | string | Non | Schéma de base de données à interroger \(par défaut : public\). Utilisez ceci pour accéder aux tables dans d'autres schémas. |
|
||||
| `schema` | string | Non | Schéma de base de données à partir duquel interroger \(par défaut : public\). Utilisez ceci pour accéder aux tables dans d'autres schémas. |
|
||||
| `select` | string | Non | Colonnes à retourner \(séparées par des virgules\). Par défaut * \(toutes les colonnes\) |
|
||||
| `filter` | string | Oui | Filtre PostgREST pour trouver la ligne spécifique \(ex. : "id=eq.123"\) |
|
||||
| `apiKey` | string | Oui | Votre clé secrète de rôle de service Supabase |
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@ Le bloc Webhook générique crée un point de terminaison flexible qui peut rece
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/blocks/webhook.png"
|
||||
alt="Configuration de webhook générique"
|
||||
src="/static/blocks/webhook-trigger.png"
|
||||
alt="Configuration du webhook générique"
|
||||
width={500}
|
||||
height={400}
|
||||
className="my-6"
|
||||
|
||||
89
apps/docs/content/docs/ja/blocks/webhook.mdx
Normal file
89
apps/docs/content/docs/ja/blocks/webhook.mdx
Normal file
@@ -0,0 +1,89 @@
|
||||
---
|
||||
title: Webhook
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Image } from '@/components/ui/image'
|
||||
|
||||
Webhookブロックは、自動的なWebhookヘッダーとオプションのHMAC署名を使用して、外部のWebhookエンドポイントにHTTP POSTリクエストを送信します。
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/blocks/webhook.png"
|
||||
alt="Webhookブロック"
|
||||
width={500}
|
||||
height={400}
|
||||
className="my-6"
|
||||
/>
|
||||
</div>
|
||||
|
||||
## 設定
|
||||
|
||||
### Webhook URL
|
||||
|
||||
Webhookリクエストの送信先エンドポイントです。静的URLと他のブロックからの動的な値の両方に対応しています。
|
||||
|
||||
### ペイロード
|
||||
|
||||
リクエストボディで送信するJSONデータです。AIワンドを使用してペイロードを生成したり、ワークフロー変数を参照したりできます。
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "workflow.completed",
|
||||
"data": {
|
||||
"result": "<agent.content>",
|
||||
"timestamp": "<function.result>"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 署名シークレット
|
||||
|
||||
HMAC-SHA256ペイロード署名用のオプションのシークレットです。指定すると、`X-Webhook-Signature`ヘッダーが追加されます。
|
||||
|
||||
```
|
||||
X-Webhook-Signature: t=1704067200000,v1=5d41402abc4b2a76b9719d911017c592...
|
||||
```
|
||||
|
||||
署名を検証するには、`HMAC-SHA256(secret, "${timestamp}.${body}")`を計算し、`v1`の値と比較します。
|
||||
|
||||
### 追加ヘッダー
|
||||
|
||||
リクエストに含めるカスタムのキーと値のヘッダーです。同じ名前の自動ヘッダーがある場合は上書きされます。
|
||||
|
||||
## 自動ヘッダー
|
||||
|
||||
すべてのリクエストには、以下のヘッダーが自動的に含まれます。
|
||||
|
||||
| ヘッダー | 説明 |
|
||||
|--------|-------------|
|
||||
| `Content-Type` | `application/json` |
|
||||
| `X-Webhook-Timestamp` | ミリ秒単位のUnixタイムスタンプ |
|
||||
| `X-Delivery-ID` | この配信の一意のUUID |
|
||||
| `Idempotency-Key` | 重複排除用の`X-Delivery-ID`と同じ |
|
||||
|
||||
## 出力
|
||||
|
||||
| 出力 | 型 | 説明 |
|
||||
|--------|------|-------------|
|
||||
| `data` | json | エンドポイントからのレスポンスボディ |
|
||||
| `status` | number | HTTPステータスコード |
|
||||
| `headers` | object | レスポンスヘッダー |
|
||||
|
||||
## 使用例
|
||||
|
||||
**外部サービスへの通知** - ワークフローの結果をSlack、Discord、またはカスタムエンドポイントに送信します。
|
||||
|
||||
```
|
||||
Agent → Function (format) → Webhook (notify)
|
||||
```
|
||||
|
||||
**外部ワークフローのトリガー** - 条件が満たされたときに他のシステムでプロセスを開始します。
|
||||
|
||||
```
|
||||
Condition (check) → Webhook (trigger) → Response
|
||||
```
|
||||
|
||||
<Callout>
|
||||
Webhookブロックは常にPOSTを使用します。他のHTTPメソッドやより詳細な制御が必要な場合は、[APIブロック](/blocks/api)を使用してください。
|
||||
</Callout>
|
||||
63
apps/docs/content/docs/ja/keyboard-shortcuts/index.mdx
Normal file
63
apps/docs/content/docs/ja/keyboard-shortcuts/index.mdx
Normal file
@@ -0,0 +1,63 @@
|
||||
---
|
||||
title: キーボードショートカット
|
||||
description: キーボードショートカットとマウス操作でワークフローキャンバスをマスターしましょう
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
|
||||
これらのキーボードショートカットとマウス操作でワークフロー構築を高速化できます。すべてのショートカットは、キャンバスにフォーカスがある時に機能します(入力フィールドに入力中は機能しません)。
|
||||
|
||||
<Callout type="info">
|
||||
**Mod**はmacOSでは`Cmd`、Windows/Linuxでは`Ctrl`を指します。
|
||||
</Callout>
|
||||
|
||||
## キャンバス操作
|
||||
|
||||
### マウス操作
|
||||
|
||||
| 操作 | 操作方法 |
|
||||
|--------|---------|
|
||||
| キャンバスの移動 | 空白部分を左ドラッグ |
|
||||
| キャンバスの移動 | スクロールまたはトラックパッド |
|
||||
| 複数ブロックの選択 | 右ドラッグで選択ボックスを描画 |
|
||||
| ブロックのドラッグ | ブロックヘッダーを左ドラッグ |
|
||||
| 選択に追加 | `Mod` + ブロックをクリック |
|
||||
|
||||
### ワークフロー操作
|
||||
|
||||
| ショートカット | 操作 |
|
||||
|----------|--------|
|
||||
| `Mod` + `Enter` | ワークフローを実行(実行中の場合はキャンセル) |
|
||||
| `Mod` + `Z` | 元に戻す |
|
||||
| `Mod` + `Shift` + `Z` | やり直す |
|
||||
| `Mod` + `C` | 選択したブロックをコピー |
|
||||
| `Mod` + `V` | ブロックを貼り付け |
|
||||
| `Delete`または`Backspace` | 選択したブロックまたはエッジを削除 |
|
||||
| `Shift` + `L` | キャンバスを自動レイアウト |
|
||||
|
||||
## パネルナビゲーション
|
||||
|
||||
これらのショートカットは、キャンバス右側のパネルタブを切り替えます。
|
||||
|
||||
| ショートカット | 操作 |
|
||||
|----------|--------|
|
||||
| `C` | Copilotタブにフォーカス |
|
||||
| `T` | Toolbarタブにフォーカス |
|
||||
| `E` | Editorタブにフォーカス |
|
||||
| `Mod` + `F` | Toolbar検索にフォーカス |
|
||||
|
||||
## グローバルナビゲーション
|
||||
|
||||
| ショートカット | 操作 |
|
||||
|----------|--------|
|
||||
| `Mod` + `K` | 検索を開く |
|
||||
| `Mod` + `Shift` + `A` | 新しいエージェントワークフローを追加 |
|
||||
| `Mod` + `Y` | テンプレートに移動 |
|
||||
| `Mod` + `L` | ログに移動 |
|
||||
|
||||
## ユーティリティ
|
||||
|
||||
| ショートカット | アクション |
|
||||
|----------|--------|
|
||||
| `Mod` + `D` | ターミナルコンソールをクリア |
|
||||
| `Mod` + `E` | 通知をクリア |
|
||||
@@ -1,230 +0,0 @@
|
||||
---
|
||||
title: Webhook
|
||||
description: カスタムウェブフックを設定して、任意のサービスからウェブフックを受信します。
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
import { Image } from '@/components/ui/image'
|
||||
|
||||
<BlockInfoCard
|
||||
type="generic_webhook"
|
||||
color="#10B981"
|
||||
/>
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/blocks/webhook.png"
|
||||
alt="Webhookブロックの設定"
|
||||
width={500}
|
||||
height={400}
|
||||
className="my-6"
|
||||
/>
|
||||
</div>
|
||||
|
||||
## 概要
|
||||
|
||||
汎用Webhookブロックを使用すると、任意の外部サービスからWebhookを受信できます。これは柔軟なトリガーであり、あらゆるJSONペイロードを処理できるため、専用のSimブロックがないサービスとの統合に最適です。
|
||||
|
||||
## 基本的な使用方法
|
||||
|
||||
### シンプルなパススルーモード
|
||||
|
||||
入力フォーマットを定義しない場合、Webhookはリクエスト本文全体をそのまま渡します:
|
||||
|
||||
```bash
|
||||
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Sim-Secret: your-secret" \
|
||||
-d '{
|
||||
"message": "Test webhook trigger",
|
||||
"data": {
|
||||
"key": "value"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
下流のブロックでデータにアクセスする方法:
|
||||
- `<webhook1.message>` → "Test webhook trigger"
|
||||
- `<webhook1.data.key>` → "value"
|
||||
|
||||
### 構造化入力フォーマット(オプション)
|
||||
|
||||
入力スキーマを定義して、型付きフィールドを取得し、ファイルアップロードなどの高度な機能を有効にします:
|
||||
|
||||
**入力フォーマット設定:**
|
||||
|
||||
```json
|
||||
[
|
||||
{ "name": "message", "type": "string" },
|
||||
{ "name": "priority", "type": "number" },
|
||||
{ "name": "documents", "type": "files" }
|
||||
]
|
||||
```
|
||||
|
||||
**Webhookリクエスト:**
|
||||
|
||||
```bash
|
||||
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Sim-Secret: your-secret" \
|
||||
-d '{
|
||||
"message": "Invoice submission",
|
||||
"priority": 1,
|
||||
"documents": [
|
||||
{
|
||||
"type": "file",
|
||||
"data": "data:application/pdf;base64,JVBERi0xLjQK...",
|
||||
"name": "invoice.pdf",
|
||||
"mime": "application/pdf"
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
## ファイルアップロード
|
||||
|
||||
### サポートされているファイル形式
|
||||
|
||||
Webhookは2つのファイル入力形式をサポートしています:
|
||||
|
||||
#### 1. Base64エンコードファイル
|
||||
ファイルコンテンツを直接アップロードする場合:
|
||||
|
||||
```json
|
||||
{
|
||||
"documents": [
|
||||
{
|
||||
"type": "file",
|
||||
"data": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgA...",
|
||||
"name": "screenshot.png",
|
||||
"mime": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- **最大サイズ**: ファイルあたり20MB
|
||||
- **フォーマット**: Base64エンコーディングを使用した標準データURL
|
||||
- **ストレージ**: ファイルは安全な実行ストレージにアップロードされます
|
||||
|
||||
#### 2. URL参照
|
||||
既存のファイルURLを渡す場合:
|
||||
|
||||
```json
|
||||
{
|
||||
"documents": [
|
||||
{
|
||||
"type": "url",
|
||||
"data": "https://example.com/files/document.pdf",
|
||||
"name": "document.pdf",
|
||||
"mime": "application/pdf"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 下流のブロックでファイルにアクセスする
|
||||
|
||||
ファイルは以下のプロパティを持つ `UserFile` オブジェクトに処理されます:
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: string, // Unique file identifier
|
||||
name: string, // Original filename
|
||||
url: string, // Presigned URL (valid for 5 minutes)
|
||||
size: number, // File size in bytes
|
||||
type: string, // MIME type
|
||||
key: string, // Storage key
|
||||
uploadedAt: string, // ISO timestamp
|
||||
expiresAt: string // ISO timestamp (5 minutes)
|
||||
}
|
||||
```
|
||||
|
||||
**ブロック内でのアクセス:**
|
||||
- `<webhook1.documents[0].url>` → ダウンロードURL
|
||||
- `<webhook1.documents[0].name>` → "invoice.pdf"
|
||||
- `<webhook1.documents[0].size>` → 524288
|
||||
- `<webhook1.documents[0].type>` → "application/pdf"
|
||||
|
||||
### ファイルアップロードの完全な例
|
||||
|
||||
```bash
|
||||
# Create a base64-encoded file
|
||||
echo "Hello World" | base64
|
||||
# SGVsbG8gV29ybGQK
|
||||
|
||||
# Send webhook with file
|
||||
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Sim-Secret: your-secret" \
|
||||
-d '{
|
||||
"subject": "Document for review",
|
||||
"attachments": [
|
||||
{
|
||||
"type": "file",
|
||||
"data": "data:text/plain;base64,SGVsbG8gV29ybGQK",
|
||||
"name": "sample.txt",
|
||||
"mime": "text/plain"
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
## 認証
|
||||
|
||||
### 認証の設定(オプション)
|
||||
|
||||
ウェブフック設定で:
|
||||
1. 「認証を要求する」を有効にする
|
||||
2. シークレットトークンを設定する
|
||||
3. ヘッダータイプを選択する:
|
||||
- **カスタムヘッダー**: `X-Sim-Secret: your-token`
|
||||
- **認証ベアラー**: `Authorization: Bearer your-token`
|
||||
|
||||
### 認証の使用
|
||||
|
||||
```bash
|
||||
# With custom header
|
||||
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Sim-Secret: your-secret-token" \
|
||||
-d '{"message": "Authenticated request"}'
|
||||
|
||||
# With bearer token
|
||||
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer your-secret-token" \
|
||||
-d '{"message": "Authenticated request"}'
|
||||
```
|
||||
|
||||
## ベストプラクティス
|
||||
|
||||
1. **構造化のための入力フォーマットの使用**: 予想されるスキーマがわかっている場合は入力フォーマットを定義してください。これにより以下が提供されます:
|
||||
- 型の検証
|
||||
- エディタでのより良いオートコンプリート
|
||||
- ファイルアップロード機能
|
||||
|
||||
2. **認証**: 不正アクセスを防ぐため、本番環境のウェブフックには常に認証を有効にしてください。
|
||||
|
||||
3. **ファイルサイズの制限**: ファイルは20MB未満に保ってください。より大きなファイルの場合は、代わりにURL参照を使用してください。
|
||||
|
||||
4. **ファイルの有効期限**: ダウンロードされたファイルのURLは5分間有効です。すぐに処理するか、長期間必要な場合は別の場所に保存してください。
|
||||
|
||||
5. **エラー処理**: ウェブフック処理は非同期です。エラーについては実行ログを確認してください。
|
||||
|
||||
6. **テスト**: 設定をデプロイする前に、エディタの「ウェブフックをテスト」ボタンを使用して設定を検証してください。
|
||||
|
||||
## ユースケース
|
||||
|
||||
- **フォーム送信**: ファイルアップロード機能を持つカスタムフォームからデータを受け取る
|
||||
- **サードパーティ連携**: ウェブフックを送信するサービス(Stripe、GitHubなど)と接続する
|
||||
- **ドキュメント処理**: 外部システムからドキュメントを受け取って処理する
|
||||
- **イベント通知**: さまざまなソースからイベントデータを受け取る
|
||||
- **カスタムAPI**: アプリケーション用のカスタムAPIエンドポイントを構築する
|
||||
|
||||
## 注意事項
|
||||
|
||||
- カテゴリ:`triggers`
|
||||
- タイプ:`generic_webhook`
|
||||
- **ファイルサポート**:入力フォーマット設定で利用可能
|
||||
- **最大ファイルサイズ**:ファイルあたり20MB
|
||||
35
apps/docs/content/docs/ja/tools/imap.mdx
Normal file
35
apps/docs/content/docs/ja/tools/imap.mdx
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
title: IMAPメール
|
||||
description: IMAP経由で新しいメールが届いたときにワークフローをトリガー(すべてのメールプロバイダーで動作)
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="imap"
|
||||
color="#6366F1"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
IMAPメールトリガーを使用すると、IMAPプロトコルをサポートする任意のメールボックスで新しいメールを受信したときに、Simワークフローを自動的に開始できます。Gmail、Outlook、Yahoo、その他ほとんどのメールプロバイダーで動作します。
|
||||
|
||||
IMAPトリガーでできること:
|
||||
|
||||
- **メール処理の自動化**:受信トレイに新しいメッセージが届いたときにリアルタイムでワークフローを開始します。
|
||||
- **送信者、件名、フォルダーでフィルタリング**:特定の条件に一致するメールにのみ反応するようにトリガーを設定します。
|
||||
- **添付ファイルの抽出と処理**:自動化フローでファイル添付を自動的にダウンロードして使用します。
|
||||
- **メールコンテンツの解析と使用**:件名、送信者、受信者、本文全体、その他のメタデータに、ワークフローの後続ステップでアクセスします。
|
||||
- **あらゆるメールプロバイダーとの統合**:ベンダーロックインなしで、標準のIMAPアクセスを提供する任意のサービスで動作します。
|
||||
- **未読、フラグ付き、カスタム条件でトリガー**:ワークフローを開始するメールの種類に対して高度なフィルターを設定します。
|
||||
|
||||
Simを使用すると、IMAP統合により、メールを実行可能な自動化ソースに変える力が得られます。顧客からの問い合わせへの対応、通知の処理、データパイプラインの開始など、手動操作なしで、メール受信トレイから直接実行できます。
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
## 使用方法
|
||||
|
||||
IMAPプロトコル経由で任意のメールサーバーに接続し、新しいメールを受信したときにワークフローをトリガーします。Gmail、Outlook、Yahoo、その他のIMAP互換メールプロバイダーをサポートします。
|
||||
|
||||
## 注意事項
|
||||
|
||||
- カテゴリ: `triggers`
|
||||
- タイプ: `imap`
|
||||
@@ -273,7 +273,7 @@ Jira Service Managementで新しい組織を作成する
|
||||
| `name` | string | 作成された組織の名前 |
|
||||
| `success` | boolean | 操作が成功したかどうか |
|
||||
|
||||
### `jsm_add_organization_to_service_desk`
|
||||
### `jsm_add_organization`
|
||||
|
||||
Jira Service Managementのサービスデスクに組織を追加する
|
||||
|
||||
|
||||
@@ -121,10 +121,8 @@ Kalshiからアカウント残高とポートフォリオ価値を取得
|
||||
|
||||
| パラメータ | 型 | 説明 |
|
||||
| --------- | ---- | ----------- |
|
||||
| `balance` | number | セント単位のアカウント残高 |
|
||||
| `portfolioValue` | number | セント単位のポートフォリオ価値 |
|
||||
| `balanceDollars` | number | ドル単位のアカウント残高 |
|
||||
| `portfolioValueDollars` | number | ドル単位のポートフォリオ価値 |
|
||||
| `balance` | number | アカウント残高(セント単位) |
|
||||
| `portfolioValue` | number | ポートフォリオ価値(セント単位) |
|
||||
|
||||
### `kalshi_get_positions`
|
||||
|
||||
|
||||
@@ -49,7 +49,8 @@ Supabaseテーブルからデータを照会する
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | はい | あなたのSupabaseプロジェクトID(例:jdrkgepadsdopsntdlom) |
|
||||
| `table` | string | はい | クエリするSupabaseテーブルの名前 |
|
||||
| `schema` | string | いいえ | クエリするデータベーススキーマ(デフォルト:public)。他のスキーマのテーブルにアクセスする場合に使用します。 |
|
||||
| `schema` | string | いいえ | クエリ元のデータベーススキーマ(デフォルト:public)。他のスキーマのテーブルにアクセスする場合に使用します。 |
|
||||
| `select` | string | いいえ | 返す列(カンマ区切り)。デフォルトは*(すべての列) |
|
||||
| `filter` | string | いいえ | PostgRESTフィルター(例:"id=eq.123") |
|
||||
| `orderBy` | string | いいえ | 並べ替える列(降順の場合はDESCを追加) |
|
||||
| `limit` | number | いいえ | 返す最大行数 |
|
||||
@@ -93,7 +94,8 @@ Supabaseテーブルにデータを挿入する
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | はい | あなたのSupabaseプロジェクトID(例:jdrkgepadsdopsntdlom) |
|
||||
| `table` | string | はい | クエリするSupabaseテーブルの名前 |
|
||||
| `schema` | string | いいえ | クエリするデータベーススキーマ(デフォルト:public)。他のスキーマのテーブルにアクセスする場合に使用します。 |
|
||||
| `schema` | string | いいえ | クエリ元のデータベーススキーマ(デフォルト:public)。他のスキーマのテーブルにアクセスする場合に使用します。 |
|
||||
| `select` | string | いいえ | 返す列(カンマ区切り)。デフォルトは*(すべての列) |
|
||||
| `filter` | string | はい | 特定の行を見つけるためのPostgRESTフィルター(例:"id=eq.123") |
|
||||
| `apiKey` | string | はい | あなたのSupabaseサービスロールシークレットキー |
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ Webhookを使用すると、外部サービスがHTTPリクエストを送信し
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/blocks/webhook.png"
|
||||
src="/static/blocks/webhook-trigger.png"
|
||||
alt="汎用Webhook設定"
|
||||
width={500}
|
||||
height={400}
|
||||
|
||||
89
apps/docs/content/docs/zh/blocks/webhook.mdx
Normal file
89
apps/docs/content/docs/zh/blocks/webhook.mdx
Normal file
@@ -0,0 +1,89 @@
|
||||
---
|
||||
title: Webhook
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
import { Image } from '@/components/ui/image'
|
||||
|
||||
Webhook 模块会向外部 webhook 端点发送 HTTP POST 请求,自动附加 webhook 头部,并可选用 HMAC 签名。
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/blocks/webhook.png"
|
||||
alt="Webhook 模块"
|
||||
width={500}
|
||||
height={400}
|
||||
className="my-6"
|
||||
/>
|
||||
</div>
|
||||
|
||||
## 配置
|
||||
|
||||
### Webhook URL
|
||||
|
||||
Webhook 请求的目标端点。支持静态 URL 和来自其他模块的动态值。
|
||||
|
||||
### 负载
|
||||
|
||||
要在请求体中发送的 JSON 数据。可使用 AI 魔杖生成负载,或引用工作流变量:
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "workflow.completed",
|
||||
"data": {
|
||||
"result": "<agent.content>",
|
||||
"timestamp": "<function.result>"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 签名密钥
|
||||
|
||||
可选的 HMAC-SHA256 负载签名密钥。填写后会添加 `X-Webhook-Signature` 头部:
|
||||
|
||||
```
|
||||
X-Webhook-Signature: t=1704067200000,v1=5d41402abc4b2a76b9719d911017c592...
|
||||
```
|
||||
|
||||
要验证签名,请计算 `HMAC-SHA256(secret, "${timestamp}.${body}")` 并与 `v1` 的值进行比对。
|
||||
|
||||
### 额外头部
|
||||
|
||||
自定义的键值头部,将随请求一同发送。若与自动头部同名,则会覆盖自动头部。
|
||||
|
||||
## 自动头部
|
||||
|
||||
每个请求都会自动包含以下头部:
|
||||
|
||||
| Header | 说明 |
|
||||
|--------|------|
|
||||
| `Content-Type` | `application/json` |
|
||||
| `X-Webhook-Timestamp` | Unix 时间戳(毫秒) |
|
||||
| `X-Delivery-ID` | 本次投递的唯一 UUID |
|
||||
| `Idempotency-Key` | 与 `X-Delivery-ID` 相同,用于去重 |
|
||||
|
||||
## 输出
|
||||
|
||||
| 输出 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `data` | json | 端点返回的响应体 |
|
||||
| `status` | number | HTTP 状态码 |
|
||||
| `headers` | object | 响应头部 |
|
||||
|
||||
## 示例用例
|
||||
|
||||
**通知外部服务** - 将工作流结果发送到 Slack、Discord 或自定义端点
|
||||
|
||||
```
|
||||
Agent → Function (format) → Webhook (notify)
|
||||
```
|
||||
|
||||
**触发外部工作流** - 当满足条件时,在其他系统中启动流程
|
||||
|
||||
```
|
||||
Condition (check) → Webhook (trigger) → Response
|
||||
```
|
||||
|
||||
<Callout>
|
||||
Webhook 模块始终使用 POST。如需使用其他 HTTP 方法或获得更多控制,请使用 [API 模块](/blocks/api)。
|
||||
</Callout>
|
||||
63
apps/docs/content/docs/zh/keyboard-shortcuts/index.mdx
Normal file
63
apps/docs/content/docs/zh/keyboard-shortcuts/index.mdx
Normal file
@@ -0,0 +1,63 @@
|
||||
---
|
||||
title: 键盘快捷键
|
||||
description: 通过键盘快捷键和鼠标操作,掌控工作流画布
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
|
||||
使用这些键盘快捷键和鼠标操作,可以加快你的工作流构建速度。所有快捷键仅在画布聚焦时有效(在输入框中输入时无效)。
|
||||
|
||||
<Callout type="info">
|
||||
**Mod** 指的是在 macOS 上为 `Cmd`,在 Windows/Linux 上为 `Ctrl`。
|
||||
</Callout>
|
||||
|
||||
## 画布操作
|
||||
|
||||
### 鼠标操作
|
||||
|
||||
| 操作 | 控制方式 |
|
||||
|--------|---------|
|
||||
| 平移/移动画布 | 在空白处左键拖动 |
|
||||
| 平移/移动画布 | 滚轮或触控板 |
|
||||
| 多选区块 | 右键拖动绘制选择框 |
|
||||
| 拖动区块 | 在区块标题处左键拖动 |
|
||||
| 添加到选择 | `Mod` + 点击区块 |
|
||||
|
||||
### 工作流操作
|
||||
|
||||
| 快捷键 | 操作 |
|
||||
|----------|--------|
|
||||
| `Mod` + `Enter` | 运行工作流(如正在运行则取消) |
|
||||
| `Mod` + `Z` | 撤销 |
|
||||
| `Mod` + `Shift` + `Z` | 重做 |
|
||||
| `Mod` + `C` | 复制所选区块 |
|
||||
| `Mod` + `V` | 粘贴区块 |
|
||||
| `Delete` 或 `Backspace` | 删除所选区块或连线 |
|
||||
| `Shift` + `L` | 自动布局画布 |
|
||||
|
||||
## 面板导航
|
||||
|
||||
这些快捷键可在画布右侧的面板标签页之间切换。
|
||||
|
||||
| 快捷键 | 操作 |
|
||||
|----------|--------|
|
||||
| `C` | 聚焦 Copilot 标签页 |
|
||||
| `T` | 聚焦 Toolbar 标签页 |
|
||||
| `E` | 聚焦 Editor 标签页 |
|
||||
| `Mod` + `F` | 聚焦 Toolbar 搜索 |
|
||||
|
||||
## 全局导航
|
||||
|
||||
| 快捷键 | 操作 |
|
||||
|----------|--------|
|
||||
| `Mod` + `K` | 打开搜索 |
|
||||
| `Mod` + `Shift` + `A` | 新建 Agent 工作流 |
|
||||
| `Mod` + `Y` | 跳转到模板 |
|
||||
| `Mod` + `L` | 跳转到日志 |
|
||||
|
||||
## 实用工具
|
||||
|
||||
| 快捷键 | 操作 |
|
||||
|----------|--------|
|
||||
| `Mod` + `D` | 清除终端控制台 |
|
||||
| `Mod` + `E` | 清除通知 |
|
||||
@@ -1,230 +0,0 @@
|
||||
---
|
||||
title: Webhook
|
||||
description: 通过配置自定义 webhook,从任何服务接收 webhook。
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
import { Image } from '@/components/ui/image'
|
||||
|
||||
<BlockInfoCard
|
||||
type="generic_webhook"
|
||||
color="#10B981"
|
||||
/>
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/blocks/webhook.png"
|
||||
alt="Webhook Block Configuration"
|
||||
width={500}
|
||||
height={400}
|
||||
className="my-6"
|
||||
/>
|
||||
</div>
|
||||
|
||||
## 概述
|
||||
|
||||
通用 Webhook 模块允许您接收来自任何外部服务的 webhook。这是一个灵活的触发器,可以处理任何 JSON 负载,非常适合与没有专用 Sim 模块的服务集成。
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 简单直通模式
|
||||
|
||||
在未定义输入格式的情况下,webhook 会按原样传递整个请求正文:
|
||||
|
||||
```bash
|
||||
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Sim-Secret: your-secret" \
|
||||
-d '{
|
||||
"message": "Test webhook trigger",
|
||||
"data": {
|
||||
"key": "value"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
在下游模块中使用以下方式访问数据:
|
||||
- `<webhook1.message>` → "测试 webhook 触发器"
|
||||
- `<webhook1.data.key>` → "值"
|
||||
|
||||
### 结构化输入格式(可选)
|
||||
|
||||
定义输入模式以获取类型化字段,并启用高级功能,例如文件上传:
|
||||
|
||||
**输入格式配置:**
|
||||
|
||||
```json
|
||||
[
|
||||
{ "name": "message", "type": "string" },
|
||||
{ "name": "priority", "type": "number" },
|
||||
{ "name": "documents", "type": "files" }
|
||||
]
|
||||
```
|
||||
|
||||
**Webhook 请求:**
|
||||
|
||||
```bash
|
||||
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Sim-Secret: your-secret" \
|
||||
-d '{
|
||||
"message": "Invoice submission",
|
||||
"priority": 1,
|
||||
"documents": [
|
||||
{
|
||||
"type": "file",
|
||||
"data": "data:application/pdf;base64,JVBERi0xLjQK...",
|
||||
"name": "invoice.pdf",
|
||||
"mime": "application/pdf"
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
## 文件上传
|
||||
|
||||
### 支持的文件格式
|
||||
|
||||
webhook 支持两种文件输入格式:
|
||||
|
||||
#### 1. Base64 编码文件
|
||||
用于直接上传文件内容:
|
||||
|
||||
```json
|
||||
{
|
||||
"documents": [
|
||||
{
|
||||
"type": "file",
|
||||
"data": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgA...",
|
||||
"name": "screenshot.png",
|
||||
"mime": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- **最大大小**:每个文件 20MB
|
||||
- **格式**:带有 base64 编码的标准数据 URL
|
||||
- **存储**:文件上传到安全的执行存储
|
||||
|
||||
#### 2. URL 引用
|
||||
用于传递现有文件 URL:
|
||||
|
||||
```json
|
||||
{
|
||||
"documents": [
|
||||
{
|
||||
"type": "url",
|
||||
"data": "https://example.com/files/document.pdf",
|
||||
"name": "document.pdf",
|
||||
"mime": "application/pdf"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 在下游模块中访问文件
|
||||
|
||||
文件被处理为具有以下属性的 `UserFile` 对象:
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: string, // Unique file identifier
|
||||
name: string, // Original filename
|
||||
url: string, // Presigned URL (valid for 5 minutes)
|
||||
size: number, // File size in bytes
|
||||
type: string, // MIME type
|
||||
key: string, // Storage key
|
||||
uploadedAt: string, // ISO timestamp
|
||||
expiresAt: string // ISO timestamp (5 minutes)
|
||||
}
|
||||
```
|
||||
|
||||
**分块访问:**
|
||||
- `<webhook1.documents[0].url>` → 下载 URL
|
||||
- `<webhook1.documents[0].name>` → "invoice.pdf"
|
||||
- `<webhook1.documents[0].size>` → 524288
|
||||
- `<webhook1.documents[0].type>` → "application/pdf"
|
||||
|
||||
### 完整文件上传示例
|
||||
|
||||
```bash
|
||||
# Create a base64-encoded file
|
||||
echo "Hello World" | base64
|
||||
# SGVsbG8gV29ybGQK
|
||||
|
||||
# Send webhook with file
|
||||
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Sim-Secret: your-secret" \
|
||||
-d '{
|
||||
"subject": "Document for review",
|
||||
"attachments": [
|
||||
{
|
||||
"type": "file",
|
||||
"data": "data:text/plain;base64,SGVsbG8gV29ybGQK",
|
||||
"name": "sample.txt",
|
||||
"mime": "text/plain"
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
## 身份验证
|
||||
|
||||
### 配置身份验证(可选)
|
||||
|
||||
在 webhook 配置中:
|
||||
1. 启用“需要身份验证”
|
||||
2. 设置一个密钥令牌
|
||||
3. 选择头类型:
|
||||
- **自定义头**: `X-Sim-Secret: your-token`
|
||||
- **授权 Bearer**: `Authorization: Bearer your-token`
|
||||
|
||||
### 使用身份验证
|
||||
|
||||
```bash
|
||||
# With custom header
|
||||
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Sim-Secret: your-secret-token" \
|
||||
-d '{"message": "Authenticated request"}'
|
||||
|
||||
# With bearer token
|
||||
curl -X POST https://sim.ai/api/webhooks/trigger/{webhook-path} \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer your-secret-token" \
|
||||
-d '{"message": "Authenticated request"}'
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **使用输入格式定义结构**:当您知道预期的模式时,定义输入格式。这提供:
|
||||
- 类型验证
|
||||
- 编辑器中的更好自动完成
|
||||
- 文件上传功能
|
||||
|
||||
2. **身份验证**:在生产环境的 webhook 中始终启用身份验证,以防止未经授权的访问。
|
||||
|
||||
3. **文件大小限制**:将文件保持在 20MB 以下。对于更大的文件,请使用 URL 引用。
|
||||
|
||||
4. **文件过期**:下载的文件具有 5 分钟的 URL 过期时间。请及时处理,或如果需要更长时间,请将其存储在其他地方。
|
||||
|
||||
5. **错误处理**:Webhook 处理是异步的。请检查执行日志以获取错误信息。
|
||||
|
||||
6. **测试**:在部署前,使用编辑器中的“测试 Webhook”按钮验证您的配置。
|
||||
|
||||
## 使用场景
|
||||
|
||||
- **表单提交**:接收带有文件上传的自定义表单数据
|
||||
- **第三方集成**:与发送 webhook 的服务(如 Stripe、GitHub 等)连接
|
||||
- **文档处理**:接受来自外部系统的文档进行处理
|
||||
- **事件通知**:接收来自各种来源的事件数据
|
||||
- **自定义 API**:为您的应用程序构建自定义 API 端点
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 类别:`triggers`
|
||||
- 类型:`generic_webhook`
|
||||
- **文件支持**:通过输入格式配置可用
|
||||
- **最大文件大小**:每个文件 20MB
|
||||
35
apps/docs/content/docs/zh/tools/imap.mdx
Normal file
35
apps/docs/content/docs/zh/tools/imap.mdx
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
title: IMAP 邮件
|
||||
description: 当通过 IMAP 收到新邮件时触发工作流(适用于任何邮箱服务商)
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="imap"
|
||||
color="#6366F1"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
IMAP 邮件触发器可以让你的 Sim 工作流在任何支持 IMAP 协议的邮箱收到新邮件时自动启动。适用于 Gmail、Outlook、Yahoo 及大多数其他邮箱服务商。
|
||||
|
||||
使用 IMAP 触发器,你可以:
|
||||
|
||||
- **自动化邮件处理**:当新邮件到达收件箱时,实时启动工作流。
|
||||
- **按发件人、主题或文件夹筛选**:配置触发器,仅对符合特定条件的邮件做出响应。
|
||||
- **提取并处理附件**:自动下载并在自动化流程中使用邮件附件。
|
||||
- **解析并利用邮件内容**:在后续工作流步骤中访问主题、发件人、收件人、正文及其他元数据。
|
||||
- **与任意邮箱服务集成**:支持所有提供标准 IMAP 访问的服务,无需受限于特定厂商。
|
||||
- **按未读、标记或自定义条件触发**:为启动工作流的邮件设置高级筛选条件。
|
||||
|
||||
借助 Sim,IMAP 集成让你能够将邮件变为可操作的自动化来源。无需人工干预,即可直接从邮箱收件箱响应客户咨询、处理通知、启动数据流程等。
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
## 使用说明
|
||||
|
||||
通过 IMAP 协议连接任意邮件服务器,在收到新邮件时触发工作流。支持 Gmail、Outlook、Yahoo 及所有兼容 IMAP 的邮箱服务商。
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 分类:`triggers`
|
||||
- 类型:`imap`
|
||||
@@ -273,7 +273,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
| `name` | string | 已创建组织的名称 |
|
||||
| `success` | boolean | 操作是否成功 |
|
||||
|
||||
### `jsm_add_organization_to_service_desk`
|
||||
### `jsm_add_organization`
|
||||
|
||||
在 Jira Service Management 中将组织添加到服务台
|
||||
|
||||
|
||||
@@ -123,8 +123,6 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
| --------- | ---- | ----------- |
|
||||
| `balance` | number | 账户余额(以分为单位) |
|
||||
| `portfolioValue` | number | 投资组合价值(以分为单位) |
|
||||
| `balanceDollars` | number | 账户余额(以美元为单位) |
|
||||
| `portfolioValueDollars` | number | 投资组合价值(以美元为单位) |
|
||||
|
||||
### `kalshi_get_positions`
|
||||
|
||||
|
||||
@@ -50,8 +50,9 @@ Sim 的 Supabase 集成使您能够轻松地将代理工作流连接到您的 Su
|
||||
| `projectId` | string | 是 | 您的 Supabase 项目 ID \(例如:jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | 是 | 要查询的 Supabase 表名 |
|
||||
| `schema` | string | 否 | 要查询的数据库 schema \(默认:public\)。用于访问其他 schema 下的表。|
|
||||
| `select` | string | 否 | 要返回的列(逗号分隔)。默认为 *(所有列)|
|
||||
| `filter` | string | 否 | PostgREST 过滤条件 \(例如:"id=eq.123"\) |
|
||||
| `orderBy` | string | 否 | 排序的列名 \(添加 DESC 表示降序\) |
|
||||
| `orderBy` | string | 否 | 排序的列(添加 DESC 表示降序)|
|
||||
| `limit` | number | 否 | 返回的最大行数 |
|
||||
| `apiKey` | string | 是 | 您的 Supabase 服务角色密钥 |
|
||||
|
||||
@@ -94,7 +95,8 @@ Sim 的 Supabase 集成使您能够轻松地将代理工作流连接到您的 Su
|
||||
| `projectId` | string | 是 | 您的 Supabase 项目 ID \(例如:jdrkgepadsdopsntdlom\) |
|
||||
| `table` | string | 是 | 要查询的 Supabase 表名 |
|
||||
| `schema` | string | 否 | 要查询的数据库 schema \(默认:public\)。用于访问其他 schema 下的表。|
|
||||
| `filter` | string | 是 | 用于查找特定行的 PostgREST 过滤条件 \(例如:"id=eq.123"\) |
|
||||
| `select` | string | 否 | 要返回的列(逗号分隔)。默认为 *(所有列)|
|
||||
| `filter` | string | 是 | PostgREST 过滤条件,用于查找特定行 \(例如:"id=eq.123"\) |
|
||||
| `apiKey` | string | 是 | 您的 Supabase 服务角色密钥 |
|
||||
|
||||
#### 输出
|
||||
|
||||
@@ -15,7 +15,7 @@ Webhook 允许外部服务通过向您的工作流发送 HTTP 请求来触发工
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Image
|
||||
src="/static/blocks/webhook.png"
|
||||
src="/static/blocks/webhook-trigger.png"
|
||||
alt="通用 Webhook 配置"
|
||||
width={500}
|
||||
height={400}
|
||||
|
||||
@@ -169,7 +169,7 @@ checksums:
|
||||
content/1: 9d1b6de2021f809cc43502d19a19bd15
|
||||
content/2: f4c40c45a45329eca670aca4fcece6f3
|
||||
content/3: b03a97486cc185beb7b51644b548875a
|
||||
content/4: a77222cf7a57362fc7eb5ebf7cc652c6
|
||||
content/4: 01c24bef59948dbecc1ae19794019d5f
|
||||
content/5: ba18ac99184b17d7e49bd1abdc814437
|
||||
content/6: 171c4e97e509427ca63acccf136779b3
|
||||
content/7: 98e1babdd0136267807b7e94ae7da6c7
|
||||
@@ -700,7 +700,7 @@ checksums:
|
||||
content/11: 04bd9805ef6a50af8469463c34486dbf
|
||||
content/12: a3671dd7ba76a87dc75464d9bf9b7b4b
|
||||
content/13: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/14: 80578981b8b3a1cf579e52ff05e7468d
|
||||
content/14: 5102b3705883f9e0c5440aeabafd1d24
|
||||
content/15: bcadfc362b69078beee0088e5936c98b
|
||||
content/16: 09ed43219d02501c829594dbf4128959
|
||||
content/17: 88ae2285d728c80937e1df8194d92c60
|
||||
@@ -712,7 +712,7 @@ checksums:
|
||||
content/23: 7d96d99e45880195ccbd34bddaac6319
|
||||
content/24: 75d05f96dff406db06b338d9ab8d0bd7
|
||||
content/25: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/26: cfd801fa517b4bcfa5fa034b2c4e908a
|
||||
content/26: 38373ac018fd7db3a20ba5308beac81e
|
||||
content/27: bcadfc362b69078beee0088e5936c98b
|
||||
content/28: a0284632eb0a15e66f69479ec477c5b1
|
||||
content/29: b1e60734e590a8ad894a96581a253bf4
|
||||
@@ -48276,7 +48276,7 @@ checksums:
|
||||
content/35: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/36: bddd30707802c07aac61620721bfaf16
|
||||
content/37: bcadfc362b69078beee0088e5936c98b
|
||||
content/38: fa2c581e6fb204f5ddbd0ffcbf0f7123
|
||||
content/38: 4619dad6a45478396332397f1e53db85
|
||||
content/39: 65de097e276f762b71d59fa7f9b0a207
|
||||
content/40: 013f52c249b5919fdb6d96700b25f379
|
||||
content/41: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
@@ -50077,7 +50077,7 @@ checksums:
|
||||
content/68: b7afc8fa3b22ea9327e336f50b82a27c
|
||||
content/69: bcadfc362b69078beee0088e5936c98b
|
||||
content/70: 0337e5d7f0bad113be176419350a41b6
|
||||
content/71: ef61f2bab8cfd25a5228d9df3ff6cf3c
|
||||
content/71: bb403ace5373d843beffe220c9a8d618
|
||||
content/72: 35a991daf9336e6bba2bd8818dd66594
|
||||
content/73: 371d0e46b4bd2c23f559b8bc112f6955
|
||||
content/74: 13644af4a2d5aea5061e9945e91f5a4f
|
||||
@@ -50166,3 +50166,62 @@ checksums:
|
||||
content/27: 764eb0e5d025b68f772d45adb7608349
|
||||
content/28: 47eb215a0fc230dc651b7bc05ab25ed0
|
||||
content/29: bf5c6bf1e75c5c5e3a0a5dd1314cb41e
|
||||
ed03212dda9fce53ddf623d1c4587006:
|
||||
meta/title: ef00d7494b69def6841620bd6554d040
|
||||
meta/description: 4b66a56c6ccc3c7e630dfc45eb8bfdf8
|
||||
content/0: 232be69c8f3053a40f695f9c9dcb3f2e
|
||||
content/1: 0628b1e7f70de9f2b5dff99452111de9
|
||||
content/2: fa4a0821069063d96727598f379fb619
|
||||
content/3: a3825edbe4c255e7370624d27b734399
|
||||
content/4: 5be2f96951187cdbf39ed7d879322cef
|
||||
content/5: 4940f2e763be1990113195e4667ff49a
|
||||
content/6: 27c579ade1a1be3e514d880388c58c2b
|
||||
content/7: 125beef2eb1e60a492faa9dc03fca0b4
|
||||
content/8: 62d5214cb3e3ec863bd5b6d74e0df126
|
||||
content/9: 421b088722ccb029a93a2388cf47d9b3
|
||||
content/10: e9ddc04f492fea4fb96bfd7fcd3eb84a
|
||||
content/11: be8e3a9794f70b9c03373db88ffc43ce
|
||||
content/12: 3a322eee25c8bd5d81e7ae92f4239300
|
||||
content/13: a82eb7d47a82c3289a00ccf27a860685
|
||||
content/14: 26b9713de1a21d662c198154b673fd7d
|
||||
b92b25e42e07ea0c1acc84c25f897c03:
|
||||
meta/title: b578e5df9a37263d79d61eea1550b381
|
||||
meta/description: a3ac2f556f8a1d72eee5058799e45b4f
|
||||
content/0: 1b031fb0c62c46b177aeed5c3d3f8f80
|
||||
content/1: 03b3a9b927648526481589ec2205aed1
|
||||
content/2: ad3b7957de2e6e94935420692f41912b
|
||||
content/3: b5d66b7cc95f747232f3c39e71e58125
|
||||
content/4: a97fd9d5ca27813be7aa04fc9162ec5d
|
||||
content/5: 47006103fb87648dd28524557c946bd0
|
||||
content/6: 821e6394b0a953e2b0842b04ae8f3105
|
||||
content/7: 7b29d23aec8fda839f3934c5fc71c6d3
|
||||
content/8: b3f310d5ef115bea5a8b75bf25d7ea9a
|
||||
content/9: 79ecd09a7bedc128285814d8b439ed40
|
||||
2bf1f583bd3a431e459e5a0142a82efd:
|
||||
meta/title: 70f95b2c27f2c3840b500fcaf79ee83c
|
||||
content/0: eb0ed7078f192304703144f4cac3442f
|
||||
content/1: 1bc1f971556fb854666c22551215d3c2
|
||||
content/2: 5127a30fba20289720806082df2eae87
|
||||
content/3: 0441638444240cd20a6c69ea1d3afbb1
|
||||
content/4: 0b5805c0201ed427ba1b56b9814ee0cb
|
||||
content/5: cf5305db38e782a1001f5208cdf6b5f1
|
||||
content/6: 575a2fc0f65f0d24a9d75fac8e8bf5f8
|
||||
content/7: 1acea0b3685c12e5c3d73c7afa9c5582
|
||||
content/8: 4464a6c6f5ccc67b95309ba6399552e9
|
||||
content/9: 336794d9cf3e900c1b5aba0071944f1c
|
||||
content/10: bf46b631598a496c37560e074454f5ec
|
||||
content/11: 3d6a55b18007832eb2ed751638e968ca
|
||||
content/12: 3f97586d23efe56c4ab94c03a0b91706
|
||||
content/13: f2caee00e0e386a5e5257862209aaaef
|
||||
content/14: 15c9ed641ef776a33a945b6e0ddb908c
|
||||
content/15: db087c66ef8c0ab22775072b10655d05
|
||||
content/16: e148c1c6e1345e9ee95657c5ba40ebf4
|
||||
content/17: 9feca6cbb058fb8070b23d139d2d96e6
|
||||
content/18: 987932038f4e9442bd89f0f8ed3c5319
|
||||
content/19: 8e0258b3891544d355fa4a92f2ae96e4
|
||||
content/20: 9c2f91f89a914bf4661512275e461104
|
||||
content/21: a5cc8d50937a37d5ae7e921fc85a71f1
|
||||
content/22: 51b2fdf484e8d6b07cdf8434034dc872
|
||||
content/23: 59da7694b8be001fec8b9f9d7b604faf
|
||||
content/24: 8fb6954068c6687d44121e21a95cf1b6
|
||||
content/25: 9e7b1a1a453340d20adf4cacbd532018
|
||||
|
||||
BIN
apps/docs/public/static/blocks/webhook-trigger.png
Normal file
BIN
apps/docs/public/static/blocks/webhook-trigger.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 38 KiB |
@@ -20,7 +20,7 @@ interface NavProps {
|
||||
}
|
||||
|
||||
export default function Nav({ hideAuthButtons = false, variant = 'landing' }: NavProps = {}) {
|
||||
const [githubStars, setGithubStars] = useState('24.4k')
|
||||
const [githubStars, setGithubStars] = useState('25.1k')
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
const [isLoginHovered, setIsLoginHovered] = useState(false)
|
||||
const router = useRouter()
|
||||
|
||||
@@ -50,8 +50,8 @@
|
||||
@layer base {
|
||||
:root,
|
||||
.light {
|
||||
--bg: #fdfdfd; /* main canvas - neutral near-white */
|
||||
--surface-1: #fcfcfc; /* sidebar, panels */
|
||||
--bg: #fefefe; /* main canvas - neutral near-white */
|
||||
--surface-1: #fefefe; /* sidebar, panels */
|
||||
--surface-2: #ffffff; /* blocks, cards, modals - pure white */
|
||||
--surface-3: #f7f7f7; /* popovers, headers */
|
||||
--surface-4: #f5f5f5; /* buttons base */
|
||||
@@ -70,6 +70,7 @@
|
||||
--text-muted: #737373;
|
||||
--text-subtle: #8c8c8c;
|
||||
--text-inverse: #ffffff;
|
||||
--text-muted-inverse: #a0a0a0;
|
||||
--text-error: #ef4444;
|
||||
|
||||
/* Borders / dividers */
|
||||
@@ -186,6 +187,7 @@
|
||||
--text-muted: #787878;
|
||||
--text-subtle: #7d7d7d;
|
||||
--text-inverse: #1b1b1b;
|
||||
--text-muted-inverse: #b3b3b3;
|
||||
--text-error: #ef4444;
|
||||
|
||||
/* --border-strong: #303030; */
|
||||
@@ -331,38 +333,38 @@
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--surface-1);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: var(--surface-7);
|
||||
background-color: #c0c0c0;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--surface-7);
|
||||
background-color: #a8a8a8;
|
||||
}
|
||||
|
||||
/* Dark Mode Global Scrollbar */
|
||||
.dark ::-webkit-scrollbar-track {
|
||||
background: var(--surface-4);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.dark ::-webkit-scrollbar-thumb {
|
||||
background-color: var(--surface-7);
|
||||
background-color: #5a5a5a;
|
||||
}
|
||||
|
||||
.dark ::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--surface-7);
|
||||
background-color: #6a6a6a;
|
||||
}
|
||||
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--surface-7) var(--surface-1);
|
||||
scrollbar-color: #c0c0c0 transparent;
|
||||
}
|
||||
|
||||
.dark * {
|
||||
scrollbar-color: var(--surface-7) var(--surface-4);
|
||||
scrollbar-color: #5a5a5a transparent;
|
||||
}
|
||||
|
||||
.copilot-scrollable {
|
||||
|
||||
@@ -2,8 +2,7 @@ import { render } from '@react-email/components'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import CareersConfirmationEmail from '@/components/emails/careers/careers-confirmation-email'
|
||||
import CareersSubmissionEmail from '@/components/emails/careers/careers-submission-email'
|
||||
import { CareersConfirmationEmail, CareersSubmissionEmail } from '@/components/emails'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { sendEmail } from '@/lib/messaging/email/mailer'
|
||||
|
||||
|
||||
@@ -156,6 +156,11 @@ describe('Chat OTP API Route', () => {
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/core/config/env', async () => {
|
||||
const { createEnvMock } = await import('@sim/testing')
|
||||
return createEnvMock()
|
||||
})
|
||||
|
||||
vi.doMock('zod', () => ({
|
||||
z: {
|
||||
object: vi.fn().mockReturnValue({
|
||||
|
||||
@@ -5,7 +5,7 @@ import { createLogger } from '@sim/logger'
|
||||
import { and, eq, gt } from 'drizzle-orm'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { renderOTPEmail } from '@/components/emails/render-email'
|
||||
import { renderOTPEmail } from '@/components/emails'
|
||||
import { getRedisClient } from '@/lib/core/config/redis'
|
||||
import { getStorageMethod } from '@/lib/core/storage'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
|
||||
@@ -249,17 +249,13 @@ describe('Chat API Route', () => {
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/core/config/env', () => ({
|
||||
env: {
|
||||
vi.doMock('@/lib/core/config/env', async () => {
|
||||
const { createEnvMock } = await import('@sim/testing')
|
||||
return createEnvMock({
|
||||
NODE_ENV: 'development',
|
||||
NEXT_PUBLIC_APP_URL: 'http://localhost:3000',
|
||||
},
|
||||
isTruthy: (value: string | boolean | number | undefined) =>
|
||||
typeof value === 'string'
|
||||
? value.toLowerCase() === 'true' || value === '1'
|
||||
: Boolean(value),
|
||||
getEnv: (variable: string) => process.env[variable],
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
const validData = {
|
||||
workflowId: 'workflow-123',
|
||||
@@ -296,15 +292,13 @@ describe('Chat API Route', () => {
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/core/config/env', () => ({
|
||||
env: {
|
||||
vi.doMock('@/lib/core/config/env', async () => {
|
||||
const { createEnvMock } = await import('@sim/testing')
|
||||
return createEnvMock({
|
||||
NODE_ENV: 'development',
|
||||
NEXT_PUBLIC_APP_URL: 'http://localhost:3000',
|
||||
},
|
||||
isTruthy: (value: string | boolean | number | undefined) =>
|
||||
typeof value === 'string' ? value === 'true' || value === '1' : Boolean(value),
|
||||
getEnv: (variable: string) => process.env[variable],
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
const validData = {
|
||||
workflowId: 'workflow-123',
|
||||
|
||||
@@ -21,12 +21,13 @@ describe('Copilot API Keys API Route', () => {
|
||||
SIM_AGENT_API_URL_DEFAULT: 'https://agent.sim.example.com',
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/core/config/env', () => ({
|
||||
env: {
|
||||
SIM_AGENT_API_URL: null,
|
||||
vi.doMock('@/lib/core/config/env', async () => {
|
||||
const { createEnvMock } = await import('@sim/testing')
|
||||
return createEnvMock({
|
||||
SIM_AGENT_API_URL: undefined,
|
||||
COPILOT_API_KEY: 'test-api-key',
|
||||
},
|
||||
}))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -46,12 +46,13 @@ describe('Copilot Stats API Route', () => {
|
||||
SIM_AGENT_API_URL_DEFAULT: 'https://agent.sim.example.com',
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/core/config/env', () => ({
|
||||
env: {
|
||||
SIM_AGENT_API_URL: null,
|
||||
vi.doMock('@/lib/core/config/env', async () => {
|
||||
const { createEnvMock } = await import('@sim/testing')
|
||||
return createEnvMock({
|
||||
SIM_AGENT_API_URL: undefined,
|
||||
COPILOT_API_KEY: 'test-api-key',
|
||||
},
|
||||
}))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
90
apps/sim/app/api/cron/cleanup-stale-executions/route.ts
Normal file
90
apps/sim/app/api/cron/cleanup-stale-executions/route.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { db } from '@sim/db'
|
||||
import { workflowExecutionLogs } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, eq, lt, sql } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { verifyCronAuth } from '@/lib/auth/internal'
|
||||
|
||||
const logger = createLogger('CleanupStaleExecutions')
|
||||
|
||||
const STALE_THRESHOLD_MINUTES = 30
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const authError = verifyCronAuth(request, 'Stale execution cleanup')
|
||||
if (authError) {
|
||||
return authError
|
||||
}
|
||||
|
||||
logger.info('Starting stale execution cleanup job')
|
||||
|
||||
const staleThreshold = new Date(Date.now() - STALE_THRESHOLD_MINUTES * 60 * 1000)
|
||||
|
||||
const staleExecutions = await db
|
||||
.select({
|
||||
id: workflowExecutionLogs.id,
|
||||
executionId: workflowExecutionLogs.executionId,
|
||||
workflowId: workflowExecutionLogs.workflowId,
|
||||
startedAt: workflowExecutionLogs.startedAt,
|
||||
})
|
||||
.from(workflowExecutionLogs)
|
||||
.where(
|
||||
and(
|
||||
eq(workflowExecutionLogs.status, 'running'),
|
||||
lt(workflowExecutionLogs.startedAt, staleThreshold)
|
||||
)
|
||||
)
|
||||
.limit(100)
|
||||
|
||||
logger.info(`Found ${staleExecutions.length} stale executions to clean up`)
|
||||
|
||||
let cleaned = 0
|
||||
let failed = 0
|
||||
|
||||
for (const execution of staleExecutions) {
|
||||
try {
|
||||
const staleDurationMs = Date.now() - new Date(execution.startedAt).getTime()
|
||||
const staleDurationMinutes = Math.round(staleDurationMs / 60000)
|
||||
|
||||
await db
|
||||
.update(workflowExecutionLogs)
|
||||
.set({
|
||||
status: 'failed',
|
||||
endedAt: new Date(),
|
||||
totalDurationMs: staleDurationMs,
|
||||
executionData: sql`jsonb_set(
|
||||
COALESCE(execution_data, '{}'::jsonb),
|
||||
ARRAY['error'],
|
||||
to_jsonb(${`Execution terminated: worker timeout or crash after ${staleDurationMinutes} minutes`}::text)
|
||||
)`,
|
||||
})
|
||||
.where(eq(workflowExecutionLogs.id, execution.id))
|
||||
|
||||
logger.info(`Cleaned up stale execution ${execution.executionId}`, {
|
||||
workflowId: execution.workflowId,
|
||||
staleDurationMinutes,
|
||||
})
|
||||
|
||||
cleaned++
|
||||
} catch (error) {
|
||||
logger.error(`Failed to clean up execution ${execution.executionId}:`, {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
})
|
||||
failed++
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Stale execution cleanup completed. Cleaned: ${cleaned}, Failed: ${failed}`)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
found: staleExecutions.length,
|
||||
cleaned,
|
||||
failed,
|
||||
thresholdMinutes: STALE_THRESHOLD_MINUTES,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error in stale execution cleanup job:', error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
176
apps/sim/app/api/emails/preview/route.ts
Normal file
176
apps/sim/app/api/emails/preview/route.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
import {
|
||||
renderBatchInvitationEmail,
|
||||
renderCareersConfirmationEmail,
|
||||
renderCareersSubmissionEmail,
|
||||
renderCreditPurchaseEmail,
|
||||
renderEnterpriseSubscriptionEmail,
|
||||
renderFreeTierUpgradeEmail,
|
||||
renderHelpConfirmationEmail,
|
||||
renderInvitationEmail,
|
||||
renderOTPEmail,
|
||||
renderPasswordResetEmail,
|
||||
renderPaymentFailedEmail,
|
||||
renderPlanWelcomeEmail,
|
||||
renderUsageThresholdEmail,
|
||||
renderWelcomeEmail,
|
||||
renderWorkspaceInvitationEmail,
|
||||
} from '@/components/emails'
|
||||
|
||||
const emailTemplates = {
|
||||
// Auth emails
|
||||
otp: () => renderOTPEmail('123456', 'user@example.com', 'email-verification'),
|
||||
'reset-password': () => renderPasswordResetEmail('John', 'https://sim.ai/reset?token=abc123'),
|
||||
welcome: () => renderWelcomeEmail('John'),
|
||||
|
||||
// Invitation emails
|
||||
invitation: () => renderInvitationEmail('Jane Doe', 'Acme Corp', 'https://sim.ai/invite/abc123'),
|
||||
'batch-invitation': () =>
|
||||
renderBatchInvitationEmail(
|
||||
'Jane Doe',
|
||||
'Acme Corp',
|
||||
'admin',
|
||||
[
|
||||
{ workspaceId: 'ws_123', workspaceName: 'Engineering', permission: 'write' },
|
||||
{ workspaceId: 'ws_456', workspaceName: 'Design', permission: 'read' },
|
||||
],
|
||||
'https://sim.ai/invite/abc123'
|
||||
),
|
||||
'workspace-invitation': () =>
|
||||
renderWorkspaceInvitationEmail(
|
||||
'John Smith',
|
||||
'Engineering Team',
|
||||
'https://sim.ai/workspace/invite/abc123'
|
||||
),
|
||||
|
||||
// Support emails
|
||||
'help-confirmation': () => renderHelpConfirmationEmail('feature_request', 2),
|
||||
|
||||
// Billing emails
|
||||
'usage-threshold': () =>
|
||||
renderUsageThresholdEmail({
|
||||
userName: 'John',
|
||||
planName: 'Pro',
|
||||
percentUsed: 75,
|
||||
currentUsage: 15,
|
||||
limit: 20,
|
||||
ctaLink: 'https://sim.ai/settings/billing',
|
||||
}),
|
||||
'enterprise-subscription': () => renderEnterpriseSubscriptionEmail('John'),
|
||||
'free-tier-upgrade': () =>
|
||||
renderFreeTierUpgradeEmail({
|
||||
userName: 'John',
|
||||
percentUsed: 90,
|
||||
currentUsage: 9,
|
||||
limit: 10,
|
||||
upgradeLink: 'https://sim.ai/settings/billing',
|
||||
}),
|
||||
'plan-welcome-pro': () =>
|
||||
renderPlanWelcomeEmail({
|
||||
planName: 'Pro',
|
||||
userName: 'John',
|
||||
loginLink: 'https://sim.ai/login',
|
||||
}),
|
||||
'plan-welcome-team': () =>
|
||||
renderPlanWelcomeEmail({
|
||||
planName: 'Team',
|
||||
userName: 'John',
|
||||
loginLink: 'https://sim.ai/login',
|
||||
}),
|
||||
'credit-purchase': () =>
|
||||
renderCreditPurchaseEmail({
|
||||
userName: 'John',
|
||||
amount: 50,
|
||||
newBalance: 75,
|
||||
}),
|
||||
'payment-failed': () =>
|
||||
renderPaymentFailedEmail({
|
||||
userName: 'John',
|
||||
amountDue: 20,
|
||||
lastFourDigits: '4242',
|
||||
billingPortalUrl: 'https://sim.ai/settings/billing',
|
||||
failureReason: 'Card declined',
|
||||
}),
|
||||
|
||||
// Careers emails
|
||||
'careers-confirmation': () => renderCareersConfirmationEmail('John Doe', 'Senior Engineer'),
|
||||
'careers-submission': () =>
|
||||
renderCareersSubmissionEmail({
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
phone: '+1 (555) 123-4567',
|
||||
position: 'Senior Engineer',
|
||||
linkedin: 'https://linkedin.com/in/johndoe',
|
||||
portfolio: 'https://johndoe.dev',
|
||||
experience: '5-10',
|
||||
location: 'San Francisco, CA',
|
||||
message:
|
||||
'I have 10 years of experience building scalable distributed systems. Most recently, I led a team at a Series B startup where we scaled from 100K to 10M users.',
|
||||
}),
|
||||
} as const
|
||||
|
||||
type EmailTemplate = keyof typeof emailTemplates
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const template = searchParams.get('template') as EmailTemplate | null
|
||||
|
||||
if (!template) {
|
||||
const categories = {
|
||||
Auth: ['otp', 'reset-password', 'welcome'],
|
||||
Invitations: ['invitation', 'batch-invitation', 'workspace-invitation'],
|
||||
Support: ['help-confirmation'],
|
||||
Billing: [
|
||||
'usage-threshold',
|
||||
'enterprise-subscription',
|
||||
'free-tier-upgrade',
|
||||
'plan-welcome-pro',
|
||||
'plan-welcome-team',
|
||||
'credit-purchase',
|
||||
'payment-failed',
|
||||
],
|
||||
Careers: ['careers-confirmation', 'careers-submission'],
|
||||
}
|
||||
|
||||
const categoryHtml = Object.entries(categories)
|
||||
.map(
|
||||
([category, templates]) => `
|
||||
<h2 style="margin-top: 24px; margin-bottom: 12px; font-size: 14px; color: #666; text-transform: uppercase; letter-spacing: 0.5px;">${category}</h2>
|
||||
<ul style="list-style: none; padding: 0; margin: 0;">
|
||||
${templates.map((t) => `<li style="margin: 8px 0;"><a href="?template=${t}" style="color: #32bd7e; text-decoration: none; font-size: 16px;">${t}</a></li>`).join('')}
|
||||
</ul>
|
||||
`
|
||||
)
|
||||
.join('')
|
||||
|
||||
return new NextResponse(
|
||||
`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Email Previews</title>
|
||||
<style>
|
||||
body { font-family: system-ui, -apple-system, sans-serif; max-width: 600px; margin: 40px auto; padding: 20px; }
|
||||
h1 { color: #333; margin-bottom: 32px; }
|
||||
a:hover { text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Email Templates</h1>
|
||||
${categoryHtml}
|
||||
</body>
|
||||
</html>`,
|
||||
{ headers: { 'Content-Type': 'text/html' } }
|
||||
)
|
||||
}
|
||||
|
||||
if (!(template in emailTemplates)) {
|
||||
return NextResponse.json({ error: `Unknown template: ${template}` }, { status: 400 })
|
||||
}
|
||||
|
||||
const html = await emailTemplates[template]()
|
||||
|
||||
return new NextResponse(html, {
|
||||
headers: { 'Content-Type': 'text/html' },
|
||||
})
|
||||
}
|
||||
@@ -118,7 +118,6 @@ ${message}
|
||||
// Send confirmation email to the user
|
||||
try {
|
||||
const confirmationHtml = await renderHelpConfirmationEmail(
|
||||
email,
|
||||
type as 'bug' | 'feedback' | 'feature_request' | 'other',
|
||||
images.length
|
||||
)
|
||||
|
||||
@@ -136,16 +136,29 @@ vi.mock('@sim/db', () => {
|
||||
},
|
||||
}),
|
||||
}),
|
||||
delete: () => ({
|
||||
where: () => Promise.resolve(),
|
||||
}),
|
||||
insert: () => ({
|
||||
values: (records: any) => {
|
||||
dbOps.order.push('insert')
|
||||
dbOps.insertRecords.push(records)
|
||||
return Promise.resolve()
|
||||
},
|
||||
}),
|
||||
transaction: vi.fn(async (fn: any) => {
|
||||
await fn({
|
||||
insert: (table: any) => ({
|
||||
delete: () => ({
|
||||
where: () => Promise.resolve(),
|
||||
}),
|
||||
insert: () => ({
|
||||
values: (records: any) => {
|
||||
dbOps.order.push('insert')
|
||||
dbOps.insertRecords.push(records)
|
||||
return Promise.resolve()
|
||||
},
|
||||
}),
|
||||
update: (table: any) => ({
|
||||
update: () => ({
|
||||
set: (payload: any) => ({
|
||||
where: () => {
|
||||
dbOps.updatePayloads.push(payload)
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
getEmailSubject,
|
||||
renderBatchInvitationEmail,
|
||||
renderInvitationEmail,
|
||||
} from '@/components/emails/render-email'
|
||||
} from '@/components/emails'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import {
|
||||
validateBulkInvitations,
|
||||
@@ -376,8 +376,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
const emailHtml = await renderInvitationEmail(
|
||||
inviter[0]?.name || 'Someone',
|
||||
organizationEntry[0]?.name || 'organization',
|
||||
`${getBaseUrl()}/invite/${orgInvitation.id}`,
|
||||
email
|
||||
`${getBaseUrl()}/invite/${orgInvitation.id}`
|
||||
)
|
||||
|
||||
emailResult = await sendEmail({
|
||||
|
||||
@@ -4,7 +4,7 @@ import { invitation, member, organization, user, userStats } from '@sim/db/schem
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getEmailSubject, renderInvitationEmail } from '@/components/emails/render-email'
|
||||
import { getEmailSubject, renderInvitationEmail } from '@/components/emails'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { getUserUsageData } from '@/lib/billing/core/usage'
|
||||
import { validateSeatAvailability } from '@/lib/billing/validation/seat-management'
|
||||
@@ -260,8 +260,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
const emailHtml = await renderInvitationEmail(
|
||||
inviter[0]?.name || 'Someone',
|
||||
organizationEntry[0]?.name || 'organization',
|
||||
`${getBaseUrl()}/invite/organization?id=${invitationId}`,
|
||||
normalizedEmail
|
||||
`${getBaseUrl()}/invite/organization?id=${invitationId}`
|
||||
)
|
||||
|
||||
const emailResult = await sendEmail({
|
||||
|
||||
@@ -21,14 +21,15 @@ export async function POST(
|
||||
) {
|
||||
const { workflowId, executionId, contextId } = await params
|
||||
|
||||
// Allow resume from dashboard without requiring deployment
|
||||
const access = await validateWorkflowAccess(request, workflowId, false)
|
||||
if (access.error) {
|
||||
return NextResponse.json({ error: access.error.message }, { status: access.error.status })
|
||||
}
|
||||
|
||||
const workflow = access.workflow!
|
||||
const workflow = access.workflow
|
||||
|
||||
let payload: any = {}
|
||||
let payload: Record<string, unknown> = {}
|
||||
try {
|
||||
payload = await request.json()
|
||||
} catch {
|
||||
@@ -148,6 +149,7 @@ export async function GET(
|
||||
) {
|
||||
const { workflowId, executionId, contextId } = await params
|
||||
|
||||
// Allow access without API key for browser-based UI (same as parent execution endpoint)
|
||||
const access = await validateWorkflowAccess(request, workflowId, false)
|
||||
if (access.error) {
|
||||
return NextResponse.json({ error: access.error.message }, { status: access.error.status })
|
||||
|
||||
101
apps/sim/app/api/tools/imap/mailboxes/route.ts
Normal file
101
apps/sim/app/api/tools/imap/mailboxes/route.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { ImapFlow } from 'imapflow'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
|
||||
const logger = createLogger('ImapMailboxesAPI')
|
||||
|
||||
interface ImapMailboxRequest {
|
||||
host: string
|
||||
port: number
|
||||
secure: boolean
|
||||
rejectUnauthorized: boolean
|
||||
username: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ success: false, message: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const body = (await request.json()) as ImapMailboxRequest
|
||||
const { host, port, secure, rejectUnauthorized, username, password } = body
|
||||
|
||||
if (!host || !username || !password) {
|
||||
return NextResponse.json(
|
||||
{ success: false, message: 'Missing required fields: host, username, password' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const client = new ImapFlow({
|
||||
host,
|
||||
port: port || 993,
|
||||
secure: secure ?? true,
|
||||
auth: {
|
||||
user: username,
|
||||
pass: password,
|
||||
},
|
||||
tls: {
|
||||
rejectUnauthorized: rejectUnauthorized ?? true,
|
||||
},
|
||||
logger: false,
|
||||
})
|
||||
|
||||
try {
|
||||
await client.connect()
|
||||
|
||||
const listResult = await client.list()
|
||||
const mailboxes = listResult.map((mailbox) => ({
|
||||
path: mailbox.path,
|
||||
name: mailbox.name,
|
||||
delimiter: mailbox.delimiter,
|
||||
}))
|
||||
|
||||
await client.logout()
|
||||
|
||||
mailboxes.sort((a, b) => {
|
||||
if (a.path === 'INBOX') return -1
|
||||
if (b.path === 'INBOX') return 1
|
||||
return a.path.localeCompare(b.path)
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
mailboxes,
|
||||
})
|
||||
} catch (error) {
|
||||
try {
|
||||
await client.logout()
|
||||
} catch {
|
||||
// Ignore logout errors
|
||||
}
|
||||
throw error
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
||||
logger.error('Error fetching IMAP mailboxes:', errorMessage)
|
||||
|
||||
let userMessage = 'Failed to connect to IMAP server'
|
||||
if (
|
||||
errorMessage.includes('AUTHENTICATIONFAILED') ||
|
||||
errorMessage.includes('Invalid credentials')
|
||||
) {
|
||||
userMessage = 'Invalid username or password. For Gmail, use an App Password.'
|
||||
} else if (errorMessage.includes('ENOTFOUND') || errorMessage.includes('getaddrinfo')) {
|
||||
userMessage = 'Could not find IMAP server. Please check the hostname.'
|
||||
} else if (errorMessage.includes('ECONNREFUSED')) {
|
||||
userMessage = 'Connection refused. Please check the port and SSL settings.'
|
||||
} else if (errorMessage.includes('certificate') || errorMessage.includes('SSL')) {
|
||||
userMessage =
|
||||
'TLS/SSL error. Try disabling "Verify TLS Certificate" for self-signed certificates.'
|
||||
} else if (errorMessage.includes('timeout')) {
|
||||
userMessage = 'Connection timed out. Please check your network and server settings.'
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: false, message: userMessage }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,10 @@
|
||||
* DELETE /api/v1/admin/workflows/:id - Delete workflow
|
||||
* GET /api/v1/admin/workflows/:id/export - Export workflow (JSON)
|
||||
* POST /api/v1/admin/workflows/import - Import single workflow
|
||||
* POST /api/v1/admin/workflows/:id/deploy - Deploy workflow
|
||||
* DELETE /api/v1/admin/workflows/:id/deploy - Undeploy workflow
|
||||
* GET /api/v1/admin/workflows/:id/versions - List deployment versions
|
||||
* POST /api/v1/admin/workflows/:id/versions/:vid/activate - Activate specific version
|
||||
*
|
||||
* Organizations:
|
||||
* GET /api/v1/admin/organizations - List all organizations
|
||||
@@ -65,6 +69,8 @@ export {
|
||||
unauthorizedResponse,
|
||||
} from '@/app/api/v1/admin/responses'
|
||||
export type {
|
||||
AdminDeploymentVersion,
|
||||
AdminDeployResult,
|
||||
AdminErrorResponse,
|
||||
AdminFolder,
|
||||
AdminListResponse,
|
||||
@@ -76,6 +82,7 @@ export type {
|
||||
AdminSeatAnalytics,
|
||||
AdminSingleResponse,
|
||||
AdminSubscription,
|
||||
AdminUndeployResult,
|
||||
AdminUser,
|
||||
AdminUserBilling,
|
||||
AdminUserBillingWithSubscription,
|
||||
|
||||
@@ -599,3 +599,23 @@ export interface AdminSeatAnalytics {
|
||||
lastActive: string | null
|
||||
}>
|
||||
}
|
||||
|
||||
export interface AdminDeploymentVersion {
|
||||
id: string
|
||||
version: number
|
||||
name: string | null
|
||||
isActive: boolean
|
||||
createdAt: string
|
||||
createdBy: string | null
|
||||
deployedByName: string | null
|
||||
}
|
||||
|
||||
export interface AdminDeployResult {
|
||||
isDeployed: boolean
|
||||
version: number
|
||||
deployedAt: string
|
||||
}
|
||||
|
||||
export interface AdminUndeployResult {
|
||||
isDeployed: boolean
|
||||
}
|
||||
|
||||
111
apps/sim/app/api/v1/admin/workflows/[id]/deploy/route.ts
Normal file
111
apps/sim/app/api/v1/admin/workflows/[id]/deploy/route.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { db, workflow } from '@sim/db'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import {
|
||||
deployWorkflow,
|
||||
loadWorkflowFromNormalizedTables,
|
||||
undeployWorkflow,
|
||||
} from '@/lib/workflows/persistence/utils'
|
||||
import { createSchedulesForDeploy, validateWorkflowSchedules } from '@/lib/workflows/schedules'
|
||||
import { withAdminAuthParams } from '@/app/api/v1/admin/middleware'
|
||||
import {
|
||||
badRequestResponse,
|
||||
internalErrorResponse,
|
||||
notFoundResponse,
|
||||
singleResponse,
|
||||
} from '@/app/api/v1/admin/responses'
|
||||
import type { AdminDeployResult, AdminUndeployResult } from '@/app/api/v1/admin/types'
|
||||
|
||||
const logger = createLogger('AdminWorkflowDeployAPI')
|
||||
|
||||
const ADMIN_ACTOR_ID = 'admin-api'
|
||||
|
||||
interface RouteParams {
|
||||
id: string
|
||||
}
|
||||
|
||||
export const POST = withAdminAuthParams<RouteParams>(async (request, context) => {
|
||||
const { id: workflowId } = await context.params
|
||||
|
||||
try {
|
||||
const [workflowRecord] = await db
|
||||
.select({ id: workflow.id, name: workflow.name })
|
||||
.from(workflow)
|
||||
.where(eq(workflow.id, workflowId))
|
||||
.limit(1)
|
||||
|
||||
if (!workflowRecord) {
|
||||
return notFoundResponse('Workflow')
|
||||
}
|
||||
|
||||
const normalizedData = await loadWorkflowFromNormalizedTables(workflowId)
|
||||
if (!normalizedData) {
|
||||
return badRequestResponse('Workflow has no saved state')
|
||||
}
|
||||
|
||||
const scheduleValidation = validateWorkflowSchedules(normalizedData.blocks)
|
||||
if (!scheduleValidation.isValid) {
|
||||
return badRequestResponse(`Invalid schedule configuration: ${scheduleValidation.error}`)
|
||||
}
|
||||
|
||||
const deployResult = await deployWorkflow({
|
||||
workflowId,
|
||||
deployedBy: ADMIN_ACTOR_ID,
|
||||
workflowName: workflowRecord.name,
|
||||
})
|
||||
|
||||
if (!deployResult.success) {
|
||||
return internalErrorResponse(deployResult.error || 'Failed to deploy workflow')
|
||||
}
|
||||
|
||||
const scheduleResult = await createSchedulesForDeploy(workflowId, normalizedData.blocks, db)
|
||||
if (!scheduleResult.success) {
|
||||
logger.warn(`Schedule creation failed for workflow ${workflowId}: ${scheduleResult.error}`)
|
||||
}
|
||||
|
||||
logger.info(`Admin API: Deployed workflow ${workflowId} as v${deployResult.version}`)
|
||||
|
||||
const response: AdminDeployResult = {
|
||||
isDeployed: true,
|
||||
version: deployResult.version!,
|
||||
deployedAt: deployResult.deployedAt!.toISOString(),
|
||||
}
|
||||
|
||||
return singleResponse(response)
|
||||
} catch (error) {
|
||||
logger.error(`Admin API: Failed to deploy workflow ${workflowId}`, { error })
|
||||
return internalErrorResponse('Failed to deploy workflow')
|
||||
}
|
||||
})
|
||||
|
||||
export const DELETE = withAdminAuthParams<RouteParams>(async (request, context) => {
|
||||
const { id: workflowId } = await context.params
|
||||
|
||||
try {
|
||||
const [workflowRecord] = await db
|
||||
.select({ id: workflow.id })
|
||||
.from(workflow)
|
||||
.where(eq(workflow.id, workflowId))
|
||||
.limit(1)
|
||||
|
||||
if (!workflowRecord) {
|
||||
return notFoundResponse('Workflow')
|
||||
}
|
||||
|
||||
const result = await undeployWorkflow({ workflowId })
|
||||
if (!result.success) {
|
||||
return internalErrorResponse(result.error || 'Failed to undeploy workflow')
|
||||
}
|
||||
|
||||
logger.info(`Admin API: Undeployed workflow ${workflowId}`)
|
||||
|
||||
const response: AdminUndeployResult = {
|
||||
isDeployed: false,
|
||||
}
|
||||
|
||||
return singleResponse(response)
|
||||
} catch (error) {
|
||||
logger.error(`Admin API: Failed to undeploy workflow ${workflowId}`, { error })
|
||||
return internalErrorResponse('Failed to undeploy workflow')
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,58 @@
|
||||
import { db, workflow } from '@sim/db'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { activateWorkflowVersion } from '@/lib/workflows/persistence/utils'
|
||||
import { withAdminAuthParams } from '@/app/api/v1/admin/middleware'
|
||||
import {
|
||||
badRequestResponse,
|
||||
internalErrorResponse,
|
||||
notFoundResponse,
|
||||
singleResponse,
|
||||
} from '@/app/api/v1/admin/responses'
|
||||
|
||||
const logger = createLogger('AdminWorkflowActivateVersionAPI')
|
||||
|
||||
interface RouteParams {
|
||||
id: string
|
||||
versionId: string
|
||||
}
|
||||
|
||||
export const POST = withAdminAuthParams<RouteParams>(async (request, context) => {
|
||||
const { id: workflowId, versionId } = await context.params
|
||||
|
||||
try {
|
||||
const [workflowRecord] = await db
|
||||
.select({ id: workflow.id })
|
||||
.from(workflow)
|
||||
.where(eq(workflow.id, workflowId))
|
||||
.limit(1)
|
||||
|
||||
if (!workflowRecord) {
|
||||
return notFoundResponse('Workflow')
|
||||
}
|
||||
|
||||
const versionNum = Number(versionId)
|
||||
if (!Number.isFinite(versionNum) || versionNum < 1) {
|
||||
return badRequestResponse('Invalid version number')
|
||||
}
|
||||
|
||||
const result = await activateWorkflowVersion({ workflowId, version: versionNum })
|
||||
if (!result.success) {
|
||||
if (result.error === 'Deployment version not found') {
|
||||
return notFoundResponse('Deployment version')
|
||||
}
|
||||
return internalErrorResponse(result.error || 'Failed to activate version')
|
||||
}
|
||||
|
||||
logger.info(`Admin API: Activated version ${versionNum} for workflow ${workflowId}`)
|
||||
|
||||
return singleResponse({
|
||||
success: true,
|
||||
version: versionNum,
|
||||
deployedAt: result.deployedAt!.toISOString(),
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error(`Admin API: Failed to activate version for workflow ${workflowId}`, { error })
|
||||
return internalErrorResponse('Failed to activate deployment version')
|
||||
}
|
||||
})
|
||||
52
apps/sim/app/api/v1/admin/workflows/[id]/versions/route.ts
Normal file
52
apps/sim/app/api/v1/admin/workflows/[id]/versions/route.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { db, workflow } from '@sim/db'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { listWorkflowVersions } from '@/lib/workflows/persistence/utils'
|
||||
import { withAdminAuthParams } from '@/app/api/v1/admin/middleware'
|
||||
import {
|
||||
internalErrorResponse,
|
||||
notFoundResponse,
|
||||
singleResponse,
|
||||
} from '@/app/api/v1/admin/responses'
|
||||
import type { AdminDeploymentVersion } from '@/app/api/v1/admin/types'
|
||||
|
||||
const logger = createLogger('AdminWorkflowVersionsAPI')
|
||||
|
||||
interface RouteParams {
|
||||
id: string
|
||||
}
|
||||
|
||||
export const GET = withAdminAuthParams<RouteParams>(async (request, context) => {
|
||||
const { id: workflowId } = await context.params
|
||||
|
||||
try {
|
||||
const [workflowRecord] = await db
|
||||
.select({ id: workflow.id })
|
||||
.from(workflow)
|
||||
.where(eq(workflow.id, workflowId))
|
||||
.limit(1)
|
||||
|
||||
if (!workflowRecord) {
|
||||
return notFoundResponse('Workflow')
|
||||
}
|
||||
|
||||
const { versions } = await listWorkflowVersions(workflowId)
|
||||
|
||||
const response: AdminDeploymentVersion[] = versions.map((v) => ({
|
||||
id: v.id,
|
||||
version: v.version,
|
||||
name: v.name,
|
||||
isActive: v.isActive,
|
||||
createdAt: v.createdAt.toISOString(),
|
||||
createdBy: v.createdBy,
|
||||
deployedByName: v.deployedByName ?? (v.createdBy === 'admin-api' ? 'Admin' : null),
|
||||
}))
|
||||
|
||||
logger.info(`Admin API: Listed ${versions.length} versions for workflow ${workflowId}`)
|
||||
|
||||
return singleResponse({ versions: response })
|
||||
} catch (error) {
|
||||
logger.error(`Admin API: Failed to list versions for workflow ${workflowId}`, { error })
|
||||
return internalErrorResponse('Failed to list deployment versions')
|
||||
}
|
||||
})
|
||||
68
apps/sim/app/api/webhooks/poll/imap/route.ts
Normal file
68
apps/sim/app/api/webhooks/poll/imap/route.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { verifyCronAuth } from '@/lib/auth/internal'
|
||||
import { acquireLock, releaseLock } from '@/lib/core/config/redis'
|
||||
import { pollImapWebhooks } from '@/lib/webhooks/imap-polling-service'
|
||||
|
||||
const logger = createLogger('ImapPollingAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
export const maxDuration = 180 // Allow up to 3 minutes for polling to complete
|
||||
|
||||
const LOCK_KEY = 'imap-polling-lock'
|
||||
const LOCK_TTL_SECONDS = 180 // Same as maxDuration (3 min)
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const requestId = nanoid()
|
||||
logger.info(`IMAP webhook polling triggered (${requestId})`)
|
||||
|
||||
let lockValue: string | undefined
|
||||
|
||||
try {
|
||||
const authError = verifyCronAuth(request, 'IMAP webhook polling')
|
||||
if (authError) {
|
||||
return authError
|
||||
}
|
||||
|
||||
lockValue = requestId // unique value to identify the holder
|
||||
const locked = await acquireLock(LOCK_KEY, lockValue, LOCK_TTL_SECONDS)
|
||||
|
||||
if (!locked) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
message: 'Polling already in progress – skipped',
|
||||
requestId,
|
||||
status: 'skip',
|
||||
},
|
||||
{ status: 202 }
|
||||
)
|
||||
}
|
||||
|
||||
const results = await pollImapWebhooks()
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'IMAP polling completed',
|
||||
requestId,
|
||||
status: 'completed',
|
||||
...results,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error(`Error during IMAP polling (${requestId}):`, error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
message: 'IMAP polling failed',
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
requestId,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
} finally {
|
||||
if (lockValue) {
|
||||
await releaseLock(LOCK_KEY, lockValue).catch(() => {})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -581,6 +581,56 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
// --- End RSS specific logic ---
|
||||
|
||||
if (savedWebhook && provider === 'grain') {
|
||||
logger.info(`[${requestId}] Grain provider detected. Creating Grain webhook subscription.`)
|
||||
try {
|
||||
const grainHookId = await createGrainWebhookSubscription(
|
||||
request,
|
||||
{
|
||||
id: savedWebhook.id,
|
||||
path: savedWebhook.path,
|
||||
providerConfig: savedWebhook.providerConfig,
|
||||
},
|
||||
requestId
|
||||
)
|
||||
|
||||
if (grainHookId) {
|
||||
// Update the webhook record with the external Grain hook ID
|
||||
const updatedConfig = {
|
||||
...(savedWebhook.providerConfig as Record<string, any>),
|
||||
externalId: grainHookId,
|
||||
}
|
||||
await db
|
||||
.update(webhook)
|
||||
.set({
|
||||
providerConfig: updatedConfig,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(webhook.id, savedWebhook.id))
|
||||
|
||||
savedWebhook.providerConfig = updatedConfig
|
||||
logger.info(`[${requestId}] Successfully created Grain webhook`, {
|
||||
grainHookId,
|
||||
webhookId: savedWebhook.id,
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
`[${requestId}] Error creating Grain webhook subscription, rolling back webhook`,
|
||||
err
|
||||
)
|
||||
await db.delete(webhook).where(eq(webhook.id, savedWebhook.id))
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to create webhook in Grain',
|
||||
details: err instanceof Error ? err.message : 'Unknown error',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
// --- End Grain specific logic ---
|
||||
|
||||
const status = targetWebhookId ? 200 : 201
|
||||
return NextResponse.json({ webhook: savedWebhook }, { status })
|
||||
} catch (error: any) {
|
||||
@@ -947,3 +997,103 @@ async function createWebflowWebhookSubscription(
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to create the webhook subscription in Grain
|
||||
async function createGrainWebhookSubscription(
|
||||
request: NextRequest,
|
||||
webhookData: any,
|
||||
requestId: string
|
||||
): Promise<string | undefined> {
|
||||
try {
|
||||
const { path, providerConfig } = webhookData
|
||||
const { apiKey, includeHighlights, includeParticipants, includeAiSummary } =
|
||||
providerConfig || {}
|
||||
|
||||
if (!apiKey) {
|
||||
logger.warn(`[${requestId}] Missing apiKey for Grain webhook creation.`, {
|
||||
webhookId: webhookData.id,
|
||||
})
|
||||
throw new Error(
|
||||
'Grain API Key is required. Please provide your Grain Personal Access Token in the trigger configuration.'
|
||||
)
|
||||
}
|
||||
|
||||
const notificationUrl = `${getBaseUrl()}/api/webhooks/trigger/${path}`
|
||||
|
||||
const grainApiUrl = 'https://api.grain.com/_/public-api/v2/hooks/create'
|
||||
|
||||
const requestBody: Record<string, any> = {
|
||||
hook_url: notificationUrl,
|
||||
}
|
||||
|
||||
// Build include object based on configuration
|
||||
const include: Record<string, boolean> = {}
|
||||
if (includeHighlights) {
|
||||
include.highlights = true
|
||||
}
|
||||
if (includeParticipants) {
|
||||
include.participants = true
|
||||
}
|
||||
if (includeAiSummary) {
|
||||
include.ai_summary = true
|
||||
}
|
||||
if (Object.keys(include).length > 0) {
|
||||
requestBody.include = include
|
||||
}
|
||||
|
||||
const grainResponse = await fetch(grainApiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Public-Api-Version': '2025-10-31',
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
})
|
||||
|
||||
const responseBody = await grainResponse.json()
|
||||
|
||||
if (!grainResponse.ok || responseBody.error) {
|
||||
const errorMessage =
|
||||
responseBody.error?.message ||
|
||||
responseBody.error ||
|
||||
responseBody.message ||
|
||||
'Unknown Grain API error'
|
||||
logger.error(
|
||||
`[${requestId}] Failed to create webhook in Grain for webhook ${webhookData.id}. Status: ${grainResponse.status}`,
|
||||
{ message: errorMessage, response: responseBody }
|
||||
)
|
||||
|
||||
let userFriendlyMessage = 'Failed to create webhook subscription in Grain'
|
||||
if (grainResponse.status === 401) {
|
||||
userFriendlyMessage =
|
||||
'Invalid Grain API Key. Please verify your Personal Access Token is correct.'
|
||||
} else if (grainResponse.status === 403) {
|
||||
userFriendlyMessage =
|
||||
'Access denied. Please ensure your Grain API Key has appropriate permissions.'
|
||||
} else if (errorMessage && errorMessage !== 'Unknown Grain API error') {
|
||||
userFriendlyMessage = `Grain error: ${errorMessage}`
|
||||
}
|
||||
|
||||
throw new Error(userFriendlyMessage)
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Successfully created webhook in Grain for webhook ${webhookData.id}.`,
|
||||
{
|
||||
grainWebhookId: responseBody.id,
|
||||
}
|
||||
)
|
||||
|
||||
return responseBody.id
|
||||
} catch (error: any) {
|
||||
logger.error(
|
||||
`[${requestId}] Exception during Grain webhook creation for webhook ${webhookData.id}.`,
|
||||
{
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
}
|
||||
)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,10 +14,6 @@ import {
|
||||
} from '@/app/api/__test-utils__/utils'
|
||||
|
||||
const {
|
||||
hasProcessedMessageMock,
|
||||
markMessageAsProcessedMock,
|
||||
closeRedisConnectionMock,
|
||||
acquireLockMock,
|
||||
generateRequestHashMock,
|
||||
validateSlackSignatureMock,
|
||||
handleWhatsAppVerificationMock,
|
||||
@@ -28,10 +24,6 @@ const {
|
||||
processWebhookMock,
|
||||
executeMock,
|
||||
} = vi.hoisted(() => ({
|
||||
hasProcessedMessageMock: vi.fn().mockResolvedValue(false),
|
||||
markMessageAsProcessedMock: vi.fn().mockResolvedValue(true),
|
||||
closeRedisConnectionMock: vi.fn().mockResolvedValue(undefined),
|
||||
acquireLockMock: vi.fn().mockResolvedValue(true),
|
||||
generateRequestHashMock: vi.fn().mockResolvedValue('test-hash-123'),
|
||||
validateSlackSignatureMock: vi.fn().mockResolvedValue(true),
|
||||
handleWhatsAppVerificationMock: vi.fn().mockResolvedValue(null),
|
||||
@@ -73,13 +65,6 @@ vi.mock('@/background/logs-webhook-delivery', () => ({
|
||||
logsWebhookDelivery: {},
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/redis', () => ({
|
||||
hasProcessedMessage: hasProcessedMessageMock,
|
||||
markMessageAsProcessed: markMessageAsProcessedMock,
|
||||
closeRedisConnection: closeRedisConnectionMock,
|
||||
acquireLock: acquireLockMock,
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/webhooks/utils', () => ({
|
||||
handleWhatsAppVerification: handleWhatsAppVerificationMock,
|
||||
handleSlackChallenge: handleSlackChallengeMock,
|
||||
@@ -201,9 +186,6 @@ describe('Webhook Trigger API Route', () => {
|
||||
workspaceId: 'test-workspace-id',
|
||||
})
|
||||
|
||||
hasProcessedMessageMock.mockResolvedValue(false)
|
||||
markMessageAsProcessedMock.mockResolvedValue(true)
|
||||
acquireLockMock.mockResolvedValue(true)
|
||||
handleWhatsAppVerificationMock.mockResolvedValue(null)
|
||||
processGenericDeduplicationMock.mockResolvedValue(null)
|
||||
processWebhookMock.mockResolvedValue(new Response('Webhook processed', { status: 200 }))
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
checkWebhookPreprocessing,
|
||||
findWebhookAndWorkflow,
|
||||
handleProviderChallenges,
|
||||
handleProviderReachabilityTest,
|
||||
parseWebhookBody,
|
||||
queueWebhookExecution,
|
||||
verifyProviderAuth,
|
||||
@@ -123,6 +124,11 @@ export async function POST(
|
||||
return authError
|
||||
}
|
||||
|
||||
const reachabilityResponse = handleProviderReachabilityTest(foundWebhook, body, requestId)
|
||||
if (reachabilityResponse) {
|
||||
return reachabilityResponse
|
||||
}
|
||||
|
||||
let preprocessError: NextResponse | null = null
|
||||
try {
|
||||
preprocessError = await checkWebhookPreprocessing(foundWorkflow, foundWebhook, requestId)
|
||||
|
||||
@@ -4,12 +4,12 @@ import { and, desc, eq } from 'drizzle-orm'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { removeMcpToolsForWorkflow, syncMcpToolsForWorkflow } from '@/lib/mcp/workflow-mcp-sync'
|
||||
import { deployWorkflow, loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
|
||||
import {
|
||||
createSchedulesForDeploy,
|
||||
deleteSchedulesForWorkflow,
|
||||
validateWorkflowSchedules,
|
||||
} from '@/lib/workflows/schedules'
|
||||
deployWorkflow,
|
||||
loadWorkflowFromNormalizedTables,
|
||||
undeployWorkflow,
|
||||
} from '@/lib/workflows/persistence/utils'
|
||||
import { createSchedulesForDeploy, validateWorkflowSchedules } from '@/lib/workflows/schedules'
|
||||
import { validateWorkflowPermissions } from '@/lib/workflows/utils'
|
||||
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
|
||||
|
||||
@@ -207,21 +207,11 @@ export async function DELETE(
|
||||
return createErrorResponse(error.message, error.status)
|
||||
}
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
await deleteSchedulesForWorkflow(id, tx)
|
||||
const result = await undeployWorkflow({ workflowId: id })
|
||||
if (!result.success) {
|
||||
return createErrorResponse(result.error || 'Failed to undeploy workflow', 500)
|
||||
}
|
||||
|
||||
await tx
|
||||
.update(workflowDeploymentVersion)
|
||||
.set({ isActive: false })
|
||||
.where(eq(workflowDeploymentVersion.workflowId, id))
|
||||
|
||||
await tx
|
||||
.update(workflow)
|
||||
.set({ isDeployed: false, deployedAt: null })
|
||||
.where(eq(workflow.id, id))
|
||||
})
|
||||
|
||||
// Remove all MCP tools that reference this workflow
|
||||
await removeMcpToolsForWorkflow(id, requestId)
|
||||
|
||||
logger.info(`[${requestId}] Workflow undeployed successfully: ${id}`)
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { db, workflow, workflowDeploymentVersion } from '@sim/db'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { syncMcpToolsForWorkflow } from '@/lib/mcp/workflow-mcp-sync'
|
||||
import { activateWorkflowVersion } from '@/lib/workflows/persistence/utils'
|
||||
import { validateWorkflowPermissions } from '@/lib/workflows/utils'
|
||||
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
|
||||
|
||||
@@ -27,68 +26,24 @@ export async function POST(
|
||||
|
||||
const versionNum = Number(version)
|
||||
if (!Number.isFinite(versionNum)) {
|
||||
return createErrorResponse('Invalid version', 400)
|
||||
return createErrorResponse('Invalid version number', 400)
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
const result = await activateWorkflowVersion({ workflowId: id, version: versionNum })
|
||||
if (!result.success) {
|
||||
return createErrorResponse(result.error || 'Failed to activate deployment', 400)
|
||||
}
|
||||
|
||||
// Get the state of the version being activated for MCP tool sync
|
||||
const [versionData] = await db
|
||||
.select({ state: workflowDeploymentVersion.state })
|
||||
.from(workflowDeploymentVersion)
|
||||
.where(
|
||||
and(
|
||||
eq(workflowDeploymentVersion.workflowId, id),
|
||||
eq(workflowDeploymentVersion.version, versionNum)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
await tx
|
||||
.update(workflowDeploymentVersion)
|
||||
.set({ isActive: false })
|
||||
.where(
|
||||
and(
|
||||
eq(workflowDeploymentVersion.workflowId, id),
|
||||
eq(workflowDeploymentVersion.isActive, true)
|
||||
)
|
||||
)
|
||||
|
||||
const updated = await tx
|
||||
.update(workflowDeploymentVersion)
|
||||
.set({ isActive: true })
|
||||
.where(
|
||||
and(
|
||||
eq(workflowDeploymentVersion.workflowId, id),
|
||||
eq(workflowDeploymentVersion.version, versionNum)
|
||||
)
|
||||
)
|
||||
.returning({ id: workflowDeploymentVersion.id })
|
||||
|
||||
if (updated.length === 0) {
|
||||
throw new Error('Deployment version not found')
|
||||
}
|
||||
|
||||
const updateData: Record<string, unknown> = {
|
||||
isDeployed: true,
|
||||
deployedAt: now,
|
||||
}
|
||||
|
||||
await tx.update(workflow).set(updateData).where(eq(workflow.id, id))
|
||||
})
|
||||
|
||||
// Sync MCP tools with the activated version's parameter schema
|
||||
if (versionData?.state) {
|
||||
if (result.state) {
|
||||
await syncMcpToolsForWorkflow({
|
||||
workflowId: id,
|
||||
requestId,
|
||||
state: versionData.state,
|
||||
state: result.state,
|
||||
context: 'activate',
|
||||
})
|
||||
}
|
||||
|
||||
return createSuccessResponse({ success: true, deployedAt: now })
|
||||
return createSuccessResponse({ success: true, deployedAt: result.deployedAt })
|
||||
} catch (error: any) {
|
||||
logger.error(`[${requestId}] Error activating deployment for workflow: ${id}`, error)
|
||||
return createErrorResponse(error.message || 'Failed to activate deployment', 500)
|
||||
|
||||
@@ -21,7 +21,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||
return createErrorResponse(error.message, error.status)
|
||||
}
|
||||
|
||||
const versions = await db
|
||||
const rawVersions = await db
|
||||
.select({
|
||||
id: workflowDeploymentVersion.id,
|
||||
version: workflowDeploymentVersion.version,
|
||||
@@ -36,6 +36,11 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||
.where(eq(workflowDeploymentVersion.workflowId, id))
|
||||
.orderBy(desc(workflowDeploymentVersion.version))
|
||||
|
||||
const versions = rawVersions.map((v) => ({
|
||||
...v,
|
||||
deployedBy: v.deployedBy ?? (v.createdBy === 'admin-api' ? 'Admin' : null),
|
||||
}))
|
||||
|
||||
return createSuccessResponse({ versions })
|
||||
} catch (error: any) {
|
||||
logger.error(`[${requestId}] Error listing deployments for workflow: ${id}`, error)
|
||||
|
||||
@@ -12,7 +12,6 @@ import { markExecutionCancelled } from '@/lib/execution/cancellation'
|
||||
import { processInputFileFields } from '@/lib/execution/files'
|
||||
import { preprocessExecution } from '@/lib/execution/preprocessing'
|
||||
import { LoggingSession } from '@/lib/logs/execution/logging-session'
|
||||
import { ALL_TRIGGER_TYPES } from '@/lib/logs/types'
|
||||
import { executeWorkflowCore } from '@/lib/workflows/executor/execution-core'
|
||||
import { type ExecutionEvent, encodeSSEEvent } from '@/lib/workflows/executor/execution-events'
|
||||
import { PauseResumeManager } from '@/lib/workflows/executor/human-in-the-loop-manager'
|
||||
@@ -24,16 +23,17 @@ import { createStreamingResponse } from '@/lib/workflows/streaming/streaming'
|
||||
import { createHttpResponseFromBlock, workflowHasResponseBlock } from '@/lib/workflows/utils'
|
||||
import type { WorkflowExecutionPayload } from '@/background/workflow-execution'
|
||||
import { normalizeName } from '@/executor/constants'
|
||||
import { type ExecutionMetadata, ExecutionSnapshot } from '@/executor/execution/snapshot'
|
||||
import { ExecutionSnapshot } from '@/executor/execution/snapshot'
|
||||
import type { ExecutionMetadata, IterationContext } from '@/executor/execution/types'
|
||||
import type { StreamingExecution } from '@/executor/types'
|
||||
import { Serializer } from '@/serializer'
|
||||
import type { SubflowType } from '@/stores/workflows/workflow/types'
|
||||
import { CORE_TRIGGER_TYPES } from '@/stores/logs/filters/types'
|
||||
|
||||
const logger = createLogger('WorkflowExecuteAPI')
|
||||
|
||||
const ExecuteWorkflowSchema = z.object({
|
||||
selectedOutputs: z.array(z.string()).optional().default([]),
|
||||
triggerType: z.enum(ALL_TRIGGER_TYPES).optional(),
|
||||
triggerType: z.enum(CORE_TRIGGER_TYPES).optional(),
|
||||
stream: z.boolean().optional(),
|
||||
useDraftState: z.boolean().optional(),
|
||||
input: z.any().optional(),
|
||||
@@ -541,11 +541,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
blockId: string,
|
||||
blockName: string,
|
||||
blockType: string,
|
||||
iterationContext?: {
|
||||
iterationCurrent: number
|
||||
iterationTotal: number
|
||||
iterationType: SubflowType
|
||||
}
|
||||
iterationContext?: IterationContext
|
||||
) => {
|
||||
logger.info(`[${requestId}] 🔷 onBlockStart called:`, { blockId, blockName, blockType })
|
||||
sendEvent({
|
||||
@@ -571,11 +567,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
blockName: string,
|
||||
blockType: string,
|
||||
callbackData: any,
|
||||
iterationContext?: {
|
||||
iterationCurrent: number
|
||||
iterationTotal: number
|
||||
iterationType: SubflowType
|
||||
}
|
||||
iterationContext?: IterationContext
|
||||
) => {
|
||||
const hasError = callbackData.output?.error
|
||||
|
||||
@@ -713,14 +705,25 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
logger.error(`[${requestId}] Missing snapshot seed for paused execution`, {
|
||||
executionId,
|
||||
})
|
||||
await loggingSession.markAsFailed('Missing snapshot seed for paused execution')
|
||||
} else {
|
||||
await PauseResumeManager.persistPauseResult({
|
||||
workflowId,
|
||||
executionId,
|
||||
pausePoints: result.pausePoints || [],
|
||||
snapshotSeed: result.snapshotSeed,
|
||||
executorUserId: result.metadata?.userId,
|
||||
})
|
||||
try {
|
||||
await PauseResumeManager.persistPauseResult({
|
||||
workflowId,
|
||||
executionId,
|
||||
pausePoints: result.pausePoints || [],
|
||||
snapshotSeed: result.snapshotSeed,
|
||||
executorUserId: result.metadata?.userId,
|
||||
})
|
||||
} catch (pauseError) {
|
||||
logger.error(`[${requestId}] Failed to persist pause result`, {
|
||||
executionId,
|
||||
error: pauseError instanceof Error ? pauseError.message : String(pauseError),
|
||||
})
|
||||
await loggingSession.markAsFailed(
|
||||
`Failed to persist pause state: ${pauseError instanceof Error ? pauseError.message : String(pauseError)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await PauseResumeManager.processQueuedResumes(executionId)
|
||||
|
||||
@@ -6,14 +6,14 @@ import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { encryptSecret } from '@/lib/core/security/encryption'
|
||||
import { ALL_TRIGGER_TYPES } from '@/lib/logs/types'
|
||||
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
||||
import { CORE_TRIGGER_TYPES } from '@/stores/logs/filters/types'
|
||||
import { MAX_EMAIL_RECIPIENTS, MAX_WORKFLOW_IDS } from '../constants'
|
||||
|
||||
const logger = createLogger('WorkspaceNotificationAPI')
|
||||
|
||||
const levelFilterSchema = z.array(z.enum(['info', 'error']))
|
||||
const triggerFilterSchema = z.array(z.enum(ALL_TRIGGER_TYPES))
|
||||
const triggerFilterSchema = z.array(z.enum(CORE_TRIGGER_TYPES))
|
||||
|
||||
const alertRuleSchema = z.enum([
|
||||
'consecutive_failures',
|
||||
|
||||
@@ -7,15 +7,15 @@ import { v4 as uuidv4 } from 'uuid'
|
||||
import { z } from 'zod'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { encryptSecret } from '@/lib/core/security/encryption'
|
||||
import { ALL_TRIGGER_TYPES } from '@/lib/logs/types'
|
||||
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
||||
import { CORE_TRIGGER_TYPES } from '@/stores/logs/filters/types'
|
||||
import { MAX_EMAIL_RECIPIENTS, MAX_NOTIFICATIONS_PER_TYPE, MAX_WORKFLOW_IDS } from './constants'
|
||||
|
||||
const logger = createLogger('WorkspaceNotificationsAPI')
|
||||
|
||||
const notificationTypeSchema = z.enum(['webhook', 'email', 'slack'])
|
||||
const levelFilterSchema = z.array(z.enum(['info', 'error']))
|
||||
const triggerFilterSchema = z.array(z.enum(ALL_TRIGGER_TYPES))
|
||||
const triggerFilterSchema = z.array(z.enum(CORE_TRIGGER_TYPES))
|
||||
|
||||
const alertRuleSchema = z.enum([
|
||||
'consecutive_failures',
|
||||
@@ -81,7 +81,7 @@ const createNotificationSchema = z
|
||||
workflowIds: z.array(z.string()).max(MAX_WORKFLOW_IDS).default([]),
|
||||
allWorkflows: z.boolean().default(false),
|
||||
levelFilter: levelFilterSchema.default(['info', 'error']),
|
||||
triggerFilter: triggerFilterSchema.default([...ALL_TRIGGER_TYPES]),
|
||||
triggerFilter: triggerFilterSchema.default([...CORE_TRIGGER_TYPES]),
|
||||
includeFinalOutput: z.boolean().default(false),
|
||||
includeTraceSpans: z.boolean().default(false),
|
||||
includeRateLimits: z.boolean().default(false),
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { WorkspaceInvitationEmail } from '@/components/emails/workspace-invitation'
|
||||
import { WorkspaceInvitationEmail } from '@/components/emails'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { sendEmail } from '@/lib/messaging/email/mailer'
|
||||
|
||||
@@ -87,14 +87,10 @@ describe('Workspace Invitations API Route', () => {
|
||||
WorkspaceInvitationEmail: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/core/config/env', () => ({
|
||||
env: {
|
||||
RESEND_API_KEY: 'test-resend-key',
|
||||
NEXT_PUBLIC_APP_URL: 'https://test.sim.ai',
|
||||
FROM_EMAIL_ADDRESS: 'Sim <noreply@test.sim.ai>',
|
||||
EMAIL_DOMAIN: 'test.sim.ai',
|
||||
},
|
||||
}))
|
||||
vi.doMock('@/lib/core/config/env', async () => {
|
||||
const { createEnvMock } = await import('@sim/testing')
|
||||
return createEnvMock()
|
||||
})
|
||||
|
||||
vi.doMock('@/lib/core/utils/urls', () => ({
|
||||
getEmailDomain: vi.fn().mockReturnValue('sim.ai'),
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, eq, inArray } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { WorkspaceInvitationEmail } from '@/components/emails/workspace-invitation'
|
||||
import { WorkspaceInvitationEmail } from '@/components/emails'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { sendEmail } from '@/lib/messaging/email/mailer'
|
||||
|
||||
@@ -117,7 +117,7 @@ export default function ChatClient({ identifier }: { identifier: string }) {
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||
const messagesContainerRef = useRef<HTMLDivElement>(null)
|
||||
const [starCount, setStarCount] = useState('24.4k')
|
||||
const [starCount, setStarCount] = useState('25.1k')
|
||||
const [conversationId, setConversationId] = useState('')
|
||||
|
||||
const [showScrollButton, setShowScrollButton] = useState(false)
|
||||
|
||||
@@ -16,7 +16,7 @@ export function LinkWithPreview({ href, children }: { href: string; children: Re
|
||||
{children}
|
||||
</a>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side='top' align='center' sideOffset={5} className='max-w-sm p-3'>
|
||||
<Tooltip.Content side='top' align='center' sideOffset={5} className='max-w-sm'>
|
||||
<span className='truncate font-medium text-xs'>{href}</span>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
|
||||
@@ -8,35 +8,156 @@ import { InviteLayout, InviteStatusCard } from '@/app/invite/components'
|
||||
|
||||
const logger = createLogger('InviteById')
|
||||
|
||||
function getErrorMessage(reason: string): string {
|
||||
switch (reason) {
|
||||
case 'missing-token':
|
||||
return 'The invitation link is invalid or missing a required parameter.'
|
||||
case 'invalid-token':
|
||||
return 'The invitation link is invalid or has already been used.'
|
||||
case 'expired':
|
||||
return 'This invitation has expired. Please ask for a new invitation.'
|
||||
case 'already-processed':
|
||||
return 'This invitation has already been accepted or declined.'
|
||||
case 'email-mismatch':
|
||||
return 'This invitation was sent to a different email address. Please log in with the correct account.'
|
||||
case 'workspace-not-found':
|
||||
return 'The workspace associated with this invitation could not be found.'
|
||||
case 'user-not-found':
|
||||
return 'Your user account could not be found. Please try logging out and logging back in.'
|
||||
case 'already-member':
|
||||
return 'You are already a member of this organization or workspace.'
|
||||
case 'already-in-organization':
|
||||
return 'You are already a member of an organization. Leave your current organization before accepting a new invitation.'
|
||||
case 'invalid-invitation':
|
||||
return 'This invitation is invalid or no longer exists.'
|
||||
case 'missing-invitation-id':
|
||||
return 'The invitation link is missing required information. Please use the original invitation link.'
|
||||
case 'server-error':
|
||||
return 'An unexpected error occurred while processing your invitation. Please try again later.'
|
||||
default:
|
||||
return 'An unknown error occurred while processing your invitation.'
|
||||
/** Error codes that can occur during invitation processing */
|
||||
type InviteErrorCode =
|
||||
| 'missing-token'
|
||||
| 'invalid-token'
|
||||
| 'expired'
|
||||
| 'already-processed'
|
||||
| 'email-mismatch'
|
||||
| 'workspace-not-found'
|
||||
| 'user-not-found'
|
||||
| 'already-member'
|
||||
| 'already-in-organization'
|
||||
| 'invalid-invitation'
|
||||
| 'missing-invitation-id'
|
||||
| 'server-error'
|
||||
| 'unauthorized'
|
||||
| 'forbidden'
|
||||
| 'network-error'
|
||||
| 'unknown'
|
||||
|
||||
interface InviteError {
|
||||
code: InviteErrorCode
|
||||
message: string
|
||||
requiresAuth?: boolean
|
||||
canRetry?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps error codes to user-friendly error objects with contextual information
|
||||
*/
|
||||
function getInviteError(reason: string): InviteError {
|
||||
const errorMap: Record<string, InviteError> = {
|
||||
'missing-token': {
|
||||
code: 'missing-token',
|
||||
message: 'The invitation link is invalid or missing a required parameter.',
|
||||
},
|
||||
'invalid-token': {
|
||||
code: 'invalid-token',
|
||||
message: 'The invitation link is invalid or has already been used.',
|
||||
},
|
||||
expired: {
|
||||
code: 'expired',
|
||||
message: 'This invitation has expired. Please ask for a new invitation.',
|
||||
},
|
||||
'already-processed': {
|
||||
code: 'already-processed',
|
||||
message: 'This invitation has already been accepted or declined.',
|
||||
},
|
||||
'email-mismatch': {
|
||||
code: 'email-mismatch',
|
||||
message:
|
||||
'This invitation was sent to a different email address. Please sign in with the correct account.',
|
||||
requiresAuth: true,
|
||||
},
|
||||
'workspace-not-found': {
|
||||
code: 'workspace-not-found',
|
||||
message: 'The workspace associated with this invitation could not be found.',
|
||||
},
|
||||
'user-not-found': {
|
||||
code: 'user-not-found',
|
||||
message: 'Your user account could not be found. Please try signing out and signing back in.',
|
||||
requiresAuth: true,
|
||||
},
|
||||
'already-member': {
|
||||
code: 'already-member',
|
||||
message: 'You are already a member of this organization or workspace.',
|
||||
},
|
||||
'already-in-organization': {
|
||||
code: 'already-in-organization',
|
||||
message:
|
||||
'You are already a member of an organization. Leave your current organization before accepting a new invitation.',
|
||||
},
|
||||
'invalid-invitation': {
|
||||
code: 'invalid-invitation',
|
||||
message: 'This invitation is invalid or no longer exists.',
|
||||
},
|
||||
'missing-invitation-id': {
|
||||
code: 'missing-invitation-id',
|
||||
message:
|
||||
'The invitation link is missing required information. Please use the original invitation link.',
|
||||
},
|
||||
'server-error': {
|
||||
code: 'server-error',
|
||||
message:
|
||||
'An unexpected error occurred while processing your invitation. Please try again later.',
|
||||
canRetry: true,
|
||||
},
|
||||
unauthorized: {
|
||||
code: 'unauthorized',
|
||||
message: 'You need to sign in to accept this invitation.',
|
||||
requiresAuth: true,
|
||||
},
|
||||
forbidden: {
|
||||
code: 'forbidden',
|
||||
message:
|
||||
'You do not have permission to accept this invitation. Please check you are signed in with the correct account.',
|
||||
requiresAuth: true,
|
||||
},
|
||||
'network-error': {
|
||||
code: 'network-error',
|
||||
message:
|
||||
'Unable to connect to the server. Please check your internet connection and try again.',
|
||||
canRetry: true,
|
||||
},
|
||||
}
|
||||
|
||||
return (
|
||||
errorMap[reason] || {
|
||||
code: 'unknown',
|
||||
message:
|
||||
'An unexpected error occurred while processing your invitation. Please try again or contact support.',
|
||||
canRetry: true,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses API error responses and extracts a standardized error code
|
||||
*/
|
||||
function parseApiError(error: unknown, statusCode?: number): InviteErrorCode {
|
||||
// Handle network/fetch errors
|
||||
if (error instanceof TypeError && error.message.includes('fetch')) {
|
||||
return 'network-error'
|
||||
}
|
||||
|
||||
// Handle error message patterns first (more specific matching)
|
||||
const errorMessage =
|
||||
typeof error === 'string' ? error.toLowerCase() : (error as Error)?.message?.toLowerCase() || ''
|
||||
|
||||
// Check specific patterns before falling back to status codes
|
||||
// Order matters: more specific patterns must come first
|
||||
if (errorMessage.includes('already a member of an organization')) return 'already-in-organization'
|
||||
if (errorMessage.includes('already a member')) return 'already-member'
|
||||
if (errorMessage.includes('email mismatch') || errorMessage.includes('different email'))
|
||||
return 'email-mismatch'
|
||||
if (errorMessage.includes('already processed')) return 'already-processed'
|
||||
if (errorMessage.includes('unauthorized')) return 'unauthorized'
|
||||
if (errorMessage.includes('forbidden') || errorMessage.includes('permission')) return 'forbidden'
|
||||
if (errorMessage.includes('not found') || errorMessage.includes('expired'))
|
||||
return 'invalid-invitation'
|
||||
|
||||
// Handle HTTP status codes as fallback
|
||||
if (statusCode) {
|
||||
if (statusCode === 401) return 'unauthorized'
|
||||
if (statusCode === 403) return 'forbidden'
|
||||
if (statusCode === 404) return 'invalid-invitation'
|
||||
if (statusCode === 409) return 'already-in-organization'
|
||||
if (statusCode >= 500) return 'server-error'
|
||||
}
|
||||
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
export default function Invite() {
|
||||
@@ -47,7 +168,7 @@ export default function Invite() {
|
||||
const { data: session, isPending } = useSession()
|
||||
const [invitationDetails, setInvitationDetails] = useState<any>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [error, setError] = useState<InviteError | null>(null)
|
||||
const [isAccepting, setIsAccepting] = useState(false)
|
||||
const [accepted, setAccepted] = useState(false)
|
||||
const [isNewUser, setIsNewUser] = useState(false)
|
||||
@@ -59,7 +180,7 @@ export default function Invite() {
|
||||
const errorReason = searchParams.get('error')
|
||||
|
||||
if (errorReason) {
|
||||
setError(getErrorMessage(errorReason))
|
||||
setError(getInviteError(errorReason))
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
@@ -99,11 +220,37 @@ export default function Invite() {
|
||||
return
|
||||
}
|
||||
|
||||
// Handle workspace invitation errors with specific status codes
|
||||
if (!workspaceInviteResponse.ok && workspaceInviteResponse.status !== 404) {
|
||||
const errorCode = parseApiError(null, workspaceInviteResponse.status)
|
||||
const errorData = await workspaceInviteResponse.json().catch(() => ({}))
|
||||
logger.error('Workspace invitation fetch failed:', {
|
||||
status: workspaceInviteResponse.status,
|
||||
error: errorData,
|
||||
})
|
||||
|
||||
// Refine error code based on response body if available
|
||||
if (errorData.error) {
|
||||
const refinedCode = parseApiError(errorData.error, workspaceInviteResponse.status)
|
||||
setError(getInviteError(refinedCode))
|
||||
} else {
|
||||
setError(getInviteError(errorCode))
|
||||
}
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const { data } = await client.organization.getInvitation({
|
||||
const { data, error: orgError } = await client.organization.getInvitation({
|
||||
query: { id: inviteId },
|
||||
})
|
||||
|
||||
if (orgError) {
|
||||
logger.error('Organization invitation fetch error:', orgError)
|
||||
const errorCode = parseApiError(orgError.message || orgError)
|
||||
throw { code: errorCode, original: orgError }
|
||||
}
|
||||
|
||||
if (data) {
|
||||
setInvitationType('organization')
|
||||
|
||||
@@ -115,7 +262,7 @@ export default function Invite() {
|
||||
if (activeOrgResponse?.data) {
|
||||
// User is already in an organization
|
||||
setCurrentOrgName(activeOrgResponse.data.name)
|
||||
setError('already-in-organization')
|
||||
setError(getInviteError('already-in-organization'))
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
@@ -139,14 +286,19 @@ export default function Invite() {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new Error('Invitation not found or has expired')
|
||||
throw { code: 'invalid-invitation' }
|
||||
}
|
||||
} catch (_err) {
|
||||
throw new Error('Invitation not found or has expired')
|
||||
} catch (orgErr: any) {
|
||||
// If this is our structured error, use it directly
|
||||
if (orgErr.code) {
|
||||
throw orgErr
|
||||
}
|
||||
throw { code: parseApiError(orgErr) }
|
||||
}
|
||||
} catch (err: any) {
|
||||
logger.error('Error fetching invitation:', err)
|
||||
setError(err.message || 'Failed to load invitation details')
|
||||
const errorCode = err.code || parseApiError(err)
|
||||
setError(getInviteError(errorCode))
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
@@ -168,7 +320,9 @@ export default function Invite() {
|
||||
const orgId = invitationDetails?.data?.organizationId
|
||||
|
||||
if (!orgId) {
|
||||
throw new Error('Organization ID not found')
|
||||
setError(getInviteError('invalid-invitation'))
|
||||
setIsAccepting(false)
|
||||
return
|
||||
}
|
||||
|
||||
// Use our custom API endpoint that handles Pro usage snapshot
|
||||
@@ -182,8 +336,15 @@ export default function Invite() {
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({ error: 'Failed to accept invitation' }))
|
||||
throw new Error(data.error || 'Failed to accept invitation')
|
||||
const data = await response.json().catch(() => ({}))
|
||||
const errorCode = parseApiError(data.error || '', response.status)
|
||||
logger.error('Failed to accept organization invitation:', {
|
||||
status: response.status,
|
||||
error: data,
|
||||
})
|
||||
setError(getInviteError(errorCode))
|
||||
setIsAccepting(false)
|
||||
return
|
||||
}
|
||||
|
||||
// Set the organization as active
|
||||
@@ -202,13 +363,8 @@ export default function Invite() {
|
||||
// Reset accepted state on error
|
||||
setAccepted(false)
|
||||
|
||||
// Check if it's a 409 conflict (already in an organization)
|
||||
if (err.status === 409 || err.message?.includes('already a member of an organization')) {
|
||||
setError('already-in-organization')
|
||||
} else {
|
||||
setError(err.message || 'Failed to accept invitation')
|
||||
}
|
||||
|
||||
const errorCode = parseApiError(err)
|
||||
setError(getInviteError(errorCode))
|
||||
setIsAccepting(false)
|
||||
}
|
||||
}
|
||||
@@ -279,12 +435,10 @@ export default function Invite() {
|
||||
}
|
||||
|
||||
if (error) {
|
||||
const errorReason = searchParams.get('error')
|
||||
const isExpiredError = errorReason === 'expired'
|
||||
const isAlreadyInOrg = error === 'already-in-organization'
|
||||
const callbackUrl = encodeURIComponent(getCallbackUrl())
|
||||
|
||||
// Special handling for already in organization
|
||||
if (isAlreadyInOrg) {
|
||||
if (error.code === 'already-in-organization') {
|
||||
return (
|
||||
<InviteLayout>
|
||||
<InviteStatusCard
|
||||
@@ -293,7 +447,7 @@ export default function Invite() {
|
||||
description={
|
||||
currentOrgName
|
||||
? `You are currently a member of "${currentOrgName}". You must leave your current organization before accepting a new invitation.`
|
||||
: 'You are already a member of an organization. Leave your current organization before accepting a new invitation.'
|
||||
: error.message
|
||||
}
|
||||
icon='users'
|
||||
actions={[
|
||||
@@ -313,24 +467,96 @@ export default function Invite() {
|
||||
)
|
||||
}
|
||||
|
||||
// Use getErrorMessage for consistent error messages
|
||||
const errorMessage = error.startsWith('You are already') ? error : getErrorMessage(error)
|
||||
// Handle email mismatch - user needs to sign in with a different account
|
||||
if (error.code === 'email-mismatch') {
|
||||
return (
|
||||
<InviteLayout>
|
||||
<InviteStatusCard
|
||||
type='warning'
|
||||
title='Wrong Account'
|
||||
description={error.message}
|
||||
icon='userPlus'
|
||||
actions={[
|
||||
{
|
||||
label: 'Sign in with a different account',
|
||||
onClick: async () => {
|
||||
await client.signOut()
|
||||
router.push(`/login?callbackUrl=${callbackUrl}&invite_flow=true`)
|
||||
},
|
||||
variant: 'default' as const,
|
||||
},
|
||||
{
|
||||
label: 'Return to Home',
|
||||
onClick: () => router.push('/'),
|
||||
variant: 'ghost' as const,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</InviteLayout>
|
||||
)
|
||||
}
|
||||
|
||||
// Handle auth-related errors - prompt user to sign in
|
||||
if (error.requiresAuth) {
|
||||
return (
|
||||
<InviteLayout>
|
||||
<InviteStatusCard
|
||||
type='warning'
|
||||
title='Authentication Required'
|
||||
description={error.message}
|
||||
icon='userPlus'
|
||||
actions={[
|
||||
{
|
||||
label: 'Sign in to continue',
|
||||
onClick: () => router.push(`/login?callbackUrl=${callbackUrl}&invite_flow=true`),
|
||||
variant: 'default' as const,
|
||||
},
|
||||
{
|
||||
label: 'Create an account',
|
||||
onClick: () => router.push(`/signup?callbackUrl=${callbackUrl}&invite_flow=true`),
|
||||
variant: 'outline' as const,
|
||||
},
|
||||
{
|
||||
label: 'Return to Home',
|
||||
onClick: () => router.push('/'),
|
||||
variant: 'ghost' as const,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</InviteLayout>
|
||||
)
|
||||
}
|
||||
|
||||
// Handle retryable errors
|
||||
const actions: Array<{
|
||||
label: string
|
||||
onClick: () => void
|
||||
variant?: 'default' | 'outline' | 'ghost'
|
||||
}> = []
|
||||
|
||||
if (error.canRetry) {
|
||||
actions.push({
|
||||
label: 'Try Again',
|
||||
onClick: () => window.location.reload(),
|
||||
variant: 'default' as const,
|
||||
})
|
||||
}
|
||||
|
||||
actions.push({
|
||||
label: 'Return to Home',
|
||||
onClick: () => router.push('/'),
|
||||
variant: error.canRetry ? ('ghost' as const) : ('default' as const),
|
||||
})
|
||||
|
||||
return (
|
||||
<InviteLayout>
|
||||
<InviteStatusCard
|
||||
type='error'
|
||||
title='Invitation Error'
|
||||
description={errorMessage}
|
||||
description={error.message}
|
||||
icon='error'
|
||||
isExpiredError={isExpiredError}
|
||||
actions={[
|
||||
{
|
||||
label: 'Return to Home',
|
||||
onClick: () => router.push('/'),
|
||||
variant: 'default' as const,
|
||||
},
|
||||
]}
|
||||
isExpiredError={error.code === 'expired'}
|
||||
actions={actions}
|
||||
/>
|
||||
</InviteLayout>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import AuthBackground from '@/app/(auth)/components/auth-background'
|
||||
import Nav from '@/app/(landing)/components/nav/nav'
|
||||
|
||||
interface InviteLayoutProps {
|
||||
@@ -8,12 +9,13 @@ interface InviteLayoutProps {
|
||||
|
||||
export default function InviteLayout({ children }: InviteLayoutProps) {
|
||||
return (
|
||||
<div className='relative min-h-screen'>
|
||||
<div className='-z-50 pointer-events-none fixed inset-0 bg-white' />
|
||||
<Nav variant='auth' />
|
||||
<div className='flex min-h-[calc(100vh-120px)] items-center justify-center px-4'>
|
||||
<div className='w-full max-w-[410px]'>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
<AuthBackground>
|
||||
<main className='relative flex min-h-screen flex-col text-foreground'>
|
||||
<Nav hideAuthButtons={true} variant='auth' />
|
||||
<div className='relative z-30 flex flex-1 items-center justify-center px-4 pb-24'>
|
||||
<div className='w-full max-w-lg px-4'>{children}</div>
|
||||
</div>
|
||||
</main>
|
||||
</AuthBackground>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import {
|
||||
AlertCircle,
|
||||
CheckCircle2,
|
||||
Loader2,
|
||||
Mail,
|
||||
RotateCcw,
|
||||
ShieldX,
|
||||
UserPlus,
|
||||
Users2,
|
||||
} from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { ArrowRight, ChevronRight, Loader2, RotateCcw } from 'lucide-react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useBrandConfig } from '@/lib/branding/branding'
|
||||
@@ -32,104 +23,53 @@ interface InviteStatusCardProps {
|
||||
isExpiredError?: boolean
|
||||
}
|
||||
|
||||
const iconMap = {
|
||||
userPlus: UserPlus,
|
||||
mail: Mail,
|
||||
users: Users2,
|
||||
error: ShieldX,
|
||||
success: CheckCircle2,
|
||||
warning: AlertCircle,
|
||||
}
|
||||
|
||||
const iconColorMap = {
|
||||
userPlus: 'text-[var(--brand-primary-hex)]',
|
||||
mail: 'text-[var(--brand-primary-hex)]',
|
||||
users: 'text-[var(--brand-primary-hex)]',
|
||||
error: 'text-red-500 dark:text-red-400',
|
||||
success: 'text-green-500 dark:text-green-400',
|
||||
warning: 'text-yellow-600 dark:text-yellow-500',
|
||||
}
|
||||
|
||||
const iconBgMap = {
|
||||
userPlus: 'bg-[var(--brand-primary-hex)]/10',
|
||||
mail: 'bg-[var(--brand-primary-hex)]/10',
|
||||
users: 'bg-[var(--brand-primary-hex)]/10',
|
||||
error: 'bg-red-50 dark:bg-red-950/20',
|
||||
success: 'bg-green-50 dark:bg-green-950/20',
|
||||
warning: 'bg-yellow-50 dark:bg-yellow-950/20',
|
||||
}
|
||||
|
||||
export function InviteStatusCard({
|
||||
type,
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
icon: _icon,
|
||||
actions = [],
|
||||
isExpiredError = false,
|
||||
}: InviteStatusCardProps) {
|
||||
const router = useRouter()
|
||||
const [buttonClass, setButtonClass] = useState('auth-button-gradient')
|
||||
const [hoveredButtonIndex, setHoveredButtonIndex] = useState<number | null>(null)
|
||||
const brandConfig = useBrandConfig()
|
||||
|
||||
useEffect(() => {
|
||||
const checkCustomBrand = () => {
|
||||
const computedStyle = getComputedStyle(document.documentElement)
|
||||
const brandAccent = computedStyle.getPropertyValue('--brand-accent-hex').trim()
|
||||
if (brandAccent && brandAccent !== '#6f3dfa') {
|
||||
setButtonClass('auth-button-custom')
|
||||
} else {
|
||||
setButtonClass('auth-button-gradient')
|
||||
}
|
||||
}
|
||||
checkCustomBrand()
|
||||
window.addEventListener('resize', checkCustomBrand)
|
||||
const observer = new MutationObserver(checkCustomBrand)
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['style', 'class'],
|
||||
})
|
||||
return () => {
|
||||
window.removeEventListener('resize', checkCustomBrand)
|
||||
observer.disconnect()
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (type === 'loading') {
|
||||
return (
|
||||
<div className={`${soehne.className} space-y-6`}>
|
||||
<>
|
||||
<div className='space-y-1 text-center'>
|
||||
<h1 className='font-medium text-[32px] text-black tracking-tight'>Loading</h1>
|
||||
<h1 className={`${soehne.className} font-medium text-[32px] text-black tracking-tight`}>
|
||||
Loading
|
||||
</h1>
|
||||
<p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}>
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
<div className='flex w-full items-center justify-center py-8'>
|
||||
<Loader2 className='h-8 w-8 animate-spin text-[var(--brand-primary-hex)]' />
|
||||
<div className={`${inter.className} mt-8 flex w-full items-center justify-center py-8`}>
|
||||
<Loader2 className='h-8 w-8 animate-spin text-muted-foreground' />
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`${inter.className} auth-text-muted fixed right-0 bottom-0 left-0 z-50 pb-8 text-center font-[340] text-[13px] leading-relaxed`}
|
||||
className={`${inter.className} auth-text-muted absolute right-0 bottom-0 left-0 px-8 pb-8 text-center font-[340] text-[13px] leading-relaxed sm:px-8 md:px-[44px]`}
|
||||
>
|
||||
Need help?{' '}
|
||||
<a
|
||||
href='mailto:help@sim.ai'
|
||||
href={`mailto:${brandConfig.supportEmail}`}
|
||||
className='auth-link underline-offset-4 transition hover:underline'
|
||||
>
|
||||
Contact support
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const IconComponent = icon ? iconMap[icon] : null
|
||||
const iconColor = icon ? iconColorMap[icon] : ''
|
||||
const iconBg = icon ? iconBgMap[icon] : ''
|
||||
|
||||
return (
|
||||
<div className={`${soehne.className} space-y-6`}>
|
||||
<>
|
||||
<div className='space-y-1 text-center'>
|
||||
<h1 className='font-medium text-[32px] text-black tracking-tight'>{title}</h1>
|
||||
<h1 className={`${soehne.className} font-medium text-[32px] text-black tracking-tight`}>
|
||||
{title}
|
||||
</h1>
|
||||
<p className={`${inter.className} font-[380] text-[16px] text-muted-foreground`}>
|
||||
{description}
|
||||
</p>
|
||||
@@ -148,28 +88,55 @@ export function InviteStatusCard({
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{actions.map((action, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
variant={action.variant || 'default'}
|
||||
className={
|
||||
(action.variant || 'default') === 'default'
|
||||
? `${buttonClass} flex w-full items-center justify-center gap-2 rounded-[10px] border font-medium text-[15px] text-white transition-all duration-200`
|
||||
: action.variant === 'outline'
|
||||
{actions.map((action, index) => {
|
||||
const isPrimary = (action.variant || 'default') === 'default'
|
||||
const isHovered = hoveredButtonIndex === index
|
||||
|
||||
if (isPrimary) {
|
||||
return (
|
||||
<Button
|
||||
key={index}
|
||||
onMouseEnter={() => setHoveredButtonIndex(index)}
|
||||
onMouseLeave={() => setHoveredButtonIndex(null)}
|
||||
className='group inline-flex w-full items-center justify-center gap-2 rounded-[10px] border border-[#6F3DFA] bg-gradient-to-b from-[#8357FF] to-[#6F3DFA] py-[6px] pr-[10px] pl-[12px] text-[15px] text-white shadow-[inset_0_2px_4px_0_#9B77FF] transition-all'
|
||||
onClick={action.onClick}
|
||||
disabled={action.disabled || action.loading}
|
||||
>
|
||||
<span className='flex items-center gap-1'>
|
||||
{action.loading ? `${action.label}...` : action.label}
|
||||
<span className='inline-flex transition-transform duration-200 group-hover:translate-x-0.5'>
|
||||
{isHovered ? (
|
||||
<ArrowRight className='h-4 w-4' aria-hidden='true' />
|
||||
) : (
|
||||
<ChevronRight className='h-4 w-4' aria-hidden='true' />
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={index}
|
||||
variant={action.variant}
|
||||
className={
|
||||
action.variant === 'outline'
|
||||
? 'w-full rounded-[10px] border-[var(--brand-primary-hex)] font-medium text-[15px] text-[var(--brand-primary-hex)] transition-colors duration-200 hover:bg-[var(--brand-primary-hex)] hover:text-white'
|
||||
: 'w-full rounded-[10px] text-muted-foreground hover:bg-secondary hover:text-foreground'
|
||||
}
|
||||
onClick={action.onClick}
|
||||
disabled={action.disabled || action.loading}
|
||||
>
|
||||
{action.loading ? `${action.label}...` : action.label}
|
||||
</Button>
|
||||
))}
|
||||
}
|
||||
onClick={action.onClick}
|
||||
disabled={action.disabled || action.loading}
|
||||
>
|
||||
{action.loading ? `${action.label}...` : action.label}
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`${inter.className} auth-text-muted fixed right-0 bottom-0 left-0 z-50 pb-8 text-center font-[340] text-[13px] leading-relaxed`}
|
||||
className={`${inter.className} auth-text-muted absolute right-0 bottom-0 left-0 px-8 pb-8 text-center font-[340] text-[13px] leading-relaxed sm:px-8 md:px-[44px]`}
|
||||
>
|
||||
Need help?{' '}
|
||||
<a
|
||||
@@ -179,6 +146,6 @@ export function InviteStatusCard({
|
||||
Contact support
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -74,6 +74,7 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
Textarea,
|
||||
TimePicker,
|
||||
Tooltip,
|
||||
Trash,
|
||||
Trash2,
|
||||
@@ -125,6 +126,7 @@ export default function PlaygroundPage() {
|
||||
const [switchValue, setSwitchValue] = useState(false)
|
||||
const [checkboxValue, setCheckboxValue] = useState(false)
|
||||
const [sliderValue, setSliderValue] = useState([50])
|
||||
const [timeValue, setTimeValue] = useState('09:30')
|
||||
const [activeTab, setActiveTab] = useState('profile')
|
||||
const [isDarkMode, setIsDarkMode] = useState(false)
|
||||
|
||||
@@ -491,6 +493,31 @@ export default function PlaygroundPage() {
|
||||
</VariantRow>
|
||||
</Section>
|
||||
|
||||
{/* TimePicker */}
|
||||
<Section title='TimePicker'>
|
||||
<VariantRow label='default'>
|
||||
<div className='w-48'>
|
||||
<TimePicker value={timeValue} onChange={setTimeValue} placeholder='Select time' />
|
||||
</div>
|
||||
<span className='text-[var(--text-secondary)] text-sm'>{timeValue}</span>
|
||||
</VariantRow>
|
||||
<VariantRow label='size sm'>
|
||||
<div className='w-48'>
|
||||
<TimePicker value='14:00' onChange={() => {}} placeholder='Small size' size='sm' />
|
||||
</div>
|
||||
</VariantRow>
|
||||
<VariantRow label='no value'>
|
||||
<div className='w-48'>
|
||||
<TimePicker placeholder='Select time...' onChange={() => {}} />
|
||||
</div>
|
||||
</VariantRow>
|
||||
<VariantRow label='disabled'>
|
||||
<div className='w-48'>
|
||||
<TimePicker value='09:00' disabled />
|
||||
</div>
|
||||
</VariantRow>
|
||||
</Section>
|
||||
|
||||
{/* Breadcrumb */}
|
||||
<Section title='Breadcrumb'>
|
||||
<Breadcrumb
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user