Compare commits

..

4 Commits

Author SHA1 Message Date
Vikhyath Mondreti
4b0b0d03c9 add check consistently for baseline diff 2026-01-25 13:22:07 -08:00
Vikhyath Mondreti
2f297686f6 remove redundant check in a2a 2026-01-25 12:49:16 -08:00
Vikhyath Mondreti
a6adbafe16 fix subblock update 2026-01-25 12:42:45 -08:00
Vikhyath Mondreti
19cde17eb8 fix(supabase): storage upload + add basic mode version 2026-01-25 12:18:48 -08:00
59 changed files with 286 additions and 740 deletions

View File

@@ -44,7 +44,7 @@ services:
deploy:
resources:
limits:
memory: 1G
memory: 4G
environment:
- NODE_ENV=development
- DATABASE_URL=postgresql://postgres:postgres@db:5432/simstudio

View File

@@ -1,40 +0,0 @@
'use client'
import { getAssetUrl } from '@/lib/utils'
interface ActionImageProps {
src: string
alt: string
}
interface ActionVideoProps {
src: string
alt: string
}
export function ActionImage({ src, alt }: ActionImageProps) {
const resolvedSrc = getAssetUrl(src.startsWith('/') ? src.slice(1) : src)
return (
<img
src={resolvedSrc}
alt={alt}
className='inline-block w-full max-w-[200px] rounded border border-neutral-200 dark:border-neutral-700'
/>
)
}
export function ActionVideo({ src, alt }: ActionVideoProps) {
const resolvedSrc = getAssetUrl(src.startsWith('/') ? src.slice(1) : src)
return (
<video
src={resolvedSrc}
autoPlay
loop
muted
playsInline
className='inline-block w-full max-w-[200px] rounded border border-neutral-200 dark:border-neutral-700'
/>
)
}

View File

@@ -10,20 +10,12 @@ Stellen Sie Sim auf Ihrer eigenen Infrastruktur mit Docker oder Kubernetes berei
## Anforderungen
| Ressource | Klein | Standard | Produktion |
|----------|-------|----------|------------|
| CPU | 2 Kerne | 4 Kerne | 8+ Kerne |
| RAM | 12 GB | 16 GB | 32+ GB |
| Speicher | 20 GB SSD | 50 GB SSD | 100+ GB SSD |
| Docker | 20.10+ | 20.10+ | Neueste Version |
**Klein**: Entwicklung, Tests, Einzelnutzer (1-5 Nutzer)
**Standard**: Teams (5-50 Nutzer), moderate Arbeitslasten
**Produktion**: Große Teams (50+ Nutzer), Hochverfügbarkeit, intensive Workflow-Ausführung
<Callout type="info">
Die Ressourcenanforderungen werden durch Workflow-Ausführung (isolated-vm Sandboxing), Dateiverarbeitung (In-Memory-Dokumentenparsing) und Vektoroperationen (pgvector) bestimmt. Arbeitsspeicher ist typischerweise der limitierende Faktor, nicht CPU. Produktionsdaten zeigen, dass die Hauptanwendung durchschnittlich 4-8 GB und bei hoher Last bis zu 12 GB benötigt.
</Callout>
| Ressource | Minimum | Empfohlen |
|----------|---------|-------------|
| CPU | 2 Kerne | 4+ Kerne |
| RAM | 12 GB | 16+ GB |
| Speicher | 20 GB SSD | 50+ GB SSD |
| Docker | 20.10+ | Neueste Version |
## Schnellstart

View File

@@ -4,7 +4,6 @@ description: Essential actions for navigating and using the Sim workflow editor
---
import { Callout } from 'fumadocs-ui/components/callout'
import { ActionImage, ActionVideo } from '@/components/ui/action-media'
A quick lookup for everyday actions in the Sim workflow editor. For keyboard shortcuts, see [Keyboard Shortcuts](/keyboard-shortcuts).
@@ -14,362 +13,124 @@ A quick lookup for everyday actions in the Sim workflow editor. For keyboard sho
## Workspaces
<table>
<thead>
<tr><th>Action</th><th>How</th><th>Preview</th></tr>
</thead>
<tbody>
<tr>
<td>Create a workspace</td>
<td>Click workspace dropdown → **New Workspace**</td>
<td><ActionVideo src="/static/quick-reference/create-workspace.mp4" alt="Create workspace" /></td>
</tr>
<tr>
<td>Switch workspaces</td>
<td>Click workspace dropdown → Select workspace</td>
<td><ActionVideo src="/static/quick-reference/switch-workspace.mp4" alt="Switch workspaces" /></td>
</tr>
<tr>
<td>Invite team members</td>
<td>Sidebar → **Invite**</td>
<td><ActionVideo src="/static/quick-reference/invite.mp4" alt="Invite team members" /></td>
</tr>
<tr>
<td>Rename a workspace</td>
<td>Right-click workspace → **Rename**</td>
<td rowSpan={4}><ActionImage src="/static/quick-reference/workspace-context-menu.png" alt="Workspace context menu" /></td>
</tr>
<tr>
<td>Duplicate a workspace</td>
<td>Right-click workspace → **Duplicate**</td>
</tr>
<tr>
<td>Export a workspace</td>
<td>Right-click workspace → **Export**</td>
</tr>
<tr>
<td>Delete a workspace</td>
<td>Right-click workspace → **Delete**</td>
</tr>
</tbody>
</table>
| Action | How |
|--------|-----|
| Create a workspace | Click workspace dropdown in sidebar → **New Workspace** |
| Rename a workspace | Workspace settings → Edit name |
| Switch workspaces | Click workspace dropdown in sidebar → Select workspace |
| Invite team members | Workspace settings → **Team** → **Invite** |
## Workflows
<table>
<thead>
<tr><th>Action</th><th>How</th><th>Preview</th></tr>
</thead>
<tbody>
<tr>
<td>Create a workflow</td>
<td>Click **+** button in sidebar</td>
<td><ActionImage src="/static/quick-reference/create-workflow.png" alt="Create workflow" /></td>
</tr>
<tr>
<td>Reorder / move workflows</td>
<td>Drag workflow up/down or onto a folder</td>
<td><ActionVideo src="/static/quick-reference/reordering.mp4" alt="Reorder workflows" /></td>
</tr>
<tr>
<td>Import a workflow</td>
<td>Click import button in sidebar → Select file</td>
<td><ActionImage src="/static/quick-reference/import-workflow.png" alt="Import workflow" /></td>
</tr>
<tr>
<td>Multi-select workflows</td>
<td>`Mod+Click` or `Shift+Click` workflows in sidebar</td>
<td><ActionVideo src="/static/quick-reference/multiselect.mp4" alt="Multi-select workflows" /></td>
</tr>
<tr>
<td>Open in new tab</td>
<td>Right-click workflow → **Open in New Tab**</td>
<td rowSpan={6}><ActionImage src="/static/quick-reference/workflow-context-menu.png" alt="Workflow context menu" /></td>
</tr>
<tr>
<td>Rename a workflow</td>
<td>Right-click workflow → **Rename**</td>
</tr>
<tr>
<td>Assign workflow color</td>
<td>Right-click workflow → **Change Color**</td>
</tr>
<tr>
<td>Duplicate a workflow</td>
<td>Right-click workflow → **Duplicate**</td>
</tr>
<tr>
<td>Export a workflow</td>
<td>Right-click workflow → **Export**</td>
</tr>
<tr>
<td>Delete a workflow</td>
<td>Right-click workflow → **Delete**</td>
</tr>
<tr>
<td>Rename a folder</td>
<td>Right-click folder → **Rename**</td>
<td rowSpan={6}><ActionImage src="/static/quick-reference/folder-context-menu.png" alt="Folder context menu" /></td>
</tr>
<tr>
<td>Create workflow in folder</td>
<td>Right-click folder → **Create workflow**</td>
</tr>
<tr>
<td>Create folder in folder</td>
<td>Right-click folder → **Create folder**</td>
</tr>
<tr>
<td>Duplicate a folder</td>
<td>Right-click folder → **Duplicate**</td>
</tr>
<tr>
<td>Export a folder</td>
<td>Right-click folder → **Export**</td>
</tr>
<tr>
<td>Delete a folder</td>
<td>Right-click folder → **Delete**</td>
</tr>
</tbody>
</table>
| Action | How |
|--------|-----|
| Create a workflow | Click **New Workflow** button or `Mod+Shift+A` |
| Rename a workflow | Double-click workflow name in sidebar, or right-click → **Rename** |
| Duplicate a workflow | Right-click workflow → **Duplicate** |
| Reorder workflows | Drag workflow up/down in the sidebar list |
| Import a workflow | Sidebar menu → **Import** → Select file |
| Create a folder | Right-click in sidebar → **New Folder** |
| Rename a folder | Right-click folder → **Rename** |
| Delete a folder | Right-click folder → **Delete** |
| Collapse/expand folder | Click folder arrow, or double-click folder |
| Move workflow to folder | Drag workflow onto folder in sidebar |
| Delete a workflow | Right-click workflow → **Delete** |
| Export a workflow | Right-click workflow → **Export** |
| Assign workflow color | Right-click workflow → **Change Color** |
| Multi-select workflows | `Mod+Click` or `Shift+Click` workflows in sidebar |
| Open in new tab | Right-click workflow → **Open in New Tab** |
## Blocks
<table>
<thead>
<tr><th>Action</th><th>How</th><th>Preview</th></tr>
</thead>
<tbody>
<tr>
<td>Add a block</td>
<td>Drag from Toolbar panel, or right-click canvas → **Add Block**</td>
<td><ActionVideo src="/static/quick-reference/add-block.mp4" alt="Add a block" /></td>
</tr>
<tr>
<td>Multi-select blocks</td>
<td>`Mod+Click` additional blocks, or shift-drag to draw selection box</td>
<td><ActionVideo src="/static/quick-reference/multiselect-blocks.mp4" alt="Multi-select blocks" /></td>
</tr>
<tr>
<td>Copy blocks</td>
<td>`Mod+C` with blocks selected</td>
<td rowSpan={2}><ActionVideo src="/static/quick-reference/copy-paste.mp4" alt="Copy and paste blocks" /></td>
</tr>
<tr>
<td>Paste blocks</td>
<td>`Mod+V` to paste copied blocks</td>
</tr>
<tr>
<td>Duplicate blocks</td>
<td>Right-click → **Duplicate**</td>
<td><ActionVideo src="/static/quick-reference/duplicate-block.mp4" alt="Duplicate blocks" /></td>
</tr>
<tr>
<td>Delete blocks</td>
<td>`Delete` or `Backspace` key, or right-click → **Delete**</td>
<td><ActionImage src="/static/quick-reference/delete-block.png" alt="Delete block" /></td>
</tr>
<tr>
<td>Rename a block</td>
<td>Click block name in header, or edit in the Editor panel</td>
<td><ActionVideo src="/static/quick-reference/rename-block.mp4" alt="Rename a block" /></td>
</tr>
<tr>
<td>Enable/Disable a block</td>
<td>Right-click → **Enable/Disable**</td>
<td><ActionImage src="/static/quick-reference/disable-block.png" alt="Disable block" /></td>
</tr>
<tr>
<td>Toggle handle orientation</td>
<td>Right-click → **Toggle Handles**</td>
<td><ActionVideo src="/static/quick-reference/toggle-handles.mp4" alt="Toggle handle orientation" /></td>
</tr>
<tr>
<td>Configure a block</td>
<td>Select block → use Editor panel on right</td>
<td><ActionVideo src="/static/quick-reference/configure-block.mp4" alt="Configure a block" /></td>
</tr>
</tbody>
</table>
| Action | How |
|--------|-----|
| Add a block | Drag from Toolbar panel, or right-click canvas → **Add Block** |
| Select a block | Click on the block |
| Multi-select blocks | `Mod+Click` additional blocks, or right-drag to draw selection box |
| Move blocks | Drag selected block(s) to new position |
| Copy blocks | `Mod+C` with blocks selected |
| Paste blocks | `Mod+V` to paste copied blocks |
| Duplicate blocks | Right-click → **Duplicate** |
| Delete blocks | `Delete` or `Backspace` key, or right-click → **Delete** |
| Rename a block | Click block name in header, or edit in the Editor panel |
| Enable/Disable a block | Right-click → **Enable/Disable** |
| Toggle handle orientation | Right-click → **Toggle Handles** |
| Toggle trigger mode | Right-click trigger block → **Toggle Trigger Mode** |
| Configure a block | Select block → use Editor panel on right |
## Connections
<table>
<thead>
<tr><th>Action</th><th>How</th><th>Preview</th></tr>
</thead>
<tbody>
<tr>
<td>Create a connection</td>
<td>Drag from output handle to input handle</td>
<td><ActionVideo src="/static/quick-reference/connect-blocks.mp4" alt="Connect blocks" /></td>
</tr>
<tr>
<td>Delete a connection</td>
<td>Click edge to select `Delete` key</td>
<td><ActionVideo src="/static/quick-reference/delete-connection.mp4" alt="Delete connection" /></td>
</tr>
<tr>
<td>Use output in another block</td>
<td>Drag connection tag into input field</td>
<td><ActionVideo src="/static/quick-reference/connection-tag.mp4" alt="Use connection tag" /></td>
</tr>
</tbody>
</table>
| Action | How |
|--------|-----|
| Create a connection | Drag from output handle to input handle |
| Delete a connection | Click edge to select → `Delete` key |
| Use output in another block | Drag connection tag into input field |
## Canvas Navigation
| Action | How |
|--------|-----|
| Pan/move canvas | Left-drag on empty space, or scroll/trackpad |
| Zoom in/out | Scroll wheel or pinch gesture |
| Auto-layout | `Shift+L` |
| Draw selection box | Right-drag on empty canvas area |
## Panels & Views
<table>
<thead>
<tr><th>Action</th><th>How</th><th>Preview</th></tr>
</thead>
<tbody>
<tr>
<td>Search toolbar</td>
<td>`Mod+F`</td>
<td><ActionVideo src="/static/quick-reference/search-toolbar.mp4" alt="Search toolbar" /></td>
</tr>
<tr>
<td>Search everything</td>
<td>`Mod+K`</td>
<td><ActionImage src="/static/quick-reference/search-everything.png" alt="Search everything" /></td>
</tr>
<tr>
<td>Toggle manual mode</td>
<td>Click toggle button to switch between manual and selector</td>
<td><ActionImage src="/static/quick-reference/toggle-manual-mode.png" alt="Toggle manual mode" /></td>
</tr>
<tr>
<td>Collapse/expand sidebar</td>
<td>Click collapse button on sidebar</td>
<td><ActionVideo src="/static/quick-reference/collapse-sidebar.mp4" alt="Collapse sidebar" /></td>
</tr>
</tbody>
</table>
| Action | How |
|--------|-----|
| Open Copilot tab | Press `C` or click Copilot tab |
| Open Toolbar tab | Press `T` or click Toolbar tab |
| Open Editor tab | Press `E` or click Editor tab |
| Search toolbar | `Mod+F` |
| Toggle advanced mode | Click toggle button on input fields |
| Resize panels | Drag panel edge |
| Collapse/expand sidebar | Click collapse button on sidebar |
## Running & Testing
<table>
<thead>
<tr><th>Action</th><th>How</th><th>Preview</th></tr>
</thead>
<tbody>
<tr>
<td>Run workflow</td>
<td>Click Run Workflow button or `Mod+Enter`</td>
<td><ActionImage src="/static/quick-reference/run-workflow.png" alt="Run workflow" /></td>
</tr>
<tr>
<td>Stop workflow</td>
<td>Click Stop button or `Mod+Enter` while running</td>
<td><ActionImage src="/static/quick-reference/stop-workflow.png" alt="Stop workflow" /></td>
</tr>
<tr>
<td>Test with chat</td>
<td>Use Chat panel on the right side</td>
<td><ActionImage src="/static/quick-reference/test-chat.png" alt="Test with chat" /></td>
</tr>
<tr>
<td>Select output to view</td>
<td>Click dropdown in Chat panel → Select block output</td>
<td><ActionImage src="/static/quick-reference/output-select.png" alt="Select output to view" /></td>
</tr>
<tr>
<td>Clear chat history</td>
<td>Click clear button in Chat panel</td>
<td><ActionImage src="/static/quick-reference/clear-chat.png" alt="Clear chat history" /></td>
</tr>
<tr>
<td>View execution logs</td>
<td>Open terminal panel at bottom, or `Mod+L`</td>
<td><ActionImage src="/static/quick-reference/terminal.png" alt="Execution logs terminal" /></td>
</tr>
<tr>
<td>Filter logs by block or status</td>
<td>Click block filter in terminal or right-click log entry → **Filter by Block** or **Filter by Status**</td>
<td><ActionImage src="/static/quick-reference/filter-block.png" alt="Filter logs by block" /></td>
</tr>
<tr>
<td>Search logs</td>
<td>Use search field in terminal or right-click log entry → **Search**</td>
<td><ActionImage src="/static/quick-reference/terminal-search.png" alt="Search logs" /></td>
</tr>
<tr>
<td>Copy log entry</td>
<td>Clipboard Icon or Right-click log entry → **Copy**</td>
<td><ActionImage src="/static/quick-reference/copy-log.png" alt="Copy log entry" /></td>
</tr>
<tr>
<td>Clear terminal</td>
<td>Trash icon or `Mod+D`</td>
<td><ActionImage src="/static/quick-reference/clear-terminal.png" alt="Clear terminal" /></td>
</tr>
</tbody>
</table>
| Action | How |
|--------|-----|
| Run workflow | Click Play button or `Mod+Enter` |
| Stop workflow | Click Stop button or `Mod+Enter` while running |
| Test with chat | Use Chat panel on the right side |
| Select output to view | Click dropdown in Chat panel → Select block output |
| Clear chat history | Click clear button in Chat panel |
| View execution logs | Open terminal panel at bottom, or `Mod+L` |
| Filter logs by block | Click block filter in terminal |
| Filter logs by status | Click status filter in terminal |
| Search logs | Use search field in terminal |
| Copy log entry | Right-click log entry → **Copy** |
| Clear terminal | `Mod+D` |
## Deployment
<table>
<thead>
<tr><th>Action</th><th>How</th><th>Preview</th></tr>
</thead>
<tbody>
<tr>
<td>Deploy a workflow</td>
<td>Click **Deploy** button in panel</td>
<td><ActionImage src="/static/quick-reference/deploy.png" alt="Deploy workflow" /></td>
</tr>
<tr>
<td>Update deployment</td>
<td>Click **Update** when changes are detected</td>
<td><ActionImage src="/static/quick-reference/update-deployment.png" alt="Update deployment" /></td>
</tr>
<tr>
<td>View deployment status</td>
<td>Check status indicator (Live/Update/Deploy) in Deploy tab</td>
<td><ActionImage src="/static/quick-reference/view-deployment.png" alt="View deployment status" /></td>
</tr>
<tr>
<td>Revert deployment</td>
<td>Access previous versions in Deploy tab → **Promote to live**</td>
<td><ActionImage src="/static/quick-reference/promote-deployment.png" alt="Promote deployment to live" /></td>
</tr>
<tr>
<td>Copy API endpoint</td>
<td>Deploy tab → Copy API endpoint URL</td>
<td><ActionImage src="/static/quick-reference/copy-api.png" alt="Copy API endpoint" /></td>
</tr>
</tbody>
</table>
| Action | How |
|--------|-----|
| Deploy a workflow | Click **Deploy** button in Deploy tab |
| Update deployment | Click **Update** when changes are detected |
| View deployment status | Check status indicator (Live/Update/Deploy) in Deploy tab |
| Revert deployment | Access previous versions in Deploy tab |
| Copy webhook URL | Deploy tab → Copy webhook URL |
| Copy API endpoint | Deploy tab → Copy API endpoint URL |
| Set up a schedule | Add Schedule trigger block → Configure interval |
## Variables
<table>
<thead>
<tr><th>Action</th><th>How</th><th>Preview</th></tr>
</thead>
<tbody>
<tr>
<td>Add / Edit / Delete workflow variable</td>
<td>Panel -> Variables -> **Add Variable**, click to edit, or delete icon</td>
<td><ActionImage src="/static/quick-reference/variables.png" alt="Variables panel" /></td>
</tr>
<tr>
<td>Add environment variable</td>
<td>Settings → **Environment Variables** → **Add**</td>
<td><ActionImage src="/static/quick-reference/add-env-variable.png" alt="Add environment variable" /></td>
</tr>
<tr>
<td>Reference a workflow variable</td>
<td>Use `<blockName.itemName>` syntax in block inputs</td>
<td><ActionImage src="/static/quick-reference/variable-reference.png" alt="Reference workflow variable" /></td>
</tr>
<tr>
<td>Reference an environment variable</td>
<td>Use `&#123;&#123;ENV_VAR&#125;&#125;` syntax in block inputs</td>
<td><ActionImage src="/static/quick-reference/env-variable-reference.png" alt="Reference environment variable" /></td>
</tr>
</tbody>
</table>
| Action | How |
|--------|-----|
| Add workflow variable | Variables tab → **Add Variable** |
| Edit workflow variable | Variables tab → Click variable to edit |
| Delete workflow variable | Variables tab → Click delete icon on variable |
| Add environment variable | Settings → **Environment Variables** → **Add** |
| Reference a variable | Use `{{variableName}}` syntax in block inputs |
## Credentials
| Action | How |
|--------|-----|
| Add API key | Block credential field → **Add Credential** → Enter API key |
| Connect OAuth account | Block credential field → **Connect** → Authorize with provider |
| Manage credentials | Settings → **Credentials** |
| Remove credential | Settings → **Credentials** → Delete credential |

View File

@@ -16,20 +16,12 @@ Deploy Sim on your own infrastructure with Docker or Kubernetes.
## Requirements
| Resource | Small | Standard | Production |
|----------|-------|----------|------------|
| CPU | 2 cores | 4 cores | 8+ cores |
| RAM | 12 GB | 16 GB | 32+ GB |
| Storage | 20 GB SSD | 50 GB SSD | 100+ GB SSD |
| Docker | 20.10+ | 20.10+ | Latest |
**Small**: Development, testing, single user (1-5 users)
**Standard**: Teams (5-50 users), moderate workloads
**Production**: Large teams (50+ users), high availability, heavy workflow execution
<Callout type="info">
Resource requirements are driven by workflow execution (isolated-vm sandboxing), file processing (in-memory document parsing), and vector operations (pgvector). Memory is typically the constraining factor rather than CPU. Production telemetry shows the main app uses 4-8 GB average with peaks up to 12 GB under heavy load.
</Callout>
| Resource | Minimum | Recommended |
|----------|---------|-------------|
| CPU | 2 cores | 4+ cores |
| RAM | 12 GB | 16+ GB |
| Storage | 20 GB SSD | 50+ GB SSD |
| Docker | 20.10+ | Latest |
## Quick Start

View File

@@ -10,20 +10,12 @@ Despliega Sim en tu propia infraestructura con Docker o Kubernetes.
## Requisitos
| Recurso | Pequeño | Estándar | Producción |
|----------|---------|----------|------------|
| CPU | 2 núcleos | 4 núcleos | 8+ núcleos |
| RAM | 12 GB | 16 GB | 32+ GB |
| Almacenamiento | 20 GB SSD | 50 GB SSD | 100+ GB SSD |
| Docker | 20.10+ | 20.10+ | Última versión |
**Pequeño**: Desarrollo, pruebas, usuario único (1-5 usuarios)
**Estándar**: Equipos (5-50 usuarios), cargas de trabajo moderadas
**Producción**: Equipos grandes (50+ usuarios), alta disponibilidad, ejecución intensiva de workflows
<Callout type="info">
Los requisitos de recursos están determinados por la ejecución de workflows (sandboxing isolated-vm), procesamiento de archivos (análisis de documentos en memoria) y operaciones vectoriales (pgvector). La memoria suele ser el factor limitante, no la CPU. La telemetría de producción muestra que la aplicación principal usa 4-8 GB en promedio con picos de hasta 12 GB bajo carga pesada.
</Callout>
| Recurso | Mínimo | Recomendado |
|----------|---------|-------------|
| CPU | 2 núcleos | 4+ núcleos |
| RAM | 12 GB | 16+ GB |
| Almacenamiento | 20 GB SSD | 50+ GB SSD |
| Docker | 20.10+ | Última versión |
## Inicio rápido

View File

@@ -10,20 +10,12 @@ Déployez Sim sur votre propre infrastructure avec Docker ou Kubernetes.
## Prérequis
| Ressource | Petit | Standard | Production |
|----------|-------|----------|------------|
| CPU | 2 cœurs | 4 cœurs | 8+ cœurs |
| RAM | 12 Go | 16 Go | 32+ Go |
| Stockage | 20 Go SSD | 50 Go SSD | 100+ Go SSD |
| Docker | 20.10+ | 20.10+ | Dernière version |
**Petit** : Développement, tests, utilisateur unique (1-5 utilisateurs)
**Standard** : Équipes (5-50 utilisateurs), charges de travail modérées
**Production** : Grandes équipes (50+ utilisateurs), haute disponibilité, exécution intensive de workflows
<Callout type="info">
Les besoins en ressources sont déterminés par l'exécution des workflows (sandboxing isolated-vm), le traitement des fichiers (analyse de documents en mémoire) et les opérations vectorielles (pgvector). La mémoire est généralement le facteur limitant, pas le CPU. La télémétrie de production montre que l'application principale utilise 4-8 Go en moyenne avec des pics jusqu'à 12 Go sous forte charge.
</Callout>
| Ressource | Minimum | Recommandé |
|----------|---------|-------------|
| CPU | 2 cœurs | 4+ cœurs |
| RAM | 12 Go | 16+ Go |
| Stockage | 20 Go SSD | 50+ Go SSD |
| Docker | 20.10+ | Dernière version |
## Démarrage rapide

View File

@@ -10,20 +10,12 @@ DockerまたはKubernetesを使用して、自社のインフラストラクチ
## 要件
| リソース | スモール | スタンダード | プロダクション |
|----------|---------|-------------|----------------|
| CPU | 2コア | 4コア | 8+コア |
| RAM | 12 GB | 16 GB | 32+ GB |
| ストレージ | 20 GB SSD | 50 GB SSD | 100+ GB SSD |
| Docker | 20.10+ | 20.10+ | 最新版 |
**スモール**: 開発、テスト、シングルユーザー1-5ユーザー
**スタンダード**: チーム5-50ユーザー、中程度のワークロード
**プロダクション**: 大規模チーム50+ユーザー)、高可用性、高負荷ワークフロー実行
<Callout type="info">
リソース要件は、ワークフロー実行isolated-vmサンドボックス、ファイル処理メモリ内ドキュメント解析、ベクトル演算pgvectorによって決まります。CPUよりもメモリが制約要因となることが多いです。本番環境のテレメトリによると、メインアプリは平均4-8 GB、高負荷時は最大12 GBを使用します。
</Callout>
| リソース | 最小 | 推奨 |
|----------|---------|-------------|
| CPU | 2コア | 4+コア |
| RAM | 12 GB | 16+ GB |
| ストレージ | 20 GB SSD | 50+ GB SSD |
| Docker | 20.10+ | 最新版 |
## クイックスタート

View File

@@ -10,20 +10,12 @@ import { Callout } from 'fumadocs-ui/components/callout'
## 要求
| 资源 | 小型 | 标准 | 生产环境 |
|----------|------|------|----------|
| CPU | 2 核 | 4 核 | 8+ 核 |
| 内存 | 12 GB | 16 GB | 32+ GB |
| 存储 | 20 GB SSD | 50 GB SSD | 100+ GB SSD |
| Docker | 20.10+ | 20.10+ | 最新版本 |
**小型**: 开发、测试、单用户1-5 用户)
**标准**: 团队5-50 用户)、中等工作负载
**生产环境**: 大型团队50+ 用户)、高可用性、密集工作流执行
<Callout type="info">
资源需求由工作流执行isolated-vm 沙箱、文件处理内存中文档解析和向量运算pgvector决定。内存通常是限制因素而不是 CPU。生产遥测数据显示主应用平均使用 4-8 GB高负载时峰值可达 12 GB。
</Callout>
| 资源 | 最低要求 | 推荐配置 |
|----------|---------|-------------|
| CPU | 2 核 | 4 核及以上 |
| 内存 | 12 GB | 16 GB 及以上 |
| 存储 | 20 GB SSD | 50 GB 及以上 SSD |
| Docker | 20.10+ | 最新版本 |
## 快速开始

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

View File

@@ -8,7 +8,6 @@ import { executeInIsolatedVM } from '@/lib/execution/isolated-vm'
import { CodeLanguage, DEFAULT_CODE_LANGUAGE, isValidCodeLanguage } from '@/lib/execution/languages'
import { escapeRegExp, normalizeName, REFERENCE } from '@/executor/constants'
import { type OutputSchema, resolveBlockReference } from '@/executor/utils/block-reference'
import { formatLiteralForCode } from '@/executor/utils/code-formatting'
import {
createEnvVarPattern,
createWorkflowVariablePattern,
@@ -388,12 +387,7 @@ function resolveWorkflowVariables(
if (type === 'number') {
variableValue = Number(variableValue)
} else if (type === 'boolean') {
if (typeof variableValue === 'boolean') {
// Already a boolean, keep as-is
} else {
const normalized = String(variableValue).toLowerCase().trim()
variableValue = normalized === 'true'
}
variableValue = variableValue === 'true' || variableValue === true
} else if (type === 'json' && typeof variableValue === 'string') {
try {
variableValue = JSON.parse(variableValue)
@@ -693,7 +687,11 @@ export async function POST(req: NextRequest) {
prologue += `const environmentVariables = JSON.parse(${JSON.stringify(JSON.stringify(envVars))});\n`
prologueLineCount++
for (const [k, v] of Object.entries(contextVariables)) {
prologue += `const ${k} = ${formatLiteralForCode(v, 'javascript')};\n`
if (v === undefined) {
prologue += `const ${k} = undefined;\n`
} else {
prologue += `const ${k} = JSON.parse(${JSON.stringify(JSON.stringify(v))});\n`
}
prologueLineCount++
}
@@ -764,7 +762,11 @@ export async function POST(req: NextRequest) {
prologue += `environmentVariables = json.loads(${JSON.stringify(JSON.stringify(envVars))})\n`
prologueLineCount++
for (const [k, v] of Object.entries(contextVariables)) {
prologue += `${k} = ${formatLiteralForCode(v, 'python')}\n`
if (v === undefined) {
prologue += `${k} = None\n`
} else {
prologue += `${k} = json.loads(${JSON.stringify(JSON.stringify(v))})\n`
}
prologueLineCount++
}
const wrapped = [

View File

@@ -408,7 +408,6 @@ describe('Knowledge Search Utils', () => {
input: ['test query'],
model: 'text-embedding-3-small',
encoding_format: 'float',
dimensions: 1536,
}),
})
)

View File

@@ -581,18 +581,6 @@ export const GmailV2Block: BlockConfig<GmailToolResponse> = {
results: { type: 'json', description: 'Search/read summary results' },
attachments: { type: 'json', description: 'Downloaded attachments (if enabled)' },
// Draft-specific outputs
draftId: {
type: 'string',
description: 'Draft ID',
condition: { field: 'operation', value: 'draft_gmail' },
},
messageId: {
type: 'string',
description: 'Gmail message ID for the draft',
condition: { field: 'operation', value: 'draft_gmail' },
},
// Trigger outputs (unchanged)
email_id: { type: 'string', description: 'Gmail message ID' },
thread_id: { type: 'string', description: 'Gmail thread ID' },

View File

@@ -1,9 +1,7 @@
import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs'
import { normalizeName } from '@/executor/constants'
import type { ExecutionContext } from '@/executor/types'
import type { OutputSchema } from '@/executor/utils/block-reference'
import type { SerializedBlock } from '@/serializer/types'
import type { ToolConfig } from '@/tools/types'
import { getTool } from '@/tools/utils'
export interface BlockDataCollection {
blockData: Record<string, unknown>
@@ -11,32 +9,6 @@ export interface BlockDataCollection {
blockOutputSchemas: Record<string, OutputSchema>
}
export function getBlockSchema(
block: SerializedBlock,
toolConfig?: ToolConfig
): OutputSchema | undefined {
const isTrigger =
block.metadata?.category === 'triggers' ||
(block.config?.params as Record<string, unknown> | undefined)?.triggerMode === true
// Triggers use saved outputs (defines the trigger payload schema)
if (isTrigger && block.outputs && Object.keys(block.outputs).length > 0) {
return block.outputs as OutputSchema
}
// When a tool is selected, tool outputs are the source of truth
if (toolConfig?.outputs && Object.keys(toolConfig.outputs).length > 0) {
return toolConfig.outputs as OutputSchema
}
// Fallback to saved outputs for blocks without tools
if (block.outputs && Object.keys(block.outputs).length > 0) {
return block.outputs as OutputSchema
}
return undefined
}
export function collectBlockData(ctx: ExecutionContext): BlockDataCollection {
const blockData: Record<string, unknown> = {}
const blockNameMapping: Record<string, string> = {}
@@ -46,21 +18,24 @@ export function collectBlockData(ctx: ExecutionContext): BlockDataCollection {
if (state.output !== undefined) {
blockData[id] = state.output
}
}
const workflowBlocks = ctx.workflow?.blocks ?? []
for (const block of workflowBlocks) {
const id = block.id
const workflowBlock = ctx.workflow?.blocks?.find((b) => b.id === id)
if (!workflowBlock) continue
if (block.metadata?.name) {
blockNameMapping[normalizeName(block.metadata.name)] = id
if (workflowBlock.metadata?.name) {
blockNameMapping[normalizeName(workflowBlock.metadata.name)] = id
}
const toolId = block.config?.tool
const toolConfig = toolId ? getTool(toolId) : undefined
const schema = getBlockSchema(block, toolConfig)
if (schema && Object.keys(schema).length > 0) {
blockOutputSchemas[id] = schema
const blockType = workflowBlock.metadata?.id
if (blockType) {
const params = workflowBlock.config?.params as Record<string, unknown> | undefined
const subBlocks = params
? Object.fromEntries(Object.entries(params).map(([k, v]) => [k, { value: v }]))
: undefined
const schema = getBlockOutputs(blockType, subBlocks)
if (schema && Object.keys(schema).length > 0) {
blockOutputSchemas[id] = schema
}
}
}

View File

@@ -1,48 +0,0 @@
/**
* Formats a JavaScript/TypeScript value as a code literal for the target language.
* Handles special cases like null, undefined, booleans, and Python-specific number representations.
*
* @param value - The value to format
* @param language - Target language ('javascript' or 'python')
* @returns A string literal representation valid in the target language
*
* @example
* formatLiteralForCode(null, 'python') // => 'None'
* formatLiteralForCode(true, 'python') // => 'True'
* formatLiteralForCode(NaN, 'python') // => "float('nan')"
* formatLiteralForCode("hello", 'javascript') // => '"hello"'
* formatLiteralForCode({a: 1}, 'python') // => "json.loads('{\"a\":1}')"
*/
export function formatLiteralForCode(value: unknown, language: 'javascript' | 'python'): string {
const isPython = language === 'python'
if (value === undefined) {
return isPython ? 'None' : 'undefined'
}
if (value === null) {
return isPython ? 'None' : 'null'
}
if (typeof value === 'boolean') {
return isPython ? (value ? 'True' : 'False') : String(value)
}
if (typeof value === 'number') {
if (Number.isNaN(value)) {
return isPython ? "float('nan')" : 'NaN'
}
if (value === Number.POSITIVE_INFINITY) {
return isPython ? "float('inf')" : 'Infinity'
}
if (value === Number.NEGATIVE_INFINITY) {
return isPython ? "float('-inf')" : '-Infinity'
}
return String(value)
}
if (typeof value === 'string') {
return JSON.stringify(value)
}
// Objects and arrays - Python needs json.loads() because JSON true/false/null aren't valid Python
if (isPython) {
return `json.loads(${JSON.stringify(JSON.stringify(value))})`
}
return JSON.stringify(value)
}

View File

@@ -378,30 +378,8 @@ function buildManualTriggerOutput(
return mergeFilesIntoOutput(output, workflowInput)
}
function buildIntegrationTriggerOutput(
workflowInput: unknown,
structuredInput: Record<string, unknown>,
hasStructured: boolean
): NormalizedBlockOutput {
const output: NormalizedBlockOutput = {}
if (hasStructured) {
for (const [key, value] of Object.entries(structuredInput)) {
output[key] = value
}
}
if (isPlainObject(workflowInput)) {
for (const [key, value] of Object.entries(workflowInput)) {
if (value !== undefined && value !== null) {
output[key] = value
} else if (!Object.hasOwn(output, key)) {
output[key] = value
}
}
}
return mergeFilesIntoOutput(output, workflowInput)
function buildIntegrationTriggerOutput(workflowInput: unknown): NormalizedBlockOutput {
return isPlainObject(workflowInput) ? (workflowInput as NormalizedBlockOutput) : {}
}
function extractSubBlocks(block: SerializedBlock): Record<string, unknown> | undefined {
@@ -450,7 +428,7 @@ export function buildStartBlockOutput(options: StartBlockOutputOptions): Normali
return buildManualTriggerOutput(finalInput, workflowInput)
case StartBlockPath.EXTERNAL_TRIGGER:
return buildIntegrationTriggerOutput(workflowInput, structuredInput, hasStructured)
return buildIntegrationTriggerOutput(workflowInput)
case StartBlockPath.LEGACY_STARTER:
return buildLegacyStarterOutput(

View File

@@ -157,14 +157,7 @@ export class VariableResolver {
let replacementError: Error | null = null
const blockType = block?.metadata?.id
const language =
blockType === BlockType.FUNCTION
? ((block?.config?.params as Record<string, unknown> | undefined)?.language as
| string
| undefined)
: undefined
// Use generic utility for smart variable reference replacement
let result = replaceValidReferences(template, (match) => {
if (replacementError) return match
@@ -174,7 +167,14 @@ export class VariableResolver {
return match
}
return this.blockResolver.formatValueForBlock(resolved, blockType, language)
const blockType = block?.metadata?.id
const isInTemplateLiteral =
blockType === BlockType.FUNCTION &&
template.includes('${') &&
template.includes('}') &&
template.includes('`')
return this.blockResolver.formatValueForBlock(resolved, blockType, isInTemplateLiteral)
} catch (error) {
replacementError = error instanceof Error ? error : new Error(String(error))
return match

View File

@@ -257,9 +257,15 @@ describe('BlockResolver', () => {
expect(result).toBe('"hello"')
})
it.concurrent('should format object for function block', () => {
it.concurrent('should format string for function block in template literal', () => {
const resolver = new BlockResolver(createTestWorkflow())
const result = resolver.formatValueForBlock({ a: 1 }, 'function')
const result = resolver.formatValueForBlock('hello', 'function', true)
expect(result).toBe('hello')
})
it.concurrent('should format object for function block in template literal', () => {
const resolver = new BlockResolver(createTestWorkflow())
const result = resolver.formatValueForBlock({ a: 1 }, 'function', true)
expect(result).toBe('{"a":1}')
})

View File

@@ -1,16 +1,15 @@
import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs'
import {
isReference,
normalizeName,
parseReferencePath,
SPECIAL_REFERENCE_PREFIXES,
} from '@/executor/constants'
import { getBlockSchema } from '@/executor/utils/block-data'
import {
InvalidFieldError,
type OutputSchema,
resolveBlockReference,
} from '@/executor/utils/block-reference'
import { formatLiteralForCode } from '@/executor/utils/code-formatting'
import {
navigatePath,
type ResolutionContext,
@@ -68,9 +67,15 @@ export class BlockResolver implements Resolver {
blockData[blockId] = output
}
const blockType = block.metadata?.id
const params = block.config?.params as Record<string, unknown> | undefined
const subBlocks = params
? Object.fromEntries(Object.entries(params).map(([k, v]) => [k, { value: v }]))
: undefined
const toolId = block.config?.tool
const toolConfig = toolId ? getTool(toolId) : undefined
const outputSchema = getBlockSchema(block, toolConfig)
const outputSchema =
toolConfig?.outputs ?? (blockType ? getBlockOutputs(blockType, subBlocks) : block.outputs)
if (outputSchema && Object.keys(outputSchema).length > 0) {
blockOutputSchemas[blockId] = outputSchema
@@ -160,13 +165,17 @@ export class BlockResolver implements Resolver {
return this.nameToBlockId.get(normalizeName(name))
}
public formatValueForBlock(value: any, blockType: string | undefined, language?: string): string {
public formatValueForBlock(
value: any,
blockType: string | undefined,
isInTemplateLiteral = false
): string {
if (blockType === 'condition') {
return this.stringifyForCondition(value)
}
if (blockType === 'function') {
return this.formatValueForCodeContext(value, language)
return this.formatValueForCodeContext(value, isInTemplateLiteral)
}
if (blockType === 'response') {
@@ -207,7 +216,29 @@ export class BlockResolver implements Resolver {
return String(value)
}
private formatValueForCodeContext(value: any, language?: string): string {
return formatLiteralForCode(value, language === 'python' ? 'python' : 'javascript')
private formatValueForCodeContext(value: any, isInTemplateLiteral: boolean): string {
if (isInTemplateLiteral) {
if (typeof value === 'string') {
return value
}
if (typeof value === 'object' && value !== null) {
return JSON.stringify(value)
}
return String(value)
}
if (typeof value === 'string') {
return JSON.stringify(value)
}
if (typeof value === 'object' && value !== null) {
return JSON.stringify(value)
}
if (value === undefined) {
return 'undefined'
}
if (value === null) {
return 'null'
}
return String(value)
}
}

View File

@@ -30,10 +30,7 @@ export function navigatePath(obj: any, path: string[]): any {
const arrayMatch = part.match(/^([^[]+)(\[.+)$/)
if (arrayMatch) {
const [, prop, bracketsPart] = arrayMatch
current =
typeof current === 'object' && current !== null
? (current as Record<string, unknown>)[prop]
: undefined
current = current[prop]
if (current === undefined || current === null) {
return undefined
}
@@ -52,10 +49,7 @@ export function navigatePath(obj: any, path: string[]): any {
const index = Number.parseInt(part, 10)
current = Array.isArray(current) ? current[index] : undefined
} else {
current =
typeof current === 'object' && current !== null
? (current as Record<string, unknown>)[part]
: undefined
current = current[part]
}
}
return current

View File

@@ -132,8 +132,6 @@ async function executeCode(request) {
for (const [key, value] of Object.entries(contextVariables)) {
if (value === undefined) {
await jail.set(key, undefined)
} else if (value === null) {
await jail.set(key, null)
} else {
await jail.set(key, new ivm.ExternalCopy(value).copyInto())
}

View File

@@ -8,17 +8,6 @@ const logger = createLogger('EmbeddingUtils')
const MAX_TOKENS_PER_REQUEST = 8000
const MAX_CONCURRENT_BATCHES = env.KB_CONFIG_CONCURRENCY_LIMIT || 50
const EMBEDDING_DIMENSIONS = 1536
/**
* Check if the model supports custom dimensions.
* text-embedding-3-* models support the dimensions parameter.
* Checks for 'embedding-3' to handle Azure deployments with custom naming conventions.
*/
function supportsCustomDimensions(modelName: string): boolean {
const name = modelName.toLowerCase()
return name.includes('embedding-3') && !name.includes('ada')
}
export class EmbeddingAPIError extends Error {
public status: number
@@ -104,19 +93,15 @@ async function getEmbeddingConfig(
async function callEmbeddingAPI(inputs: string[], config: EmbeddingConfig): Promise<number[][]> {
return retryWithExponentialBackoff(
async () => {
const useDimensions = supportsCustomDimensions(config.modelName)
const requestBody = config.useAzure
? {
input: inputs,
encoding_format: 'float',
...(useDimensions && { dimensions: EMBEDDING_DIMENSIONS }),
}
: {
input: inputs,
model: config.modelName,
encoding_format: 'float',
...(useDimensions && { dimensions: EMBEDDING_DIMENSIONS }),
}
const response = await fetch(config.apiUrl, {

View File

@@ -18,52 +18,6 @@ const logger = createLogger('BlobClient')
let _blobServiceClient: BlobServiceClientInstance | null = null
interface ParsedCredentials {
accountName: string
accountKey: string
}
/**
* Extract account name and key from an Azure connection string.
* Connection strings have the format: DefaultEndpointsProtocol=https;AccountName=...;AccountKey=...;EndpointSuffix=...
*/
function parseConnectionString(connectionString: string): ParsedCredentials {
const accountNameMatch = connectionString.match(/AccountName=([^;]+)/)
if (!accountNameMatch) {
throw new Error('Cannot extract account name from connection string')
}
const accountKeyMatch = connectionString.match(/AccountKey=([^;]+)/)
if (!accountKeyMatch) {
throw new Error('Cannot extract account key from connection string')
}
return {
accountName: accountNameMatch[1],
accountKey: accountKeyMatch[1],
}
}
/**
* Get account credentials from BLOB_CONFIG, extracting from connection string if necessary.
*/
function getAccountCredentials(): ParsedCredentials {
if (BLOB_CONFIG.connectionString) {
return parseConnectionString(BLOB_CONFIG.connectionString)
}
if (BLOB_CONFIG.accountName && BLOB_CONFIG.accountKey) {
return {
accountName: BLOB_CONFIG.accountName,
accountKey: BLOB_CONFIG.accountKey,
}
}
throw new Error(
'Azure Blob Storage credentials are missing set AZURE_CONNECTION_STRING or both AZURE_ACCOUNT_NAME and AZURE_ACCOUNT_KEY'
)
}
export async function getBlobServiceClient(): Promise<BlobServiceClientInstance> {
if (_blobServiceClient) return _blobServiceClient
@@ -173,8 +127,6 @@ export async function getPresignedUrl(key: string, expiresIn = 3600) {
const containerClient = blobServiceClient.getContainerClient(BLOB_CONFIG.containerName)
const blockBlobClient = containerClient.getBlockBlobClient(key)
const { accountName, accountKey } = getAccountCredentials()
const sasOptions = {
containerName: BLOB_CONFIG.containerName,
blobName: key,
@@ -185,7 +137,13 @@ export async function getPresignedUrl(key: string, expiresIn = 3600) {
const sasToken = generateBlobSASQueryParameters(
sasOptions,
new StorageSharedKeyCredential(accountName, accountKey)
new StorageSharedKeyCredential(
BLOB_CONFIG.accountName,
BLOB_CONFIG.accountKey ??
(() => {
throw new Error('AZURE_ACCOUNT_KEY is required when using account name authentication')
})()
)
).toString()
return `${blockBlobClient.url}?${sasToken}`
@@ -210,14 +168,9 @@ export async function getPresignedUrlWithConfig(
StorageSharedKeyCredential,
} = await import('@azure/storage-blob')
let tempBlobServiceClient: BlobServiceClientInstance
let accountName: string
let accountKey: string
if (customConfig.connectionString) {
tempBlobServiceClient = BlobServiceClient.fromConnectionString(customConfig.connectionString)
const credentials = parseConnectionString(customConfig.connectionString)
accountName = credentials.accountName
accountKey = credentials.accountKey
} else if (customConfig.accountName && customConfig.accountKey) {
const sharedKeyCredential = new StorageSharedKeyCredential(
customConfig.accountName,
@@ -227,8 +180,6 @@ export async function getPresignedUrlWithConfig(
`https://${customConfig.accountName}.blob.core.windows.net`,
sharedKeyCredential
)
accountName = customConfig.accountName
accountKey = customConfig.accountKey
} else {
throw new Error(
'Custom blob config must include either connectionString or accountName + accountKey'
@@ -248,7 +199,13 @@ export async function getPresignedUrlWithConfig(
const sasToken = generateBlobSASQueryParameters(
sasOptions,
new StorageSharedKeyCredential(accountName, accountKey)
new StorageSharedKeyCredential(
customConfig.accountName,
customConfig.accountKey ??
(() => {
throw new Error('Account key is required when using account name authentication')
})()
)
).toString()
return `${blockBlobClient.url}?${sasToken}`
@@ -446,9 +403,13 @@ export async function getMultipartPartUrls(
if (customConfig) {
if (customConfig.connectionString) {
blobServiceClient = BlobServiceClient.fromConnectionString(customConfig.connectionString)
const credentials = parseConnectionString(customConfig.connectionString)
accountName = credentials.accountName
accountKey = credentials.accountKey
const match = customConfig.connectionString.match(/AccountName=([^;]+)/)
if (!match) throw new Error('Cannot extract account name from connection string')
accountName = match[1]
const keyMatch = customConfig.connectionString.match(/AccountKey=([^;]+)/)
if (!keyMatch) throw new Error('Cannot extract account key from connection string')
accountKey = keyMatch[1]
} else if (customConfig.accountName && customConfig.accountKey) {
const credential = new StorageSharedKeyCredential(
customConfig.accountName,
@@ -467,9 +428,12 @@ export async function getMultipartPartUrls(
} else {
blobServiceClient = await getBlobServiceClient()
containerName = BLOB_CONFIG.containerName
const credentials = getAccountCredentials()
accountName = credentials.accountName
accountKey = credentials.accountKey
accountName = BLOB_CONFIG.accountName
accountKey =
BLOB_CONFIG.accountKey ||
(() => {
throw new Error('AZURE_ACCOUNT_KEY is required')
})()
}
const containerClient = blobServiceClient.getContainerClient(containerName)
@@ -537,10 +501,12 @@ export async function completeMultipartUpload(
const containerClient = blobServiceClient.getContainerClient(containerName)
const blockBlobClient = containerClient.getBlockBlobClient(key)
// Sort parts by part number and extract block IDs
const sortedBlockIds = parts
.sort((a, b) => a.partNumber - b.partNumber)
.map((part) => part.blockId)
// Commit the block list to create the final blob
await blockBlobClient.commitBlockList(sortedBlockIds, {
metadata: {
multipartUpload: 'completed',
@@ -591,8 +557,10 @@ export async function abortMultipartUpload(key: string, customConfig?: BlobConfi
const blockBlobClient = containerClient.getBlockBlobClient(key)
try {
// Delete the blob if it exists (this also cleans up any uncommitted blocks)
await blockBlobClient.deleteIfExists()
} catch (error) {
// Ignore errors since we're just cleaning up
logger.warn('Error cleaning up multipart upload:', error)
}
}

View File

@@ -618,6 +618,13 @@ export function getToolOutputs(
}
}
/**
* Generates output paths for a tool-based block.
*
* @param blockConfig - The block configuration containing tools config
* @param subBlocks - SubBlock values for tool selection and condition evaluation
* @returns Array of output paths for the tool, or empty array on error
*/
export function getToolOutputPaths(
blockConfig: BlockConfig,
subBlocks?: Record<string, SubBlockWithValue>
@@ -627,22 +634,12 @@ export function getToolOutputPaths(
if (!outputs || Object.keys(outputs).length === 0) return []
if (subBlocks && blockConfig.outputs) {
const filteredBlockOutputs = filterOutputsByCondition(blockConfig.outputs, subBlocks)
const allowedKeys = new Set(Object.keys(filteredBlockOutputs))
const filteredOutputs: Record<string, any> = {}
for (const [key, value] of Object.entries(outputs)) {
const blockOutput = blockConfig.outputs[key]
if (!blockOutput || typeof blockOutput !== 'object') {
filteredOutputs[key] = value
continue
}
const condition = 'condition' in blockOutput ? blockOutput.condition : undefined
if (condition) {
if (evaluateOutputCondition(condition, subBlocks)) {
filteredOutputs[key] = value
}
} else {
if (allowedKeys.has(key)) {
filteredOutputs[key] = value
}
}

View File

@@ -26,7 +26,7 @@ describe('VariableManager', () => {
it.concurrent('should handle boolean type variables', () => {
expect(VariableManager.parseInputForStorage('true', 'boolean')).toBe(true)
expect(VariableManager.parseInputForStorage('false', 'boolean')).toBe(false)
expect(VariableManager.parseInputForStorage('1', 'boolean')).toBe(false)
expect(VariableManager.parseInputForStorage('1', 'boolean')).toBe(true)
expect(VariableManager.parseInputForStorage('0', 'boolean')).toBe(false)
expect(VariableManager.parseInputForStorage('"true"', 'boolean')).toBe(true)
expect(VariableManager.parseInputForStorage("'false'", 'boolean')).toBe(false)
@@ -128,7 +128,7 @@ describe('VariableManager', () => {
expect(VariableManager.resolveForExecution(false, 'boolean')).toBe(false)
expect(VariableManager.resolveForExecution('true', 'boolean')).toBe(true)
expect(VariableManager.resolveForExecution('false', 'boolean')).toBe(false)
expect(VariableManager.resolveForExecution('1', 'boolean')).toBe(false)
expect(VariableManager.resolveForExecution('1', 'boolean')).toBe(true)
expect(VariableManager.resolveForExecution('0', 'boolean')).toBe(false)
})

View File

@@ -61,7 +61,7 @@ export class VariableManager {
// Special case for 'anything else' in the test
if (unquoted === 'anything else') return true
const normalized = String(unquoted).toLowerCase().trim()
return normalized === 'true'
return normalized === 'true' || normalized === '1'
}
case 'object':

View File

@@ -52,7 +52,7 @@ services:
deploy:
resources:
limits:
memory: 1G
memory: 8G
healthcheck:
test: ['CMD', 'wget', '--spider', '--quiet', 'http://127.0.0.1:3002/health']
interval: 90s

View File

@@ -56,7 +56,7 @@ services:
deploy:
resources:
limits:
memory: 1G
memory: 8G
healthcheck:
test: ['CMD', 'wget', '--spider', '--quiet', 'http://127.0.0.1:3002/health']
interval: 90s

View File

@@ -42,7 +42,7 @@ services:
deploy:
resources:
limits:
memory: 1G
memory: 4G
environment:
- DATABASE_URL=postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:5432/${POSTGRES_DB:-simstudio}
- NEXT_PUBLIC_APP_URL=${NEXT_PUBLIC_APP_URL:-http://localhost:3000}

View File

@@ -13,10 +13,10 @@ app:
resources:
limits:
memory: "8Gi"
memory: "6Gi"
cpu: "2000m"
requests:
memory: "6Gi"
memory: "4Gi"
cpu: "1000m"
# Production URLs (REQUIRED - update with your actual domain names)
@@ -52,11 +52,11 @@ realtime:
resources:
limits:
memory: "1Gi"
cpu: "500m"
memory: "4Gi"
cpu: "1000m"
requests:
memory: "512Mi"
cpu: "250m"
memory: "2Gi"
cpu: "500m"
env:
NEXT_PUBLIC_APP_URL: "https://sim.acme.ai"

View File

@@ -29,10 +29,10 @@ app:
# Resource limits and requests
resources:
limits:
memory: "8Gi"
memory: "4Gi"
cpu: "2000m"
requests:
memory: "4Gi"
memory: "2Gi"
cpu: "1000m"
# Node selector for pod scheduling (leave empty to allow scheduling on any node)
@@ -245,11 +245,11 @@ realtime:
# Resource limits and requests
resources:
limits:
memory: "2Gi"
cpu: "1000m"
requests:
memory: "1Gi"
cpu: "500m"
requests:
memory: "512Mi"
cpu: "250m"
# Node selector for pod scheduling (leave empty to allow scheduling on any node)
nodeSelector: {}