mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-29 08:48:02 -05:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e9394706f | ||
|
|
c426fd4d4c |
@@ -26,41 +26,78 @@ In Sim, the YouTube integration enables your agents to programmatically search a
|
|||||||
|
|
||||||
## Usage Instructions
|
## Usage Instructions
|
||||||
|
|
||||||
Integrate YouTube into the workflow. Can search for videos, get trending videos, get video details, get video captions, get video categories, get channel information, get all videos from a channel, get channel playlists, get playlist items, and get video comments.
|
Integrate YouTube into the workflow. Can search for videos, get video details, get channel information, get all videos from a channel, get channel playlists, get playlist items, find related videos, and get video comments.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Tools
|
## Tools
|
||||||
|
|
||||||
### `youtube_captions`
|
### `youtube_search`
|
||||||
|
|
||||||
List available caption tracks (subtitles/transcripts) for a YouTube video. Returns information about each caption including language, type, and whether it is auto-generated.
|
Search for videos on YouTube using the YouTube Data API. Supports advanced filtering by channel, date range, duration, category, quality, captions, and more.
|
||||||
|
|
||||||
#### Input
|
#### Input
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
| --------- | ---- | -------- | ----------- |
|
| --------- | ---- | -------- | ----------- |
|
||||||
| `videoId` | string | Yes | YouTube video ID to get captions for |
|
| `query` | string | Yes | Search query for YouTube videos |
|
||||||
|
| `maxResults` | number | No | Maximum number of videos to return \(1-50\) |
|
||||||
|
| `apiKey` | string | Yes | YouTube API Key |
|
||||||
|
| `channelId` | string | No | Filter results to a specific YouTube channel ID |
|
||||||
|
| `publishedAfter` | string | No | Only return videos published after this date \(RFC 3339 format: "2024-01-01T00:00:00Z"\) |
|
||||||
|
| `publishedBefore` | string | No | Only return videos published before this date \(RFC 3339 format: "2024-01-01T00:00:00Z"\) |
|
||||||
|
| `videoDuration` | string | No | Filter by video length: "short" \(<4 min\), "medium" \(4-20 min\), "long" \(>20 min\), "any" |
|
||||||
|
| `order` | string | No | Sort results by: "date", "rating", "relevance" \(default\), "title", "videoCount", "viewCount" |
|
||||||
|
| `videoCategoryId` | string | No | Filter by YouTube category ID \(e.g., "10" for Music, "20" for Gaming\) |
|
||||||
|
| `videoDefinition` | string | No | Filter by video quality: "high" \(HD\), "standard", "any" |
|
||||||
|
| `videoCaption` | string | No | Filter by caption availability: "closedCaption" \(has captions\), "none" \(no captions\), "any" |
|
||||||
|
| `regionCode` | string | No | Return results relevant to a specific region \(ISO 3166-1 alpha-2 country code, e.g., "US", "GB"\) |
|
||||||
|
| `relevanceLanguage` | string | No | Return results most relevant to a language \(ISO 639-1 code, e.g., "en", "es"\) |
|
||||||
|
| `safeSearch` | string | No | Content filtering level: "moderate" \(default\), "none", "strict" |
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ---- | ----------- |
|
||||||
|
| `items` | array | Array of YouTube videos matching the search query |
|
||||||
|
| ↳ `videoId` | string | YouTube video ID |
|
||||||
|
| ↳ `title` | string | Video title |
|
||||||
|
| ↳ `description` | string | Video description |
|
||||||
|
| ↳ `thumbnail` | string | Video thumbnail URL |
|
||||||
|
| `totalResults` | number | Total number of search results available |
|
||||||
|
| `nextPageToken` | string | Token for accessing the next page of results |
|
||||||
|
|
||||||
|
### `youtube_video_details`
|
||||||
|
|
||||||
|
Get detailed information about a specific YouTube video.
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `videoId` | string | Yes | YouTube video ID |
|
||||||
| `apiKey` | string | Yes | YouTube API Key |
|
| `apiKey` | string | Yes | YouTube API Key |
|
||||||
|
|
||||||
#### Output
|
#### Output
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
| Parameter | Type | Description |
|
||||||
| --------- | ---- | ----------- |
|
| --------- | ---- | ----------- |
|
||||||
| `items` | array | Array of available caption tracks for the video |
|
| `videoId` | string | YouTube video ID |
|
||||||
| ↳ `captionId` | string | Caption track ID |
|
| `title` | string | Video title |
|
||||||
| ↳ `language` | string | Language code of the caption \(e.g., |
|
| `description` | string | Video description |
|
||||||
| ↳ `name` | string | Name/label of the caption track |
|
| `channelId` | string | Channel ID |
|
||||||
| ↳ `trackKind` | string | Type of caption track: |
|
| `channelTitle` | string | Channel name |
|
||||||
| ↳ `lastUpdated` | string | When the caption was last updated |
|
| `publishedAt` | string | Published date and time |
|
||||||
| ↳ `isCC` | boolean | Whether this is a closed caption track |
|
| `duration` | string | Video duration in ISO 8601 format |
|
||||||
| ↳ `isAutoSynced` | boolean | Whether the caption timing was automatically synced |
|
| `viewCount` | number | Number of views |
|
||||||
| ↳ `audioTrackType` | string | Type of audio track this caption is for |
|
| `likeCount` | number | Number of likes |
|
||||||
| `totalResults` | number | Total number of caption tracks available |
|
| `commentCount` | number | Number of comments |
|
||||||
|
| `thumbnail` | string | Video thumbnail URL |
|
||||||
|
| `tags` | array | Video tags |
|
||||||
|
|
||||||
### `youtube_channel_info`
|
### `youtube_channel_info`
|
||||||
|
|
||||||
Get detailed information about a YouTube channel including statistics, branding, and content details.
|
Get detailed information about a YouTube channel.
|
||||||
|
|
||||||
#### Input
|
#### Input
|
||||||
|
|
||||||
@@ -77,20 +114,43 @@ Get detailed information about a YouTube channel including statistics, branding,
|
|||||||
| `channelId` | string | YouTube channel ID |
|
| `channelId` | string | YouTube channel ID |
|
||||||
| `title` | string | Channel name |
|
| `title` | string | Channel name |
|
||||||
| `description` | string | Channel description |
|
| `description` | string | Channel description |
|
||||||
| `subscriberCount` | number | Number of subscribers \(0 if hidden\) |
|
| `subscriberCount` | number | Number of subscribers |
|
||||||
| `videoCount` | number | Number of public videos |
|
| `videoCount` | number | Number of videos |
|
||||||
| `viewCount` | number | Total channel views |
|
| `viewCount` | number | Total channel views |
|
||||||
| `publishedAt` | string | Channel creation date |
|
| `publishedAt` | string | Channel creation date |
|
||||||
| `thumbnail` | string | Channel thumbnail/avatar URL |
|
| `thumbnail` | string | Channel thumbnail URL |
|
||||||
| `customUrl` | string | Channel custom URL \(handle\) |
|
| `customUrl` | string | Channel custom URL |
|
||||||
| `country` | string | Country the channel is associated with |
|
|
||||||
| `uploadsPlaylistId` | string | Playlist ID containing all channel uploads \(use with playlist_items\) |
|
### `youtube_channel_videos`
|
||||||
| `bannerImageUrl` | string | Channel banner image URL |
|
|
||||||
| `hiddenSubscriberCount` | boolean | Whether the subscriber count is hidden |
|
Get all videos from a specific YouTube channel, with sorting options.
|
||||||
|
|
||||||
|
#### Input
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
| --------- | ---- | -------- | ----------- |
|
||||||
|
| `channelId` | string | Yes | YouTube channel ID to get videos from |
|
||||||
|
| `maxResults` | number | No | Maximum number of videos to return \(1-50\) |
|
||||||
|
| `order` | string | No | Sort order: "date" \(newest first\), "rating", "relevance", "title", "viewCount" |
|
||||||
|
| `pageToken` | string | No | Page token for pagination |
|
||||||
|
| `apiKey` | string | Yes | YouTube API Key |
|
||||||
|
|
||||||
|
#### Output
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ---- | ----------- |
|
||||||
|
| `items` | array | Array of videos from the channel |
|
||||||
|
| ↳ `videoId` | string | YouTube video ID |
|
||||||
|
| ↳ `title` | string | Video title |
|
||||||
|
| ↳ `description` | string | Video description |
|
||||||
|
| ↳ `thumbnail` | string | Video thumbnail URL |
|
||||||
|
| ↳ `publishedAt` | string | Video publish date |
|
||||||
|
| `totalResults` | number | Total number of videos in the channel |
|
||||||
|
| `nextPageToken` | string | Token for accessing the next page of results |
|
||||||
|
|
||||||
### `youtube_channel_playlists`
|
### `youtube_channel_playlists`
|
||||||
|
|
||||||
Get all public playlists from a specific YouTube channel.
|
Get all playlists from a specific YouTube channel.
|
||||||
|
|
||||||
#### Input
|
#### Input
|
||||||
|
|
||||||
@@ -112,80 +172,19 @@ Get all public playlists from a specific YouTube channel.
|
|||||||
| ↳ `thumbnail` | string | Playlist thumbnail URL |
|
| ↳ `thumbnail` | string | Playlist thumbnail URL |
|
||||||
| ↳ `itemCount` | number | Number of videos in playlist |
|
| ↳ `itemCount` | number | Number of videos in playlist |
|
||||||
| ↳ `publishedAt` | string | Playlist creation date |
|
| ↳ `publishedAt` | string | Playlist creation date |
|
||||||
| ↳ `channelTitle` | string | Channel name |
|
|
||||||
| `totalResults` | number | Total number of playlists in the channel |
|
| `totalResults` | number | Total number of playlists in the channel |
|
||||||
| `nextPageToken` | string | Token for accessing the next page of results |
|
| `nextPageToken` | string | Token for accessing the next page of results |
|
||||||
|
|
||||||
### `youtube_channel_videos`
|
|
||||||
|
|
||||||
Search for videos from a specific YouTube channel with sorting options. For complete channel video list, use channel_info to get uploadsPlaylistId, then use playlist_items.
|
|
||||||
|
|
||||||
#### Input
|
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
|
||||||
| --------- | ---- | -------- | ----------- |
|
|
||||||
| `channelId` | string | Yes | YouTube channel ID to get videos from |
|
|
||||||
| `maxResults` | number | No | Maximum number of videos to return \(1-50\) |
|
|
||||||
| `order` | string | No | Sort order: "date" \(newest first, default\), "rating", "relevance", "title", "viewCount" |
|
|
||||||
| `pageToken` | string | No | Page token for pagination |
|
|
||||||
| `apiKey` | string | Yes | YouTube API Key |
|
|
||||||
|
|
||||||
#### Output
|
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
| --------- | ---- | ----------- |
|
|
||||||
| `items` | array | Array of videos from the channel |
|
|
||||||
| ↳ `videoId` | string | YouTube video ID |
|
|
||||||
| ↳ `title` | string | Video title |
|
|
||||||
| ↳ `description` | string | Video description |
|
|
||||||
| ↳ `thumbnail` | string | Video thumbnail URL |
|
|
||||||
| ↳ `publishedAt` | string | Video publish date |
|
|
||||||
| ↳ `channelTitle` | string | Channel name |
|
|
||||||
| `totalResults` | number | Total number of videos in the channel |
|
|
||||||
| `nextPageToken` | string | Token for accessing the next page of results |
|
|
||||||
|
|
||||||
### `youtube_comments`
|
|
||||||
|
|
||||||
Get top-level comments from a YouTube video with author details and engagement.
|
|
||||||
|
|
||||||
#### Input
|
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
|
||||||
| --------- | ---- | -------- | ----------- |
|
|
||||||
| `videoId` | string | Yes | YouTube video ID |
|
|
||||||
| `maxResults` | number | No | Maximum number of comments to return \(1-100\) |
|
|
||||||
| `order` | string | No | Order of comments: "time" \(newest first\) or "relevance" \(most relevant first\) |
|
|
||||||
| `pageToken` | string | No | Page token for pagination |
|
|
||||||
| `apiKey` | string | Yes | YouTube API Key |
|
|
||||||
|
|
||||||
#### Output
|
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
| --------- | ---- | ----------- |
|
|
||||||
| `items` | array | Array of top-level comments from the video |
|
|
||||||
| ↳ `commentId` | string | Comment ID |
|
|
||||||
| ↳ `authorDisplayName` | string | Comment author display name |
|
|
||||||
| ↳ `authorChannelUrl` | string | Comment author channel URL |
|
|
||||||
| ↳ `authorProfileImageUrl` | string | Comment author profile image URL |
|
|
||||||
| ↳ `textDisplay` | string | Comment text \(HTML formatted\) |
|
|
||||||
| ↳ `textOriginal` | string | Comment text \(plain text\) |
|
|
||||||
| ↳ `likeCount` | number | Number of likes on the comment |
|
|
||||||
| ↳ `publishedAt` | string | When the comment was posted |
|
|
||||||
| ↳ `updatedAt` | string | When the comment was last edited |
|
|
||||||
| ↳ `replyCount` | number | Number of replies to this comment |
|
|
||||||
| `totalResults` | number | Total number of comment threads available |
|
|
||||||
| `nextPageToken` | string | Token for accessing the next page of results |
|
|
||||||
|
|
||||||
### `youtube_playlist_items`
|
### `youtube_playlist_items`
|
||||||
|
|
||||||
Get videos from a YouTube playlist. Can be used with a channel uploads playlist to get all channel videos.
|
Get videos from a YouTube playlist.
|
||||||
|
|
||||||
#### Input
|
#### Input
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
| --------- | ---- | -------- | ----------- |
|
| --------- | ---- | -------- | ----------- |
|
||||||
| `playlistId` | string | Yes | YouTube playlist ID. Use uploadsPlaylistId from channel_info to get all channel videos. |
|
| `playlistId` | string | Yes | YouTube playlist ID |
|
||||||
| `maxResults` | number | No | Maximum number of videos to return \(1-50\) |
|
| `maxResults` | number | No | Maximum number of videos to return |
|
||||||
| `pageToken` | string | No | Page token for pagination |
|
| `pageToken` | string | No | Page token for pagination |
|
||||||
| `apiKey` | string | Yes | YouTube API Key |
|
| `apiKey` | string | Yes | YouTube API Key |
|
||||||
|
|
||||||
@@ -199,65 +198,22 @@ Get videos from a YouTube playlist. Can be used with a channel uploads playlist
|
|||||||
| ↳ `description` | string | Video description |
|
| ↳ `description` | string | Video description |
|
||||||
| ↳ `thumbnail` | string | Video thumbnail URL |
|
| ↳ `thumbnail` | string | Video thumbnail URL |
|
||||||
| ↳ `publishedAt` | string | Date added to playlist |
|
| ↳ `publishedAt` | string | Date added to playlist |
|
||||||
| ↳ `channelTitle` | string | Playlist owner channel name |
|
| ↳ `channelTitle` | string | Channel name |
|
||||||
| ↳ `position` | number | Position in playlist \(0-indexed\) |
|
| ↳ `position` | number | Position in playlist |
|
||||||
| ↳ `videoOwnerChannelId` | string | Channel ID of the video owner |
|
|
||||||
| ↳ `videoOwnerChannelTitle` | string | Channel name of the video owner |
|
|
||||||
| `totalResults` | number | Total number of items in playlist |
|
| `totalResults` | number | Total number of items in playlist |
|
||||||
| `nextPageToken` | string | Token for accessing the next page of results |
|
| `nextPageToken` | string | Token for accessing the next page of results |
|
||||||
|
|
||||||
### `youtube_search`
|
### `youtube_comments`
|
||||||
|
|
||||||
Search for videos on YouTube using the YouTube Data API. Supports advanced filtering by channel, date range, duration, category, quality, captions, live streams, and more.
|
Get comments from a YouTube video.
|
||||||
|
|
||||||
#### Input
|
#### Input
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
| Parameter | Type | Required | Description |
|
||||||
| --------- | ---- | -------- | ----------- |
|
| --------- | ---- | -------- | ----------- |
|
||||||
| `query` | string | Yes | Search query for YouTube videos |
|
| `videoId` | string | Yes | YouTube video ID |
|
||||||
| `maxResults` | number | No | Maximum number of videos to return \(1-50\) |
|
| `maxResults` | number | No | Maximum number of comments to return |
|
||||||
| `pageToken` | string | No | Page token for pagination \(use nextPageToken from previous response\) |
|
| `order` | string | No | Order of comments: time or relevance |
|
||||||
| `apiKey` | string | Yes | YouTube API Key |
|
|
||||||
| `channelId` | string | No | Filter results to a specific YouTube channel ID |
|
|
||||||
| `publishedAfter` | string | No | Only return videos published after this date \(RFC 3339 format: "2024-01-01T00:00:00Z"\) |
|
|
||||||
| `publishedBefore` | string | No | Only return videos published before this date \(RFC 3339 format: "2024-01-01T00:00:00Z"\) |
|
|
||||||
| `videoDuration` | string | No | Filter by video length: "short" \(<4 min\), "medium" \(4-20 min\), "long" \(>20 min\), "any" |
|
|
||||||
| `order` | string | No | Sort results by: "date", "rating", "relevance" \(default\), "title", "videoCount", "viewCount" |
|
|
||||||
| `videoCategoryId` | string | No | Filter by YouTube category ID \(e.g., "10" for Music, "20" for Gaming\). Use video_categories to list IDs. |
|
|
||||||
| `videoDefinition` | string | No | Filter by video quality: "high" \(HD\), "standard", "any" |
|
|
||||||
| `videoCaption` | string | No | Filter by caption availability: "closedCaption" \(has captions\), "none" \(no captions\), "any" |
|
|
||||||
| `eventType` | string | No | Filter by live broadcast status: "live" \(currently live\), "upcoming" \(scheduled\), "completed" \(past streams\) |
|
|
||||||
| `regionCode` | string | No | Return results relevant to a specific region \(ISO 3166-1 alpha-2 country code, e.g., "US", "GB"\) |
|
|
||||||
| `relevanceLanguage` | string | No | Return results most relevant to a language \(ISO 639-1 code, e.g., "en", "es"\) |
|
|
||||||
| `safeSearch` | string | No | Content filtering level: "moderate" \(default\), "none", "strict" |
|
|
||||||
|
|
||||||
#### Output
|
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
| --------- | ---- | ----------- |
|
|
||||||
| `items` | array | Array of YouTube videos matching the search query |
|
|
||||||
| ↳ `videoId` | string | YouTube video ID |
|
|
||||||
| ↳ `title` | string | Video title |
|
|
||||||
| ↳ `description` | string | Video description |
|
|
||||||
| ↳ `thumbnail` | string | Video thumbnail URL |
|
|
||||||
| ↳ `channelId` | string | Channel ID that uploaded the video |
|
|
||||||
| ↳ `channelTitle` | string | Channel name |
|
|
||||||
| ↳ `publishedAt` | string | Video publish date |
|
|
||||||
| ↳ `liveBroadcastContent` | string | Live broadcast status: |
|
|
||||||
| `totalResults` | number | Total number of search results available |
|
|
||||||
| `nextPageToken` | string | Token for accessing the next page of results |
|
|
||||||
|
|
||||||
### `youtube_trending`
|
|
||||||
|
|
||||||
Get the most popular/trending videos on YouTube. Can filter by region and video category.
|
|
||||||
|
|
||||||
#### Input
|
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
|
||||||
| --------- | ---- | -------- | ----------- |
|
|
||||||
| `regionCode` | string | No | ISO 3166-1 alpha-2 country code to get trending videos for \(e.g., "US", "GB", "JP"\). Defaults to US. |
|
|
||||||
| `videoCategoryId` | string | No | Filter by video category ID \(e.g., "10" for Music, "20" for Gaming, "17" for Sports\) |
|
|
||||||
| `maxResults` | number | No | Maximum number of trending videos to return \(1-50\) |
|
|
||||||
| `pageToken` | string | No | Page token for pagination |
|
| `pageToken` | string | No | Page token for pagination |
|
||||||
| `apiKey` | string | Yes | YouTube API Key |
|
| `apiKey` | string | Yes | YouTube API Key |
|
||||||
|
|
||||||
@@ -265,84 +221,17 @@ Get the most popular/trending videos on YouTube. Can filter by region and video
|
|||||||
|
|
||||||
| Parameter | Type | Description |
|
| Parameter | Type | Description |
|
||||||
| --------- | ---- | ----------- |
|
| --------- | ---- | ----------- |
|
||||||
| `items` | array | Array of trending videos |
|
| `items` | array | Array of comments from the video |
|
||||||
| ↳ `videoId` | string | YouTube video ID |
|
| ↳ `commentId` | string | Comment ID |
|
||||||
| ↳ `title` | string | Video title |
|
| ↳ `authorDisplayName` | string | Comment author name |
|
||||||
| ↳ `description` | string | Video description |
|
| ↳ `authorChannelUrl` | string | Comment author channel URL |
|
||||||
| ↳ `thumbnail` | string | Video thumbnail URL |
|
| ↳ `textDisplay` | string | Comment text \(HTML formatted\) |
|
||||||
| ↳ `channelId` | string | Channel ID |
|
| ↳ `textOriginal` | string | Comment text \(plain text\) |
|
||||||
| ↳ `channelTitle` | string | Channel name |
|
|
||||||
| ↳ `publishedAt` | string | Video publish date |
|
|
||||||
| ↳ `viewCount` | number | Number of views |
|
|
||||||
| ↳ `likeCount` | number | Number of likes |
|
| ↳ `likeCount` | number | Number of likes |
|
||||||
| ↳ `commentCount` | number | Number of comments |
|
| ↳ `publishedAt` | string | Comment publish date |
|
||||||
| ↳ `duration` | string | Video duration in ISO 8601 format |
|
| ↳ `updatedAt` | string | Comment last updated date |
|
||||||
| `totalResults` | number | Total number of trending videos available |
|
| ↳ `replyCount` | number | Number of replies |
|
||||||
|
| `totalResults` | number | Total number of comments |
|
||||||
| `nextPageToken` | string | Token for accessing the next page of results |
|
| `nextPageToken` | string | Token for accessing the next page of results |
|
||||||
|
|
||||||
### `youtube_video_categories`
|
|
||||||
|
|
||||||
Get a list of video categories available on YouTube. Use this to discover valid category IDs for filtering search and trending results.
|
|
||||||
|
|
||||||
#### Input
|
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
|
||||||
| --------- | ---- | -------- | ----------- |
|
|
||||||
| `regionCode` | string | No | ISO 3166-1 alpha-2 country code to get categories for \(e.g., "US", "GB", "JP"\). Defaults to US. |
|
|
||||||
| `hl` | string | No | Language for category titles \(e.g., "en", "es", "fr"\). Defaults to English. |
|
|
||||||
| `apiKey` | string | Yes | YouTube API Key |
|
|
||||||
|
|
||||||
#### Output
|
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
| --------- | ---- | ----------- |
|
|
||||||
| `items` | array | Array of video categories available in the specified region |
|
|
||||||
| ↳ `categoryId` | string | Category ID to use in search/trending filters \(e.g., |
|
|
||||||
| ↳ `title` | string | Human-readable category name |
|
|
||||||
| ↳ `assignable` | boolean | Whether videos can be tagged with this category |
|
|
||||||
| `totalResults` | number | Total number of categories available |
|
|
||||||
|
|
||||||
### `youtube_video_details`
|
|
||||||
|
|
||||||
Get detailed information about a specific YouTube video including statistics, content details, live streaming info, and metadata.
|
|
||||||
|
|
||||||
#### Input
|
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
|
||||||
| --------- | ---- | -------- | ----------- |
|
|
||||||
| `videoId` | string | Yes | YouTube video ID |
|
|
||||||
| `apiKey` | string | Yes | YouTube API Key |
|
|
||||||
|
|
||||||
#### Output
|
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
| --------- | ---- | ----------- |
|
|
||||||
| `videoId` | string | YouTube video ID |
|
|
||||||
| `title` | string | Video title |
|
|
||||||
| `description` | string | Video description |
|
|
||||||
| `channelId` | string | Channel ID |
|
|
||||||
| `channelTitle` | string | Channel name |
|
|
||||||
| `publishedAt` | string | Published date and time |
|
|
||||||
| `duration` | string | Video duration in ISO 8601 format \(e.g., |
|
|
||||||
| `viewCount` | number | Number of views |
|
|
||||||
| `likeCount` | number | Number of likes |
|
|
||||||
| `commentCount` | number | Number of comments |
|
|
||||||
| `favoriteCount` | number | Number of times added to favorites |
|
|
||||||
| `thumbnail` | string | Video thumbnail URL |
|
|
||||||
| `tags` | array | Video tags |
|
|
||||||
| `categoryId` | string | YouTube video category ID |
|
|
||||||
| `definition` | string | Video definition: |
|
|
||||||
| `caption` | string | Whether captions are available: |
|
|
||||||
| `licensedContent` | boolean | Whether the video is licensed content |
|
|
||||||
| `privacyStatus` | string | Video privacy status: |
|
|
||||||
| `liveBroadcastContent` | string | Live broadcast status: |
|
|
||||||
| `defaultLanguage` | string | Default language of the video metadata |
|
|
||||||
| `defaultAudioLanguage` | string | Default audio language of the video |
|
|
||||||
| `isLiveContent` | boolean | Whether this video is or was a live stream |
|
|
||||||
| `scheduledStartTime` | string | Scheduled start time for upcoming live streams \(ISO 8601\) |
|
|
||||||
| `actualStartTime` | string | When the live stream actually started \(ISO 8601\) |
|
|
||||||
| `actualEndTime` | string | When the live stream ended \(ISO 8601\) |
|
|
||||||
| `concurrentViewers` | number | Current number of viewers \(only for active live streams\) |
|
|
||||||
| `activeLiveChatId` | string | Live chat ID for the stream \(only for active live streams\) |
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export async function GET(_request: NextRequest, { params }: { params: Promise<{
|
|||||||
deploymentVersionName: workflowDeploymentVersion.name,
|
deploymentVersionName: workflowDeploymentVersion.name,
|
||||||
})
|
})
|
||||||
.from(workflowExecutionLogs)
|
.from(workflowExecutionLogs)
|
||||||
.leftJoin(workflow, eq(workflowExecutionLogs.workflowId, workflow.id))
|
.innerJoin(workflow, eq(workflowExecutionLogs.workflowId, workflow.id))
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
workflowDeploymentVersion,
|
workflowDeploymentVersion,
|
||||||
eq(workflowDeploymentVersion.id, workflowExecutionLogs.deploymentVersionId)
|
eq(workflowDeploymentVersion.id, workflowExecutionLogs.deploymentVersionId)
|
||||||
@@ -65,7 +65,7 @@ export async function GET(_request: NextRequest, { params }: { params: Promise<{
|
|||||||
permissions,
|
permissions,
|
||||||
and(
|
and(
|
||||||
eq(permissions.entityType, 'workspace'),
|
eq(permissions.entityType, 'workspace'),
|
||||||
eq(permissions.entityId, workflowExecutionLogs.workspaceId),
|
eq(permissions.entityId, workflow.workspaceId),
|
||||||
eq(permissions.userId, userId)
|
eq(permissions.userId, userId)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -77,19 +77,17 @@ export async function GET(_request: NextRequest, { params }: { params: Promise<{
|
|||||||
return NextResponse.json({ error: 'Not found' }, { status: 404 })
|
return NextResponse.json({ error: 'Not found' }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
const workflowSummary = log.workflowId
|
const workflowSummary = {
|
||||||
? {
|
id: log.workflowId,
|
||||||
id: log.workflowId,
|
name: log.workflowName,
|
||||||
name: log.workflowName,
|
description: log.workflowDescription,
|
||||||
description: log.workflowDescription,
|
color: log.workflowColor,
|
||||||
color: log.workflowColor,
|
folderId: log.workflowFolderId,
|
||||||
folderId: log.workflowFolderId,
|
userId: log.workflowUserId,
|
||||||
userId: log.workflowUserId,
|
workspaceId: log.workflowWorkspaceId,
|
||||||
workspaceId: log.workflowWorkspaceId,
|
createdAt: log.workflowCreatedAt,
|
||||||
createdAt: log.workflowCreatedAt,
|
updatedAt: log.workflowUpdatedAt,
|
||||||
updatedAt: log.workflowUpdatedAt,
|
}
|
||||||
}
|
|
||||||
: null
|
|
||||||
|
|
||||||
const response = {
|
const response = {
|
||||||
id: log.id,
|
id: log.id,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { db } from '@sim/db'
|
import { db } from '@sim/db'
|
||||||
import { subscription, user, workflowExecutionLogs, workspace } from '@sim/db/schema'
|
import { subscription, user, workflow, workflowExecutionLogs } from '@sim/db/schema'
|
||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { and, eq, inArray, lt, sql } from 'drizzle-orm'
|
import { and, eq, inArray, lt, sql } from 'drizzle-orm'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
@@ -40,17 +40,17 @@ export async function GET(request: NextRequest) {
|
|||||||
|
|
||||||
const freeUserIds = freeUsers.map((u) => u.userId)
|
const freeUserIds = freeUsers.map((u) => u.userId)
|
||||||
|
|
||||||
const workspacesQuery = await db
|
const workflowsQuery = await db
|
||||||
.select({ id: workspace.id })
|
.select({ id: workflow.id })
|
||||||
.from(workspace)
|
.from(workflow)
|
||||||
.where(inArray(workspace.billedAccountUserId, freeUserIds))
|
.where(inArray(workflow.userId, freeUserIds))
|
||||||
|
|
||||||
if (workspacesQuery.length === 0) {
|
if (workflowsQuery.length === 0) {
|
||||||
logger.info('No workspaces found for free users')
|
logger.info('No workflows found for free users')
|
||||||
return NextResponse.json({ message: 'No workspaces found for cleanup' })
|
return NextResponse.json({ message: 'No workflows found for cleanup' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const workspaceIds = workspacesQuery.map((w) => w.id)
|
const workflowIds = workflowsQuery.map((w) => w.id)
|
||||||
|
|
||||||
const results = {
|
const results = {
|
||||||
enhancedLogs: {
|
enhancedLogs: {
|
||||||
@@ -77,7 +77,7 @@ export async function GET(request: NextRequest) {
|
|||||||
let batchesProcessed = 0
|
let batchesProcessed = 0
|
||||||
let hasMoreLogs = true
|
let hasMoreLogs = true
|
||||||
|
|
||||||
logger.info(`Starting enhanced logs cleanup for ${workspaceIds.length} workspaces`)
|
logger.info(`Starting enhanced logs cleanup for ${workflowIds.length} workflows`)
|
||||||
|
|
||||||
while (hasMoreLogs && batchesProcessed < MAX_BATCHES) {
|
while (hasMoreLogs && batchesProcessed < MAX_BATCHES) {
|
||||||
const oldEnhancedLogs = await db
|
const oldEnhancedLogs = await db
|
||||||
@@ -99,7 +99,7 @@ export async function GET(request: NextRequest) {
|
|||||||
.from(workflowExecutionLogs)
|
.from(workflowExecutionLogs)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
inArray(workflowExecutionLogs.workspaceId, workspaceIds),
|
inArray(workflowExecutionLogs.workflowId, workflowIds),
|
||||||
lt(workflowExecutionLogs.createdAt, retentionDate)
|
lt(workflowExecutionLogs.createdAt, retentionDate)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -127,7 +127,7 @@ export async function GET(request: NextRequest) {
|
|||||||
customKey: enhancedLogKey,
|
customKey: enhancedLogKey,
|
||||||
metadata: {
|
metadata: {
|
||||||
logId: String(log.id),
|
logId: String(log.id),
|
||||||
workflowId: String(log.workflowId ?? ''),
|
workflowId: String(log.workflowId),
|
||||||
executionId: String(log.executionId),
|
executionId: String(log.executionId),
|
||||||
logType: 'enhanced',
|
logType: 'enhanced',
|
||||||
archivedAt: new Date().toISOString(),
|
archivedAt: new Date().toISOString(),
|
||||||
|
|||||||
@@ -6,11 +6,10 @@ import {
|
|||||||
workflowExecutionSnapshots,
|
workflowExecutionSnapshots,
|
||||||
} from '@sim/db/schema'
|
} from '@sim/db/schema'
|
||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { and, eq, inArray } from 'drizzle-orm'
|
import { and, eq } from 'drizzle-orm'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import type { TraceSpan, WorkflowExecutionLog } from '@/lib/logs/types'
|
|
||||||
|
|
||||||
const logger = createLogger('LogsByExecutionIdAPI')
|
const logger = createLogger('LogsByExecutionIdAPI')
|
||||||
|
|
||||||
@@ -49,15 +48,14 @@ export async function GET(
|
|||||||
endedAt: workflowExecutionLogs.endedAt,
|
endedAt: workflowExecutionLogs.endedAt,
|
||||||
totalDurationMs: workflowExecutionLogs.totalDurationMs,
|
totalDurationMs: workflowExecutionLogs.totalDurationMs,
|
||||||
cost: workflowExecutionLogs.cost,
|
cost: workflowExecutionLogs.cost,
|
||||||
executionData: workflowExecutionLogs.executionData,
|
|
||||||
})
|
})
|
||||||
.from(workflowExecutionLogs)
|
.from(workflowExecutionLogs)
|
||||||
.leftJoin(workflow, eq(workflowExecutionLogs.workflowId, workflow.id))
|
.innerJoin(workflow, eq(workflowExecutionLogs.workflowId, workflow.id))
|
||||||
.innerJoin(
|
.innerJoin(
|
||||||
permissions,
|
permissions,
|
||||||
and(
|
and(
|
||||||
eq(permissions.entityType, 'workspace'),
|
eq(permissions.entityType, 'workspace'),
|
||||||
eq(permissions.entityId, workflowExecutionLogs.workspaceId),
|
eq(permissions.entityId, workflow.workspaceId),
|
||||||
eq(permissions.userId, authenticatedUserId)
|
eq(permissions.userId, authenticatedUserId)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -80,42 +78,10 @@ export async function GET(
|
|||||||
return NextResponse.json({ error: 'Workflow state snapshot not found' }, { status: 404 })
|
return NextResponse.json({ error: 'Workflow state snapshot not found' }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
const executionData = workflowLog.executionData as WorkflowExecutionLog['executionData']
|
|
||||||
const traceSpans = (executionData?.traceSpans as TraceSpan[]) || []
|
|
||||||
const childSnapshotIds = new Set<string>()
|
|
||||||
const collectSnapshotIds = (spans: TraceSpan[]) => {
|
|
||||||
spans.forEach((span) => {
|
|
||||||
const snapshotId = span.childWorkflowSnapshotId
|
|
||||||
if (typeof snapshotId === 'string') {
|
|
||||||
childSnapshotIds.add(snapshotId)
|
|
||||||
}
|
|
||||||
if (span.children?.length) {
|
|
||||||
collectSnapshotIds(span.children)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (traceSpans.length > 0) {
|
|
||||||
collectSnapshotIds(traceSpans)
|
|
||||||
}
|
|
||||||
|
|
||||||
const childWorkflowSnapshots =
|
|
||||||
childSnapshotIds.size > 0
|
|
||||||
? await db
|
|
||||||
.select()
|
|
||||||
.from(workflowExecutionSnapshots)
|
|
||||||
.where(inArray(workflowExecutionSnapshots.id, Array.from(childSnapshotIds)))
|
|
||||||
: []
|
|
||||||
|
|
||||||
const childSnapshotMap = childWorkflowSnapshots.reduce<Record<string, unknown>>((acc, snap) => {
|
|
||||||
acc[snap.id] = snap.stateData
|
|
||||||
return acc
|
|
||||||
}, {})
|
|
||||||
|
|
||||||
const response = {
|
const response = {
|
||||||
executionId,
|
executionId,
|
||||||
workflowId: workflowLog.workflowId,
|
workflowId: workflowLog.workflowId,
|
||||||
workflowState: snapshot.stateData,
|
workflowState: snapshot.stateData,
|
||||||
childWorkflowSnapshots: childSnapshotMap,
|
|
||||||
executionMetadata: {
|
executionMetadata: {
|
||||||
trigger: workflowLog.trigger,
|
trigger: workflowLog.trigger,
|
||||||
startedAt: workflowLog.startedAt.toISOString(),
|
startedAt: workflowLog.startedAt.toISOString(),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { db } from '@sim/db'
|
import { db } from '@sim/db'
|
||||||
import { permissions, workflow, workflowExecutionLogs } from '@sim/db/schema'
|
import { permissions, workflow, workflowExecutionLogs } from '@sim/db/schema'
|
||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { and, desc, eq, sql } from 'drizzle-orm'
|
import { and, desc, eq } from 'drizzle-orm'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { buildFilterConditions, LogFilterParamsSchema } from '@/lib/logs/filters'
|
import { buildFilterConditions, LogFilterParamsSchema } from '@/lib/logs/filters'
|
||||||
@@ -41,7 +41,7 @@ export async function GET(request: NextRequest) {
|
|||||||
totalDurationMs: workflowExecutionLogs.totalDurationMs,
|
totalDurationMs: workflowExecutionLogs.totalDurationMs,
|
||||||
cost: workflowExecutionLogs.cost,
|
cost: workflowExecutionLogs.cost,
|
||||||
executionData: workflowExecutionLogs.executionData,
|
executionData: workflowExecutionLogs.executionData,
|
||||||
workflowName: sql<string>`COALESCE(${workflow.name}, 'Deleted Workflow')`,
|
workflowName: workflow.name,
|
||||||
}
|
}
|
||||||
|
|
||||||
const workspaceCondition = eq(workflowExecutionLogs.workspaceId, params.workspaceId)
|
const workspaceCondition = eq(workflowExecutionLogs.workspaceId, params.workspaceId)
|
||||||
@@ -74,7 +74,7 @@ export async function GET(request: NextRequest) {
|
|||||||
const rows = await db
|
const rows = await db
|
||||||
.select(selectColumns)
|
.select(selectColumns)
|
||||||
.from(workflowExecutionLogs)
|
.from(workflowExecutionLogs)
|
||||||
.leftJoin(workflow, eq(workflowExecutionLogs.workflowId, workflow.id))
|
.innerJoin(workflow, eq(workflowExecutionLogs.workflowId, workflow.id))
|
||||||
.innerJoin(
|
.innerJoin(
|
||||||
permissions,
|
permissions,
|
||||||
and(
|
and(
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ export async function GET(request: NextRequest) {
|
|||||||
workflowDeploymentVersion,
|
workflowDeploymentVersion,
|
||||||
eq(workflowDeploymentVersion.id, workflowExecutionLogs.deploymentVersionId)
|
eq(workflowDeploymentVersion.id, workflowExecutionLogs.deploymentVersionId)
|
||||||
)
|
)
|
||||||
.leftJoin(workflow, eq(workflowExecutionLogs.workflowId, workflow.id))
|
.innerJoin(workflow, eq(workflowExecutionLogs.workflowId, workflow.id))
|
||||||
.innerJoin(
|
.innerJoin(
|
||||||
permissions,
|
permissions,
|
||||||
and(
|
and(
|
||||||
@@ -190,7 +190,7 @@ export async function GET(request: NextRequest) {
|
|||||||
pausedExecutions,
|
pausedExecutions,
|
||||||
eq(pausedExecutions.executionId, workflowExecutionLogs.executionId)
|
eq(pausedExecutions.executionId, workflowExecutionLogs.executionId)
|
||||||
)
|
)
|
||||||
.leftJoin(workflow, eq(workflowExecutionLogs.workflowId, workflow.id))
|
.innerJoin(workflow, eq(workflowExecutionLogs.workflowId, workflow.id))
|
||||||
.innerJoin(
|
.innerJoin(
|
||||||
permissions,
|
permissions,
|
||||||
and(
|
and(
|
||||||
@@ -314,19 +314,17 @@ export async function GET(request: NextRequest) {
|
|||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const workflowSummary = log.workflowId
|
const workflowSummary = {
|
||||||
? {
|
id: log.workflowId,
|
||||||
id: log.workflowId,
|
name: log.workflowName,
|
||||||
name: log.workflowName,
|
description: log.workflowDescription,
|
||||||
description: log.workflowDescription,
|
color: log.workflowColor,
|
||||||
color: log.workflowColor,
|
folderId: log.workflowFolderId,
|
||||||
folderId: log.workflowFolderId,
|
userId: log.workflowUserId,
|
||||||
userId: log.workflowUserId,
|
workspaceId: log.workflowWorkspaceId,
|
||||||
workspaceId: log.workflowWorkspaceId,
|
createdAt: log.workflowCreatedAt,
|
||||||
createdAt: log.workflowCreatedAt,
|
updatedAt: log.workflowUpdatedAt,
|
||||||
updatedAt: log.workflowUpdatedAt,
|
}
|
||||||
}
|
|
||||||
: null
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: log.id,
|
id: log.id,
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export async function GET(request: NextRequest) {
|
|||||||
maxTime: sql<string>`MAX(${workflowExecutionLogs.startedAt})`,
|
maxTime: sql<string>`MAX(${workflowExecutionLogs.startedAt})`,
|
||||||
})
|
})
|
||||||
.from(workflowExecutionLogs)
|
.from(workflowExecutionLogs)
|
||||||
.leftJoin(workflow, eq(workflowExecutionLogs.workflowId, workflow.id))
|
.innerJoin(workflow, eq(workflowExecutionLogs.workflowId, workflow.id))
|
||||||
.innerJoin(
|
.innerJoin(
|
||||||
permissions,
|
permissions,
|
||||||
and(
|
and(
|
||||||
@@ -103,8 +103,8 @@ export async function GET(request: NextRequest) {
|
|||||||
|
|
||||||
const statsQuery = await db
|
const statsQuery = await db
|
||||||
.select({
|
.select({
|
||||||
workflowId: sql<string>`COALESCE(${workflowExecutionLogs.workflowId}, 'deleted')`,
|
workflowId: workflowExecutionLogs.workflowId,
|
||||||
workflowName: sql<string>`COALESCE(${workflow.name}, 'Deleted Workflow')`,
|
workflowName: workflow.name,
|
||||||
segmentIndex:
|
segmentIndex:
|
||||||
sql<number>`FLOOR(EXTRACT(EPOCH FROM (${workflowExecutionLogs.startedAt} - ${startTimeIso}::timestamp)) * 1000 / ${segmentMs})`.as(
|
sql<number>`FLOOR(EXTRACT(EPOCH FROM (${workflowExecutionLogs.startedAt} - ${startTimeIso}::timestamp)) * 1000 / ${segmentMs})`.as(
|
||||||
'segment_index'
|
'segment_index'
|
||||||
@@ -120,7 +120,7 @@ export async function GET(request: NextRequest) {
|
|||||||
),
|
),
|
||||||
})
|
})
|
||||||
.from(workflowExecutionLogs)
|
.from(workflowExecutionLogs)
|
||||||
.leftJoin(workflow, eq(workflowExecutionLogs.workflowId, workflow.id))
|
.innerJoin(workflow, eq(workflowExecutionLogs.workflowId, workflow.id))
|
||||||
.innerJoin(
|
.innerJoin(
|
||||||
permissions,
|
permissions,
|
||||||
and(
|
and(
|
||||||
@@ -130,11 +130,7 @@ export async function GET(request: NextRequest) {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
.where(whereCondition)
|
.where(whereCondition)
|
||||||
.groupBy(
|
.groupBy(workflowExecutionLogs.workflowId, workflow.name, sql`segment_index`)
|
||||||
sql`COALESCE(${workflowExecutionLogs.workflowId}, 'deleted')`,
|
|
||||||
sql`COALESCE(${workflow.name}, 'Deleted Workflow')`,
|
|
||||||
sql`segment_index`
|
|
||||||
)
|
|
||||||
|
|
||||||
const workflowMap = new Map<
|
const workflowMap = new Map<
|
||||||
string,
|
string,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { hasAccessControlAccess } from '@/lib/billing'
|
|||||||
import {
|
import {
|
||||||
type PermissionGroupConfig,
|
type PermissionGroupConfig,
|
||||||
parsePermissionGroupConfig,
|
parsePermissionGroupConfig,
|
||||||
} from '@/lib/permission-groups/types'
|
} from '@/ee/access-control/lib/types'
|
||||||
|
|
||||||
const logger = createLogger('PermissionGroup')
|
const logger = createLogger('PermissionGroup')
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
DEFAULT_PERMISSION_GROUP_CONFIG,
|
DEFAULT_PERMISSION_GROUP_CONFIG,
|
||||||
type PermissionGroupConfig,
|
type PermissionGroupConfig,
|
||||||
parsePermissionGroupConfig,
|
parsePermissionGroupConfig,
|
||||||
} from '@/lib/permission-groups/types'
|
} from '@/ee/access-control/lib/types'
|
||||||
|
|
||||||
const logger = createLogger('PermissionGroups')
|
const logger = createLogger('PermissionGroups')
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { and, eq } from 'drizzle-orm'
|
|||||||
import { NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { getSession } from '@/lib/auth'
|
import { getSession } from '@/lib/auth'
|
||||||
import { isOrganizationOnEnterprisePlan } from '@/lib/billing'
|
import { isOrganizationOnEnterprisePlan } from '@/lib/billing'
|
||||||
import { parsePermissionGroupConfig } from '@/lib/permission-groups/types'
|
import { parsePermissionGroupConfig } from '@/ee/access-control/lib/types'
|
||||||
|
|
||||||
export async function GET(req: Request) {
|
export async function GET(req: Request) {
|
||||||
const session = await getSession()
|
const session = await getSession()
|
||||||
|
|||||||
@@ -133,7 +133,9 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
|||||||
const finalWorkflowData = {
|
const finalWorkflowData = {
|
||||||
...workflowData,
|
...workflowData,
|
||||||
state: {
|
state: {
|
||||||
|
// Default values for expected properties
|
||||||
deploymentStatuses: {},
|
deploymentStatuses: {},
|
||||||
|
// Data from normalized tables
|
||||||
blocks: normalizedData.blocks,
|
blocks: normalizedData.blocks,
|
||||||
edges: normalizedData.edges,
|
edges: normalizedData.edges,
|
||||||
loops: normalizedData.loops,
|
loops: normalizedData.loops,
|
||||||
@@ -141,11 +143,8 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
|||||||
lastSaved: Date.now(),
|
lastSaved: Date.now(),
|
||||||
isDeployed: workflowData.isDeployed || false,
|
isDeployed: workflowData.isDeployed || false,
|
||||||
deployedAt: workflowData.deployedAt,
|
deployedAt: workflowData.deployedAt,
|
||||||
metadata: {
|
|
||||||
name: workflowData.name,
|
|
||||||
description: workflowData.description,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
// Include workflow variables
|
||||||
variables: workflowData.variables || {},
|
variables: workflowData.variables || {},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,10 +166,6 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
|||||||
lastSaved: Date.now(),
|
lastSaved: Date.now(),
|
||||||
isDeployed: workflowData.isDeployed || false,
|
isDeployed: workflowData.isDeployed || false,
|
||||||
deployedAt: workflowData.deployedAt,
|
deployedAt: workflowData.deployedAt,
|
||||||
metadata: {
|
|
||||||
name: workflowData.name,
|
|
||||||
description: workflowData.description,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
variables: workflowData.variables || {},
|
variables: workflowData.variables || {},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -215,7 +215,6 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const log of logs) {
|
for (const log of logs) {
|
||||||
if (!log.workflowId) continue // Skip logs for deleted workflows
|
|
||||||
const idx = Math.min(
|
const idx = Math.min(
|
||||||
segments - 1,
|
segments - 1,
|
||||||
Math.max(0, Math.floor((log.startedAt.getTime() - start.getTime()) / segmentMs))
|
Math.max(0, Math.floor((log.startedAt.getTime() - start.getTime()) / segmentMs))
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import { memo } from 'react'
|
import { memo } from 'react'
|
||||||
import { cn } from '@/lib/core/utils/cn'
|
import { cn } from '@/lib/core/utils/cn'
|
||||||
import {
|
|
||||||
DELETED_WORKFLOW_COLOR,
|
|
||||||
DELETED_WORKFLOW_LABEL,
|
|
||||||
} from '@/app/workspace/[workspaceId]/logs/utils'
|
|
||||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||||
import { StatusBar, type StatusBarSegment } from '..'
|
import { StatusBar, type StatusBarSegment } from '..'
|
||||||
|
|
||||||
@@ -65,32 +61,22 @@ export function WorkflowsList({
|
|||||||
<div>
|
<div>
|
||||||
{filteredExecutions.map((workflow, idx) => {
|
{filteredExecutions.map((workflow, idx) => {
|
||||||
const isSelected = expandedWorkflowId === workflow.workflowId
|
const isSelected = expandedWorkflowId === workflow.workflowId
|
||||||
const isDeletedWorkflow = workflow.workflowName === DELETED_WORKFLOW_LABEL
|
|
||||||
const workflowColor = isDeletedWorkflow
|
|
||||||
? DELETED_WORKFLOW_COLOR
|
|
||||||
: workflows[workflow.workflowId]?.color || '#64748b'
|
|
||||||
const canToggle = !isDeletedWorkflow
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={workflow.workflowId}
|
key={workflow.workflowId}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex h-[44px] items-center gap-[16px] px-[24px] hover:bg-[var(--surface-3)] dark:hover:bg-[var(--surface-4)]',
|
'flex h-[44px] cursor-pointer items-center gap-[16px] px-[24px] hover:bg-[var(--surface-3)] dark:hover:bg-[var(--surface-4)]',
|
||||||
canToggle ? 'cursor-pointer' : 'cursor-default',
|
|
||||||
isSelected && 'bg-[var(--surface-3)] dark:bg-[var(--surface-4)]'
|
isSelected && 'bg-[var(--surface-3)] dark:bg-[var(--surface-4)]'
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => onToggleWorkflow(workflow.workflowId)}
|
||||||
if (canToggle) {
|
|
||||||
onToggleWorkflow(workflow.workflowId)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{/* Workflow name with color */}
|
{/* Workflow name with color */}
|
||||||
<div className='flex w-[160px] flex-shrink-0 items-center gap-[8px] pr-[8px]'>
|
<div className='flex w-[160px] flex-shrink-0 items-center gap-[8px] pr-[8px]'>
|
||||||
<div
|
<div
|
||||||
className='h-[10px] w-[10px] flex-shrink-0 rounded-[3px]'
|
className='h-[10px] w-[10px] flex-shrink-0 rounded-[3px]'
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: workflowColor,
|
backgroundColor: workflows[workflow.workflowId]?.color || '#64748b',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span className='min-w-0 truncate font-medium text-[12px] text-[var(--text-primary)]'>
|
<span className='min-w-0 truncate font-medium text-[12px] text-[var(--text-primary)]'>
|
||||||
|
|||||||
@@ -80,9 +80,6 @@ export function ExecutionSnapshot({
|
|||||||
}, [executionId, closeMenu])
|
}, [executionId, closeMenu])
|
||||||
|
|
||||||
const workflowState = data?.workflowState as WorkflowState | undefined
|
const workflowState = data?.workflowState as WorkflowState | undefined
|
||||||
const childWorkflowSnapshots = data?.childWorkflowSnapshots as
|
|
||||||
| Record<string, WorkflowState>
|
|
||||||
| undefined
|
|
||||||
|
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
@@ -151,7 +148,6 @@ export function ExecutionSnapshot({
|
|||||||
key={executionId}
|
key={executionId}
|
||||||
workflowState={workflowState}
|
workflowState={workflowState}
|
||||||
traceSpans={traceSpans}
|
traceSpans={traceSpans}
|
||||||
childWorkflowSnapshots={childWorkflowSnapshots}
|
|
||||||
className={className}
|
className={className}
|
||||||
height={height}
|
height={height}
|
||||||
width={width}
|
width={width}
|
||||||
|
|||||||
@@ -26,8 +26,6 @@ import {
|
|||||||
} from '@/app/workspace/[workspaceId]/logs/components'
|
} from '@/app/workspace/[workspaceId]/logs/components'
|
||||||
import { useLogDetailsResize } from '@/app/workspace/[workspaceId]/logs/hooks'
|
import { useLogDetailsResize } from '@/app/workspace/[workspaceId]/logs/hooks'
|
||||||
import {
|
import {
|
||||||
DELETED_WORKFLOW_COLOR,
|
|
||||||
DELETED_WORKFLOW_LABEL,
|
|
||||||
formatDate,
|
formatDate,
|
||||||
getDisplayStatus,
|
getDisplayStatus,
|
||||||
StatusBadge,
|
StatusBadge,
|
||||||
@@ -388,25 +386,22 @@ export const LogDetails = memo(function LogDetails({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Workflow Card */}
|
{/* Workflow Card */}
|
||||||
<div className='flex w-0 min-w-0 flex-1 flex-col gap-[8px]'>
|
{log.workflow && (
|
||||||
<div className='font-medium text-[12px] text-[var(--text-tertiary)]'>
|
<div className='flex w-0 min-w-0 flex-1 flex-col gap-[8px]'>
|
||||||
Workflow
|
<div className='font-medium text-[12px] text-[var(--text-tertiary)]'>
|
||||||
|
Workflow
|
||||||
|
</div>
|
||||||
|
<div className='flex min-w-0 items-center gap-[8px]'>
|
||||||
|
<div
|
||||||
|
className='h-[10px] w-[10px] flex-shrink-0 rounded-[3px]'
|
||||||
|
style={{ backgroundColor: log.workflow?.color }}
|
||||||
|
/>
|
||||||
|
<span className='min-w-0 flex-1 truncate font-medium text-[14px] text-[var(--text-secondary)]'>
|
||||||
|
{log.workflow.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex min-w-0 items-center gap-[8px]'>
|
)}
|
||||||
<div
|
|
||||||
className='h-[10px] w-[10px] flex-shrink-0 rounded-[3px]'
|
|
||||||
style={{
|
|
||||||
backgroundColor:
|
|
||||||
log.workflow?.color ||
|
|
||||||
(!log.workflowId ? DELETED_WORKFLOW_COLOR : undefined),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span className='min-w-0 flex-1 truncate font-medium text-[14px] text-[var(--text-secondary)]'>
|
|
||||||
{log.workflow?.name ||
|
|
||||||
(!log.workflowId ? DELETED_WORKFLOW_LABEL : 'Unknown')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Execution ID */}
|
{/* Execution ID */}
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import { List, type RowComponentProps, useListRef } from 'react-window'
|
|||||||
import { Badge, buttonVariants } from '@/components/emcn'
|
import { Badge, buttonVariants } from '@/components/emcn'
|
||||||
import { cn } from '@/lib/core/utils/cn'
|
import { cn } from '@/lib/core/utils/cn'
|
||||||
import {
|
import {
|
||||||
DELETED_WORKFLOW_COLOR,
|
|
||||||
DELETED_WORKFLOW_LABEL,
|
|
||||||
formatDate,
|
formatDate,
|
||||||
formatDuration,
|
formatDuration,
|
||||||
getDisplayStatus,
|
getDisplayStatus,
|
||||||
@@ -35,11 +33,6 @@ interface LogRowProps {
|
|||||||
const LogRow = memo(
|
const LogRow = memo(
|
||||||
function LogRow({ log, isSelected, onClick, onContextMenu, selectedRowRef }: LogRowProps) {
|
function LogRow({ log, isSelected, onClick, onContextMenu, selectedRowRef }: LogRowProps) {
|
||||||
const formattedDate = useMemo(() => formatDate(log.createdAt), [log.createdAt])
|
const formattedDate = useMemo(() => formatDate(log.createdAt), [log.createdAt])
|
||||||
const isDeletedWorkflow = !log.workflow?.id && !log.workflowId
|
|
||||||
const workflowName = isDeletedWorkflow
|
|
||||||
? DELETED_WORKFLOW_LABEL
|
|
||||||
: log.workflow?.name || 'Unknown'
|
|
||||||
const workflowColor = isDeletedWorkflow ? DELETED_WORKFLOW_COLOR : log.workflow?.color
|
|
||||||
|
|
||||||
const handleClick = useCallback(() => onClick(log), [onClick, log])
|
const handleClick = useCallback(() => onClick(log), [onClick, log])
|
||||||
|
|
||||||
@@ -85,15 +78,10 @@ const LogRow = memo(
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className='h-[10px] w-[10px] flex-shrink-0 rounded-[3px]'
|
className='h-[10px] w-[10px] flex-shrink-0 rounded-[3px]'
|
||||||
style={{ backgroundColor: workflowColor }}
|
style={{ backgroundColor: log.workflow?.color }}
|
||||||
/>
|
/>
|
||||||
<span
|
<span className='min-w-0 truncate font-medium text-[12px] text-[var(--text-primary)]'>
|
||||||
className={cn(
|
{log.workflow?.name || 'Unknown'}
|
||||||
'min-w-0 truncate font-medium text-[12px]',
|
|
||||||
isDeletedWorkflow ? 'text-[var(--text-tertiary)]' : 'text-[var(--text-primary)]'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{workflowName}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -27,9 +27,6 @@ export const LOG_COLUMN_ORDER: readonly LogColumnKey[] = [
|
|||||||
'duration',
|
'duration',
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
export const DELETED_WORKFLOW_LABEL = 'Deleted Workflow'
|
|
||||||
export const DELETED_WORKFLOW_COLOR = 'var(--text-tertiary)'
|
|
||||||
|
|
||||||
export type LogStatus = 'error' | 'pending' | 'running' | 'info' | 'cancelled'
|
export type LogStatus = 'error' | 'pending' | 'running' | 'info' | 'cancelled'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ interface SelectorComboboxProps {
|
|||||||
readOnly?: boolean
|
readOnly?: boolean
|
||||||
onOptionChange?: (value: string) => void
|
onOptionChange?: (value: string) => void
|
||||||
allowSearch?: boolean
|
allowSearch?: boolean
|
||||||
missingOptionLabel?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SelectorCombobox({
|
export function SelectorCombobox({
|
||||||
@@ -38,7 +37,6 @@ export function SelectorCombobox({
|
|||||||
readOnly,
|
readOnly,
|
||||||
onOptionChange,
|
onOptionChange,
|
||||||
allowSearch = true,
|
allowSearch = true,
|
||||||
missingOptionLabel,
|
|
||||||
}: SelectorComboboxProps) {
|
}: SelectorComboboxProps) {
|
||||||
const [storeValueRaw, setStoreValue] = useSubBlockValue<string | null | undefined>(
|
const [storeValueRaw, setStoreValue] = useSubBlockValue<string | null | undefined>(
|
||||||
blockId,
|
blockId,
|
||||||
@@ -62,16 +60,7 @@ export function SelectorCombobox({
|
|||||||
detailId: activeValue,
|
detailId: activeValue,
|
||||||
})
|
})
|
||||||
const optionMap = useSelectorOptionMap(options, detailOption ?? undefined)
|
const optionMap = useSelectorOptionMap(options, detailOption ?? undefined)
|
||||||
const hasMissingOption =
|
const selectedLabel = activeValue ? (optionMap.get(activeValue)?.label ?? activeValue) : ''
|
||||||
Boolean(activeValue) &&
|
|
||||||
Boolean(missingOptionLabel) &&
|
|
||||||
!isLoading &&
|
|
||||||
!optionMap.get(activeValue!)
|
|
||||||
const selectedLabel = activeValue
|
|
||||||
? hasMissingOption
|
|
||||||
? missingOptionLabel
|
|
||||||
: (optionMap.get(activeValue)?.label ?? activeValue)
|
|
||||||
: ''
|
|
||||||
const [inputValue, setInputValue] = useState(selectedLabel)
|
const [inputValue, setInputValue] = useState(selectedLabel)
|
||||||
const previousActiveValue = useRef<string | undefined>(activeValue)
|
const previousActiveValue = useRef<string | undefined>(activeValue)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { DELETED_WORKFLOW_LABEL } from '@/app/workspace/[workspaceId]/logs/utils'
|
|
||||||
import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox'
|
import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox'
|
||||||
import type { SubBlockConfig } from '@/blocks/types'
|
import type { SubBlockConfig } from '@/blocks/types'
|
||||||
import type { SelectorContext } from '@/hooks/selectors/types'
|
import type { SelectorContext } from '@/hooks/selectors/types'
|
||||||
@@ -41,7 +40,6 @@ export function WorkflowSelectorInput({
|
|||||||
isPreview={isPreview}
|
isPreview={isPreview}
|
||||||
previewValue={previewValue}
|
previewValue={previewValue}
|
||||||
placeholder={subBlock.placeholder || 'Select workflow...'}
|
placeholder={subBlock.placeholder || 'Select workflow...'}
|
||||||
missingOptionLabel={DELETED_WORKFLOW_LABEL}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,14 +4,11 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|||||||
import {
|
import {
|
||||||
ArrowDown,
|
ArrowDown,
|
||||||
ArrowUp,
|
ArrowUp,
|
||||||
Check,
|
|
||||||
ChevronDown as ChevronDownIcon,
|
ChevronDown as ChevronDownIcon,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
Clipboard,
|
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
Maximize2,
|
Maximize2,
|
||||||
RepeatIcon,
|
RepeatIcon,
|
||||||
Search,
|
|
||||||
SplitIcon,
|
SplitIcon,
|
||||||
X,
|
X,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
@@ -37,7 +34,6 @@ import {
|
|||||||
isSubBlockFeatureEnabled,
|
isSubBlockFeatureEnabled,
|
||||||
isSubBlockVisibleForMode,
|
isSubBlockVisibleForMode,
|
||||||
} from '@/lib/workflows/subblocks/visibility'
|
} from '@/lib/workflows/subblocks/visibility'
|
||||||
import { DELETED_WORKFLOW_LABEL } from '@/app/workspace/[workspaceId]/logs/utils'
|
|
||||||
import { SubBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components'
|
import { SubBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components'
|
||||||
import { PreviewContextMenu } from '@/app/workspace/[workspaceId]/w/components/preview/components/preview-context-menu'
|
import { PreviewContextMenu } from '@/app/workspace/[workspaceId]/w/components/preview/components/preview-context-menu'
|
||||||
import { PreviewWorkflow } from '@/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow'
|
import { PreviewWorkflow } from '@/app/workspace/[workspaceId]/w/components/preview/components/preview-workflow'
|
||||||
@@ -694,7 +690,6 @@ interface ExecutionData {
|
|||||||
output?: unknown
|
output?: unknown
|
||||||
status?: string
|
status?: string
|
||||||
durationMs?: number
|
durationMs?: number
|
||||||
childWorkflowSnapshotId?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WorkflowVariable {
|
interface WorkflowVariable {
|
||||||
@@ -719,8 +714,6 @@ interface PreviewEditorProps {
|
|||||||
parallels?: Record<string, Parallel>
|
parallels?: Record<string, Parallel>
|
||||||
/** When true, shows "Not Executed" badge if no executionData is provided */
|
/** When true, shows "Not Executed" badge if no executionData is provided */
|
||||||
isExecutionMode?: boolean
|
isExecutionMode?: boolean
|
||||||
/** Child workflow snapshots keyed by snapshot ID (execution mode only) */
|
|
||||||
childWorkflowSnapshots?: Record<string, WorkflowState>
|
|
||||||
/** Optional close handler - if not provided, no close button is shown */
|
/** Optional close handler - if not provided, no close button is shown */
|
||||||
onClose?: () => void
|
onClose?: () => void
|
||||||
/** Callback to drill down into a nested workflow block */
|
/** Callback to drill down into a nested workflow block */
|
||||||
@@ -746,7 +739,6 @@ function PreviewEditorContent({
|
|||||||
loops,
|
loops,
|
||||||
parallels,
|
parallels,
|
||||||
isExecutionMode = false,
|
isExecutionMode = false,
|
||||||
childWorkflowSnapshots,
|
|
||||||
onClose,
|
onClose,
|
||||||
onDrillDown,
|
onDrillDown,
|
||||||
}: PreviewEditorProps) {
|
}: PreviewEditorProps) {
|
||||||
@@ -776,35 +768,17 @@ function PreviewEditorContent({
|
|||||||
const { data: childWorkflowState, isLoading: isLoadingChildWorkflow } = useWorkflowState(
|
const { data: childWorkflowState, isLoading: isLoadingChildWorkflow } = useWorkflowState(
|
||||||
childWorkflowId ?? undefined
|
childWorkflowId ?? undefined
|
||||||
)
|
)
|
||||||
const childWorkflowSnapshotId = executionData?.childWorkflowSnapshotId
|
|
||||||
const childWorkflowSnapshotState = childWorkflowSnapshotId
|
|
||||||
? childWorkflowSnapshots?.[childWorkflowSnapshotId]
|
|
||||||
: undefined
|
|
||||||
const resolvedChildWorkflowState = isExecutionMode
|
|
||||||
? childWorkflowSnapshotState
|
|
||||||
: childWorkflowState
|
|
||||||
const resolvedIsLoadingChildWorkflow = isExecutionMode ? false : isLoadingChildWorkflow
|
|
||||||
const isMissingChildWorkflow =
|
|
||||||
Boolean(childWorkflowId) && !resolvedIsLoadingChildWorkflow && !resolvedChildWorkflowState
|
|
||||||
|
|
||||||
/** Drills down into the child workflow or opens it in a new tab */
|
/** Drills down into the child workflow or opens it in a new tab */
|
||||||
const handleExpandChildWorkflow = useCallback(() => {
|
const handleExpandChildWorkflow = useCallback(() => {
|
||||||
if (!childWorkflowId) return
|
if (!childWorkflowId || !childWorkflowState) return
|
||||||
|
|
||||||
if (isExecutionMode && onDrillDown) {
|
if (isExecutionMode && onDrillDown) {
|
||||||
if (!childWorkflowSnapshotState) return
|
onDrillDown(block.id, childWorkflowState)
|
||||||
onDrillDown(block.id, childWorkflowSnapshotState)
|
|
||||||
} else if (workspaceId) {
|
} else if (workspaceId) {
|
||||||
window.open(`/workspace/${workspaceId}/w/${childWorkflowId}`, '_blank', 'noopener,noreferrer')
|
window.open(`/workspace/${workspaceId}/w/${childWorkflowId}`, '_blank', 'noopener,noreferrer')
|
||||||
}
|
}
|
||||||
}, [
|
}, [childWorkflowId, childWorkflowState, isExecutionMode, onDrillDown, block.id, workspaceId])
|
||||||
childWorkflowId,
|
|
||||||
childWorkflowSnapshotState,
|
|
||||||
isExecutionMode,
|
|
||||||
onDrillDown,
|
|
||||||
block.id,
|
|
||||||
workspaceId,
|
|
||||||
])
|
|
||||||
|
|
||||||
const contentRef = useRef<HTMLDivElement>(null)
|
const contentRef = useRef<HTMLDivElement>(null)
|
||||||
const subBlocksRef = useRef<HTMLDivElement>(null)
|
const subBlocksRef = useRef<HTMLDivElement>(null)
|
||||||
@@ -839,13 +813,6 @@ function PreviewEditorContent({
|
|||||||
} = useContextMenu()
|
} = useContextMenu()
|
||||||
|
|
||||||
const [contextMenuData, setContextMenuData] = useState({ content: '', copyOnly: false })
|
const [contextMenuData, setContextMenuData] = useState({ content: '', copyOnly: false })
|
||||||
const [copiedSection, setCopiedSection] = useState<'input' | 'output' | null>(null)
|
|
||||||
|
|
||||||
const handleCopySection = useCallback((content: string, section: 'input' | 'output') => {
|
|
||||||
navigator.clipboard.writeText(content)
|
|
||||||
setCopiedSection(section)
|
|
||||||
setTimeout(() => setCopiedSection(null), 1500)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const openContextMenu = useCallback(
|
const openContextMenu = useCallback(
|
||||||
(e: React.MouseEvent, content: string, copyOnly: boolean) => {
|
(e: React.MouseEvent, content: string, copyOnly: boolean) => {
|
||||||
@@ -895,6 +862,9 @@ function PreviewEditorContent({
|
|||||||
}
|
}
|
||||||
}, [contextMenuData.content])
|
}, [contextMenuData.content])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles mouse down event on the resize handle to initiate resizing
|
||||||
|
*/
|
||||||
const handleConnectionsResizeMouseDown = useCallback(
|
const handleConnectionsResizeMouseDown = useCallback(
|
||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
setIsResizing(true)
|
setIsResizing(true)
|
||||||
@@ -904,12 +874,18 @@ function PreviewEditorContent({
|
|||||||
[connectionsHeight]
|
[connectionsHeight]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle connections collapsed state
|
||||||
|
*/
|
||||||
const toggleConnectionsCollapsed = useCallback(() => {
|
const toggleConnectionsCollapsed = useCallback(() => {
|
||||||
setConnectionsHeight((prev) =>
|
setConnectionsHeight((prev) =>
|
||||||
prev <= MIN_CONNECTIONS_HEIGHT ? DEFAULT_CONNECTIONS_HEIGHT : MIN_CONNECTIONS_HEIGHT
|
prev <= MIN_CONNECTIONS_HEIGHT ? DEFAULT_CONNECTIONS_HEIGHT : MIN_CONNECTIONS_HEIGHT
|
||||||
)
|
)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up resize event listeners during resize operations
|
||||||
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isResizing) return
|
if (!isResizing) return
|
||||||
|
|
||||||
@@ -1229,11 +1205,7 @@ function PreviewEditorContent({
|
|||||||
}
|
}
|
||||||
emptyMessage='No input data'
|
emptyMessage='No input data'
|
||||||
>
|
>
|
||||||
<div
|
<div onContextMenu={handleExecutionContextMenu} ref={contentRef}>
|
||||||
onContextMenu={handleExecutionContextMenu}
|
|
||||||
ref={contentRef}
|
|
||||||
className='relative'
|
|
||||||
>
|
|
||||||
<Code.Viewer
|
<Code.Viewer
|
||||||
code={formatValueAsJson(executionData.input)}
|
code={formatValueAsJson(executionData.input)}
|
||||||
language='json'
|
language='json'
|
||||||
@@ -1243,49 +1215,6 @@ function PreviewEditorContent({
|
|||||||
currentMatchIndex={currentMatchIndex}
|
currentMatchIndex={currentMatchIndex}
|
||||||
onMatchCountChange={handleMatchCountChange}
|
onMatchCountChange={handleMatchCountChange}
|
||||||
/>
|
/>
|
||||||
{/* Action buttons overlay */}
|
|
||||||
{!isSearchActive && (
|
|
||||||
<div className='absolute top-[7px] right-[6px] z-10 flex gap-[4px]'>
|
|
||||||
<Tooltip.Root>
|
|
||||||
<Tooltip.Trigger asChild>
|
|
||||||
<Button
|
|
||||||
type='button'
|
|
||||||
variant='ghost'
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
handleCopySection(formatValueAsJson(executionData.input), 'input')
|
|
||||||
}}
|
|
||||||
className='h-[20px] w-[20px] cursor-pointer border border-[var(--border-1)] bg-transparent p-0 backdrop-blur-sm hover:bg-[var(--surface-4)]'
|
|
||||||
>
|
|
||||||
{copiedSection === 'input' ? (
|
|
||||||
<Check className='h-[10px] w-[10px] text-[var(--text-success)]' />
|
|
||||||
) : (
|
|
||||||
<Clipboard className='h-[10px] w-[10px]' />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</Tooltip.Trigger>
|
|
||||||
<Tooltip.Content side='top'>
|
|
||||||
{copiedSection === 'input' ? 'Copied' : 'Copy'}
|
|
||||||
</Tooltip.Content>
|
|
||||||
</Tooltip.Root>
|
|
||||||
<Tooltip.Root>
|
|
||||||
<Tooltip.Trigger asChild>
|
|
||||||
<Button
|
|
||||||
type='button'
|
|
||||||
variant='ghost'
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
activateSearch()
|
|
||||||
}}
|
|
||||||
className='h-[20px] w-[20px] cursor-pointer border border-[var(--border-1)] bg-transparent p-0 backdrop-blur-sm hover:bg-[var(--surface-4)]'
|
|
||||||
>
|
|
||||||
<Search className='h-[10px] w-[10px]' />
|
|
||||||
</Button>
|
|
||||||
</Tooltip.Trigger>
|
|
||||||
<Tooltip.Content side='top'>Search</Tooltip.Content>
|
|
||||||
</Tooltip.Root>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</CollapsibleSection>
|
</CollapsibleSection>
|
||||||
)}
|
)}
|
||||||
@@ -1302,7 +1231,7 @@ function PreviewEditorContent({
|
|||||||
emptyMessage='No output data'
|
emptyMessage='No output data'
|
||||||
isError={executionData.status === 'error'}
|
isError={executionData.status === 'error'}
|
||||||
>
|
>
|
||||||
<div onContextMenu={handleExecutionContextMenu} className='relative'>
|
<div onContextMenu={handleExecutionContextMenu}>
|
||||||
<Code.Viewer
|
<Code.Viewer
|
||||||
code={formatValueAsJson(executionData.output)}
|
code={formatValueAsJson(executionData.output)}
|
||||||
language='json'
|
language='json'
|
||||||
@@ -1315,49 +1244,6 @@ function PreviewEditorContent({
|
|||||||
currentMatchIndex={currentMatchIndex}
|
currentMatchIndex={currentMatchIndex}
|
||||||
onMatchCountChange={handleMatchCountChange}
|
onMatchCountChange={handleMatchCountChange}
|
||||||
/>
|
/>
|
||||||
{/* Action buttons overlay */}
|
|
||||||
{!isSearchActive && (
|
|
||||||
<div className='absolute top-[7px] right-[6px] z-10 flex gap-[4px]'>
|
|
||||||
<Tooltip.Root>
|
|
||||||
<Tooltip.Trigger asChild>
|
|
||||||
<Button
|
|
||||||
type='button'
|
|
||||||
variant='ghost'
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
handleCopySection(formatValueAsJson(executionData.output), 'output')
|
|
||||||
}}
|
|
||||||
className='h-[20px] w-[20px] cursor-pointer border border-[var(--border-1)] bg-transparent p-0 backdrop-blur-sm hover:bg-[var(--surface-4)]'
|
|
||||||
>
|
|
||||||
{copiedSection === 'output' ? (
|
|
||||||
<Check className='h-[10px] w-[10px] text-[var(--text-success)]' />
|
|
||||||
) : (
|
|
||||||
<Clipboard className='h-[10px] w-[10px]' />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</Tooltip.Trigger>
|
|
||||||
<Tooltip.Content side='top'>
|
|
||||||
{copiedSection === 'output' ? 'Copied' : 'Copy'}
|
|
||||||
</Tooltip.Content>
|
|
||||||
</Tooltip.Root>
|
|
||||||
<Tooltip.Root>
|
|
||||||
<Tooltip.Trigger asChild>
|
|
||||||
<Button
|
|
||||||
type='button'
|
|
||||||
variant='ghost'
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
activateSearch()
|
|
||||||
}}
|
|
||||||
className='h-[20px] w-[20px] cursor-pointer border border-[var(--border-1)] bg-transparent p-0 backdrop-blur-sm hover:bg-[var(--surface-4)]'
|
|
||||||
>
|
|
||||||
<Search className='h-[10px] w-[10px]' />
|
|
||||||
</Button>
|
|
||||||
</Tooltip.Trigger>
|
|
||||||
<Tooltip.Content side='top'>Search</Tooltip.Content>
|
|
||||||
</Tooltip.Root>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</CollapsibleSection>
|
</CollapsibleSection>
|
||||||
)}
|
)}
|
||||||
@@ -1370,7 +1256,7 @@ function PreviewEditorContent({
|
|||||||
Workflow Preview
|
Workflow Preview
|
||||||
</div>
|
</div>
|
||||||
<div className='relative h-[160px] overflow-hidden rounded-[4px] border border-[var(--border)]'>
|
<div className='relative h-[160px] overflow-hidden rounded-[4px] border border-[var(--border)]'>
|
||||||
{resolvedIsLoadingChildWorkflow ? (
|
{isLoadingChildWorkflow ? (
|
||||||
<div className='flex h-full items-center justify-center bg-[var(--surface-3)]'>
|
<div className='flex h-full items-center justify-center bg-[var(--surface-3)]'>
|
||||||
<div
|
<div
|
||||||
className='h-[18px] w-[18px] animate-spin rounded-full'
|
className='h-[18px] w-[18px] animate-spin rounded-full'
|
||||||
@@ -1383,11 +1269,11 @@ function PreviewEditorContent({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : resolvedChildWorkflowState ? (
|
) : childWorkflowState ? (
|
||||||
<>
|
<>
|
||||||
<div className='[&_*:active]:!cursor-grabbing [&_*]:!cursor-grab [&_.react-flow__handle]:!hidden h-full w-full'>
|
<div className='[&_*:active]:!cursor-grabbing [&_*]:!cursor-grab [&_.react-flow__handle]:!hidden h-full w-full'>
|
||||||
<PreviewWorkflow
|
<PreviewWorkflow
|
||||||
workflowState={resolvedChildWorkflowState}
|
workflowState={childWorkflowState}
|
||||||
height={160}
|
height={160}
|
||||||
width='100%'
|
width='100%'
|
||||||
isPannable={true}
|
isPannable={true}
|
||||||
@@ -1419,9 +1305,7 @@ function PreviewEditorContent({
|
|||||||
) : (
|
) : (
|
||||||
<div className='flex h-full items-center justify-center bg-[var(--surface-3)]'>
|
<div className='flex h-full items-center justify-center bg-[var(--surface-3)]'>
|
||||||
<span className='text-[13px] text-[var(--text-tertiary)]'>
|
<span className='text-[13px] text-[var(--text-tertiary)]'>
|
||||||
{isMissingChildWorkflow
|
Unable to load preview
|
||||||
? DELETED_WORKFLOW_LABEL
|
|
||||||
: 'Unable to load preview'}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
isSubBlockFeatureEnabled,
|
isSubBlockFeatureEnabled,
|
||||||
isSubBlockVisibleForMode,
|
isSubBlockVisibleForMode,
|
||||||
} from '@/lib/workflows/subblocks/visibility'
|
} from '@/lib/workflows/subblocks/visibility'
|
||||||
import { DELETED_WORKFLOW_LABEL } from '@/app/workspace/[workspaceId]/logs/utils'
|
|
||||||
import { getDisplayValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block'
|
import { getDisplayValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block'
|
||||||
import { getBlock } from '@/blocks'
|
import { getBlock } from '@/blocks'
|
||||||
import { SELECTOR_TYPES_HYDRATION_REQUIRED, type SubBlockConfig } from '@/blocks/types'
|
import { SELECTOR_TYPES_HYDRATION_REQUIRED, type SubBlockConfig } from '@/blocks/types'
|
||||||
@@ -113,7 +112,7 @@ function resolveWorkflowName(
|
|||||||
if (!rawValue || typeof rawValue !== 'string') return null
|
if (!rawValue || typeof rawValue !== 'string') return null
|
||||||
|
|
||||||
const workflowMap = useWorkflowRegistry.getState().workflows
|
const workflowMap = useWorkflowRegistry.getState().workflows
|
||||||
return workflowMap[rawValue]?.name ?? DELETED_WORKFLOW_LABEL
|
return workflowMap[rawValue]?.name ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -19,8 +19,6 @@ interface TraceSpan {
|
|||||||
status?: string
|
status?: string
|
||||||
duration?: number
|
duration?: number
|
||||||
children?: TraceSpan[]
|
children?: TraceSpan[]
|
||||||
childWorkflowSnapshotId?: string
|
|
||||||
childWorkflowId?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BlockExecutionData {
|
interface BlockExecutionData {
|
||||||
@@ -30,7 +28,6 @@ interface BlockExecutionData {
|
|||||||
durationMs: number
|
durationMs: number
|
||||||
/** Child trace spans for nested workflow blocks */
|
/** Child trace spans for nested workflow blocks */
|
||||||
children?: TraceSpan[]
|
children?: TraceSpan[]
|
||||||
childWorkflowSnapshotId?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Represents a level in the workflow navigation stack */
|
/** Represents a level in the workflow navigation stack */
|
||||||
@@ -38,7 +35,6 @@ interface WorkflowStackEntry {
|
|||||||
workflowState: WorkflowState
|
workflowState: WorkflowState
|
||||||
traceSpans: TraceSpan[]
|
traceSpans: TraceSpan[]
|
||||||
blockExecutions: Record<string, BlockExecutionData>
|
blockExecutions: Record<string, BlockExecutionData>
|
||||||
workflowName: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -93,7 +89,6 @@ export function buildBlockExecutions(spans: TraceSpan[]): Record<string, BlockEx
|
|||||||
status: span.status || 'unknown',
|
status: span.status || 'unknown',
|
||||||
durationMs: span.duration || 0,
|
durationMs: span.duration || 0,
|
||||||
children: span.children,
|
children: span.children,
|
||||||
childWorkflowSnapshotId: span.childWorkflowSnapshotId,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,8 +103,6 @@ interface PreviewProps {
|
|||||||
traceSpans?: TraceSpan[]
|
traceSpans?: TraceSpan[]
|
||||||
/** Pre-computed block executions (optional - will be built from traceSpans if not provided) */
|
/** Pre-computed block executions (optional - will be built from traceSpans if not provided) */
|
||||||
blockExecutions?: Record<string, BlockExecutionData>
|
blockExecutions?: Record<string, BlockExecutionData>
|
||||||
/** Child workflow snapshots keyed by snapshot ID (execution mode only) */
|
|
||||||
childWorkflowSnapshots?: Record<string, WorkflowState>
|
|
||||||
/** Additional CSS class names */
|
/** Additional CSS class names */
|
||||||
className?: string
|
className?: string
|
||||||
/** Height of the component */
|
/** Height of the component */
|
||||||
@@ -142,7 +135,6 @@ export function Preview({
|
|||||||
workflowState: rootWorkflowState,
|
workflowState: rootWorkflowState,
|
||||||
traceSpans: rootTraceSpans,
|
traceSpans: rootTraceSpans,
|
||||||
blockExecutions: providedBlockExecutions,
|
blockExecutions: providedBlockExecutions,
|
||||||
childWorkflowSnapshots,
|
|
||||||
className,
|
className,
|
||||||
height = '100%',
|
height = '100%',
|
||||||
width = '100%',
|
width = '100%',
|
||||||
@@ -152,6 +144,7 @@ export function Preview({
|
|||||||
initialSelectedBlockId,
|
initialSelectedBlockId,
|
||||||
autoSelectLeftmost = true,
|
autoSelectLeftmost = true,
|
||||||
}: PreviewProps) {
|
}: PreviewProps) {
|
||||||
|
/** Initialize pinnedBlockId synchronously to ensure sidebar is present from first render */
|
||||||
const [pinnedBlockId, setPinnedBlockId] = useState<string | null>(() => {
|
const [pinnedBlockId, setPinnedBlockId] = useState<string | null>(() => {
|
||||||
if (initialSelectedBlockId) return initialSelectedBlockId
|
if (initialSelectedBlockId) return initialSelectedBlockId
|
||||||
if (autoSelectLeftmost) {
|
if (autoSelectLeftmost) {
|
||||||
@@ -160,14 +153,17 @@ export function Preview({
|
|||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/** Stack for nested workflow navigation. Empty means we're at the root level. */
|
||||||
const [workflowStack, setWorkflowStack] = useState<WorkflowStackEntry[]>([])
|
const [workflowStack, setWorkflowStack] = useState<WorkflowStackEntry[]>([])
|
||||||
|
|
||||||
|
/** Block executions for the root level */
|
||||||
const rootBlockExecutions = useMemo(() => {
|
const rootBlockExecutions = useMemo(() => {
|
||||||
if (providedBlockExecutions) return providedBlockExecutions
|
if (providedBlockExecutions) return providedBlockExecutions
|
||||||
if (!rootTraceSpans || !Array.isArray(rootTraceSpans)) return {}
|
if (!rootTraceSpans || !Array.isArray(rootTraceSpans)) return {}
|
||||||
return buildBlockExecutions(rootTraceSpans)
|
return buildBlockExecutions(rootTraceSpans)
|
||||||
}, [providedBlockExecutions, rootTraceSpans])
|
}, [providedBlockExecutions, rootTraceSpans])
|
||||||
|
|
||||||
|
/** Current block executions - either from stack or root */
|
||||||
const blockExecutions = useMemo(() => {
|
const blockExecutions = useMemo(() => {
|
||||||
if (workflowStack.length > 0) {
|
if (workflowStack.length > 0) {
|
||||||
return workflowStack[workflowStack.length - 1].blockExecutions
|
return workflowStack[workflowStack.length - 1].blockExecutions
|
||||||
@@ -175,6 +171,7 @@ export function Preview({
|
|||||||
return rootBlockExecutions
|
return rootBlockExecutions
|
||||||
}, [workflowStack, rootBlockExecutions])
|
}, [workflowStack, rootBlockExecutions])
|
||||||
|
|
||||||
|
/** Current workflow state - either from stack or root */
|
||||||
const workflowState = useMemo(() => {
|
const workflowState = useMemo(() => {
|
||||||
if (workflowStack.length > 0) {
|
if (workflowStack.length > 0) {
|
||||||
return workflowStack[workflowStack.length - 1].workflowState
|
return workflowStack[workflowStack.length - 1].workflowState
|
||||||
@@ -182,39 +179,41 @@ export function Preview({
|
|||||||
return rootWorkflowState
|
return rootWorkflowState
|
||||||
}, [workflowStack, rootWorkflowState])
|
}, [workflowStack, rootWorkflowState])
|
||||||
|
|
||||||
|
/** Whether we're in execution mode (have trace spans/block executions) */
|
||||||
const isExecutionMode = useMemo(() => {
|
const isExecutionMode = useMemo(() => {
|
||||||
return Object.keys(blockExecutions).length > 0
|
return Object.keys(blockExecutions).length > 0
|
||||||
}, [blockExecutions])
|
}, [blockExecutions])
|
||||||
|
|
||||||
|
/** Handler to drill down into a nested workflow block */
|
||||||
const handleDrillDown = useCallback(
|
const handleDrillDown = useCallback(
|
||||||
(blockId: string, childWorkflowState: WorkflowState) => {
|
(blockId: string, childWorkflowState: WorkflowState) => {
|
||||||
const blockExecution = blockExecutions[blockId]
|
const blockExecution = blockExecutions[blockId]
|
||||||
const childTraceSpans = extractChildTraceSpans(blockExecution)
|
const childTraceSpans = extractChildTraceSpans(blockExecution)
|
||||||
const childBlockExecutions = buildBlockExecutions(childTraceSpans)
|
const childBlockExecutions = buildBlockExecutions(childTraceSpans)
|
||||||
|
|
||||||
const workflowName = childWorkflowState.metadata?.name || 'Nested Workflow'
|
|
||||||
|
|
||||||
setWorkflowStack((prev) => [
|
setWorkflowStack((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
{
|
{
|
||||||
workflowState: childWorkflowState,
|
workflowState: childWorkflowState,
|
||||||
traceSpans: childTraceSpans,
|
traceSpans: childTraceSpans,
|
||||||
blockExecutions: childBlockExecutions,
|
blockExecutions: childBlockExecutions,
|
||||||
workflowName,
|
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
|
/** Set pinned block synchronously to avoid double fitView from sidebar resize */
|
||||||
const leftmostId = getLeftmostBlockId(childWorkflowState)
|
const leftmostId = getLeftmostBlockId(childWorkflowState)
|
||||||
setPinnedBlockId(leftmostId)
|
setPinnedBlockId(leftmostId)
|
||||||
},
|
},
|
||||||
[blockExecutions]
|
[blockExecutions]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/** Handler to go back up the stack */
|
||||||
const handleGoBack = useCallback(() => {
|
const handleGoBack = useCallback(() => {
|
||||||
setWorkflowStack((prev) => prev.slice(0, -1))
|
setWorkflowStack((prev) => prev.slice(0, -1))
|
||||||
setPinnedBlockId(null)
|
setPinnedBlockId(null)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
/** Handlers for node interactions - memoized to prevent unnecessary re-renders */
|
||||||
const handleNodeClick = useCallback((blockId: string) => {
|
const handleNodeClick = useCallback((blockId: string) => {
|
||||||
setPinnedBlockId(blockId)
|
setPinnedBlockId(blockId)
|
||||||
}, [])
|
}, [])
|
||||||
@@ -233,8 +232,6 @@ export function Preview({
|
|||||||
|
|
||||||
const isNested = workflowStack.length > 0
|
const isNested = workflowStack.length > 0
|
||||||
|
|
||||||
const currentWorkflowName = isNested ? workflowStack[workflowStack.length - 1].workflowName : null
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{ height, width }}
|
style={{ height, width }}
|
||||||
@@ -245,27 +242,20 @@ export function Preview({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isNested && (
|
{isNested && (
|
||||||
<div className='absolute top-[12px] left-[12px] z-20 flex items-center gap-[6px]'>
|
<div className='absolute top-[12px] left-[12px] z-20'>
|
||||||
<Tooltip.Root>
|
<Tooltip.Root>
|
||||||
<Tooltip.Trigger asChild>
|
<Tooltip.Trigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
onClick={handleGoBack}
|
onClick={handleGoBack}
|
||||||
className='flex h-[28px] items-center gap-[5px] rounded-[6px] border border-[var(--border)] bg-[var(--surface-2)] px-[10px] text-[var(--text-secondary)] shadow-sm hover:bg-[var(--surface-4)] hover:text-[var(--text-primary)]'
|
className='flex h-[30px] items-center gap-[5px] border border-[var(--border)] bg-[var(--surface-2)] px-[10px] hover:bg-[var(--surface-4)]'
|
||||||
>
|
>
|
||||||
<ArrowLeft className='h-[12px] w-[12px]' />
|
<ArrowLeft className='h-[13px] w-[13px]' />
|
||||||
<span className='font-medium text-[12px]'>Back</span>
|
<span className='font-medium text-[13px]'>Back</span>
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip.Trigger>
|
</Tooltip.Trigger>
|
||||||
<Tooltip.Content side='bottom'>Go back to parent workflow</Tooltip.Content>
|
<Tooltip.Content side='bottom'>Go back to parent workflow</Tooltip.Content>
|
||||||
</Tooltip.Root>
|
</Tooltip.Root>
|
||||||
{currentWorkflowName && (
|
|
||||||
<div className='flex h-[28px] max-w-[200px] items-center rounded-[6px] border border-[var(--border)] bg-[var(--surface-2)] px-[10px] shadow-sm'>
|
|
||||||
<span className='truncate font-medium text-[12px] text-[var(--text-secondary)]'>
|
|
||||||
{currentWorkflowName}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -294,7 +284,6 @@ export function Preview({
|
|||||||
loops={workflowState.loops}
|
loops={workflowState.loops}
|
||||||
parallels={workflowState.parallels}
|
parallels={workflowState.parallels}
|
||||||
isExecutionMode={isExecutionMode}
|
isExecutionMode={isExecutionMode}
|
||||||
childWorkflowSnapshots={childWorkflowSnapshots}
|
|
||||||
onClose={handleEditorClose}
|
onClose={handleEditorClose}
|
||||||
onDrillDown={handleDrillDown}
|
onDrillDown={handleDrillDown}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
export { AccessControl } from './access-control/access-control'
|
|
||||||
export { ApiKeys } from './api-keys/api-keys'
|
export { ApiKeys } from './api-keys/api-keys'
|
||||||
export { BYOK } from './byok/byok'
|
export { BYOK } from './byok/byok'
|
||||||
export { Copilot } from './copilot/copilot'
|
export { Copilot } from './copilot/copilot'
|
||||||
@@ -10,7 +9,6 @@ export { Files as FileUploads } from './files/files'
|
|||||||
export { General } from './general/general'
|
export { General } from './general/general'
|
||||||
export { Integrations } from './integrations/integrations'
|
export { Integrations } from './integrations/integrations'
|
||||||
export { MCP } from './mcp/mcp'
|
export { MCP } from './mcp/mcp'
|
||||||
export { SSO } from './sso/sso'
|
|
||||||
export { Subscription } from './subscription/subscription'
|
export { Subscription } from './subscription/subscription'
|
||||||
export { TeamManagement } from './team-management/team-management'
|
export { TeamManagement } from './team-management/team-management'
|
||||||
export { WorkflowMcpServers } from './workflow-mcp-servers/workflow-mcp-servers'
|
export { WorkflowMcpServers } from './workflow-mcp-servers/workflow-mcp-servers'
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ import { getEnv, isTruthy } from '@/lib/core/config/env'
|
|||||||
import { isHosted } from '@/lib/core/config/feature-flags'
|
import { isHosted } from '@/lib/core/config/feature-flags'
|
||||||
import { getUserRole } from '@/lib/workspaces/organization'
|
import { getUserRole } from '@/lib/workspaces/organization'
|
||||||
import {
|
import {
|
||||||
AccessControl,
|
|
||||||
ApiKeys,
|
ApiKeys,
|
||||||
BYOK,
|
BYOK,
|
||||||
Copilot,
|
Copilot,
|
||||||
@@ -53,15 +52,15 @@ import {
|
|||||||
General,
|
General,
|
||||||
Integrations,
|
Integrations,
|
||||||
MCP,
|
MCP,
|
||||||
SSO,
|
|
||||||
Subscription,
|
Subscription,
|
||||||
TeamManagement,
|
TeamManagement,
|
||||||
WorkflowMcpServers,
|
WorkflowMcpServers,
|
||||||
} from '@/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components'
|
} from '@/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components'
|
||||||
import { TemplateProfile } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/template-profile/template-profile'
|
import { TemplateProfile } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/template-profile/template-profile'
|
||||||
|
import { AccessControl } from '@/ee/access-control'
|
||||||
|
import { SSO, ssoKeys, useSSOProviders } from '@/ee/sso'
|
||||||
import { generalSettingsKeys, useGeneralSettings } from '@/hooks/queries/general-settings'
|
import { generalSettingsKeys, useGeneralSettings } from '@/hooks/queries/general-settings'
|
||||||
import { organizationKeys, useOrganizations } from '@/hooks/queries/organization'
|
import { organizationKeys, useOrganizations } from '@/hooks/queries/organization'
|
||||||
import { ssoKeys, useSSOProviders } from '@/hooks/queries/sso'
|
|
||||||
import { subscriptionKeys, useSubscriptionData } from '@/hooks/queries/subscription'
|
import { subscriptionKeys, useSubscriptionData } from '@/hooks/queries/subscription'
|
||||||
import { usePermissionConfig } from '@/hooks/use-permission-config'
|
import { usePermissionConfig } from '@/hooks/use-permission-config'
|
||||||
import { useSettingsModalStore } from '@/stores/modals/settings/store'
|
import { useSettingsModalStore } from '@/stores/modals/settings/store'
|
||||||
|
|||||||
@@ -66,10 +66,7 @@ function generateSignature(secret: string, timestamp: number, body: string): str
|
|||||||
async function buildPayload(
|
async function buildPayload(
|
||||||
log: WorkflowExecutionLog,
|
log: WorkflowExecutionLog,
|
||||||
subscription: typeof workspaceNotificationSubscription.$inferSelect
|
subscription: typeof workspaceNotificationSubscription.$inferSelect
|
||||||
): Promise<NotificationPayload | null> {
|
): Promise<NotificationPayload> {
|
||||||
// Skip notifications for deleted workflows
|
|
||||||
if (!log.workflowId) return null
|
|
||||||
|
|
||||||
const workflowData = await db
|
const workflowData = await db
|
||||||
.select({ name: workflowTable.name, userId: workflowTable.userId })
|
.select({ name: workflowTable.name, userId: workflowTable.userId })
|
||||||
.from(workflowTable)
|
.from(workflowTable)
|
||||||
@@ -529,13 +526,6 @@ export async function executeNotificationDelivery(params: NotificationDeliveryPa
|
|||||||
const attempts = claimed[0].attempts
|
const attempts = claimed[0].attempts
|
||||||
const payload = await buildPayload(log, subscription)
|
const payload = await buildPayload(log, subscription)
|
||||||
|
|
||||||
// Skip delivery for deleted workflows
|
|
||||||
if (!payload) {
|
|
||||||
await updateDeliveryStatus(deliveryId, 'failed', 'Workflow was deleted')
|
|
||||||
logger.info(`Skipping delivery ${deliveryId} - workflow was deleted`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let result: { success: boolean; status?: number; error?: string }
|
let result: { success: boolean; status?: number; error?: string }
|
||||||
|
|
||||||
switch (notificationType) {
|
switch (notificationType) {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export const YouTubeBlock: BlockConfig<YouTubeResponse> = {
|
|||||||
description: 'Interact with YouTube videos, channels, and playlists',
|
description: 'Interact with YouTube videos, channels, and playlists',
|
||||||
authMode: AuthMode.ApiKey,
|
authMode: AuthMode.ApiKey,
|
||||||
longDescription:
|
longDescription:
|
||||||
'Integrate YouTube into the workflow. Can search for videos, get trending videos, get video details, get video categories, get channel information, get all videos from a channel, get channel playlists, get playlist items, and get video comments.',
|
'Integrate YouTube into the workflow. Can search for videos, get video details, get channel information, get all videos from a channel, get channel playlists, get playlist items, find related videos, and get video comments.',
|
||||||
docsLink: 'https://docs.sim.ai/tools/youtube',
|
docsLink: 'https://docs.sim.ai/tools/youtube',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
bgColor: '#FF0000',
|
bgColor: '#FF0000',
|
||||||
@@ -21,9 +21,7 @@ export const YouTubeBlock: BlockConfig<YouTubeResponse> = {
|
|||||||
type: 'dropdown',
|
type: 'dropdown',
|
||||||
options: [
|
options: [
|
||||||
{ label: 'Search Videos', id: 'youtube_search' },
|
{ label: 'Search Videos', id: 'youtube_search' },
|
||||||
{ label: 'Get Trending Videos', id: 'youtube_trending' },
|
|
||||||
{ label: 'Get Video Details', id: 'youtube_video_details' },
|
{ label: 'Get Video Details', id: 'youtube_video_details' },
|
||||||
{ label: 'Get Video Categories', id: 'youtube_video_categories' },
|
|
||||||
{ label: 'Get Channel Info', id: 'youtube_channel_info' },
|
{ label: 'Get Channel Info', id: 'youtube_channel_info' },
|
||||||
{ label: 'Get Channel Videos', id: 'youtube_channel_videos' },
|
{ label: 'Get Channel Videos', id: 'youtube_channel_videos' },
|
||||||
{ label: 'Get Channel Playlists', id: 'youtube_channel_playlists' },
|
{ label: 'Get Channel Playlists', id: 'youtube_channel_playlists' },
|
||||||
@@ -51,13 +49,6 @@ export const YouTubeBlock: BlockConfig<YouTubeResponse> = {
|
|||||||
integer: true,
|
integer: true,
|
||||||
condition: { field: 'operation', value: 'youtube_search' },
|
condition: { field: 'operation', value: 'youtube_search' },
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'pageToken',
|
|
||||||
title: 'Page Token',
|
|
||||||
type: 'short-input',
|
|
||||||
placeholder: 'Token for pagination (from nextPageToken)',
|
|
||||||
condition: { field: 'operation', value: 'youtube_search' },
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'channelId',
|
id: 'channelId',
|
||||||
title: 'Filter by Channel ID',
|
title: 'Filter by Channel ID',
|
||||||
@@ -65,19 +56,6 @@ export const YouTubeBlock: BlockConfig<YouTubeResponse> = {
|
|||||||
placeholder: 'Filter results to a specific channel',
|
placeholder: 'Filter results to a specific channel',
|
||||||
condition: { field: 'operation', value: 'youtube_search' },
|
condition: { field: 'operation', value: 'youtube_search' },
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'eventType',
|
|
||||||
title: 'Live Stream Filter',
|
|
||||||
type: 'dropdown',
|
|
||||||
options: [
|
|
||||||
{ label: 'All Videos', id: '' },
|
|
||||||
{ label: 'Currently Live', id: 'live' },
|
|
||||||
{ label: 'Upcoming Streams', id: 'upcoming' },
|
|
||||||
{ label: 'Past Streams', id: 'completed' },
|
|
||||||
],
|
|
||||||
value: () => '',
|
|
||||||
condition: { field: 'operation', value: 'youtube_search' },
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'publishedAfter',
|
id: 'publishedAfter',
|
||||||
title: 'Published After',
|
title: 'Published After',
|
||||||
@@ -153,7 +131,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
|||||||
id: 'videoCategoryId',
|
id: 'videoCategoryId',
|
||||||
title: 'Category ID',
|
title: 'Category ID',
|
||||||
type: 'short-input',
|
type: 'short-input',
|
||||||
placeholder: 'Use Get Video Categories to find IDs',
|
placeholder: '10 for Music, 20 for Gaming',
|
||||||
condition: { field: 'operation', value: 'youtube_search' },
|
condition: { field: 'operation', value: 'youtube_search' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -185,10 +163,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
|||||||
title: 'Region Code',
|
title: 'Region Code',
|
||||||
type: 'short-input',
|
type: 'short-input',
|
||||||
placeholder: 'US, GB, JP',
|
placeholder: 'US, GB, JP',
|
||||||
condition: {
|
condition: { field: 'operation', value: 'youtube_search' },
|
||||||
field: 'operation',
|
|
||||||
value: ['youtube_search', 'youtube_trending', 'youtube_video_categories'],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'relevanceLanguage',
|
id: 'relevanceLanguage',
|
||||||
@@ -209,31 +184,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
|||||||
value: () => 'moderate',
|
value: () => 'moderate',
|
||||||
condition: { field: 'operation', value: 'youtube_search' },
|
condition: { field: 'operation', value: 'youtube_search' },
|
||||||
},
|
},
|
||||||
// Get Trending Videos operation inputs
|
|
||||||
{
|
|
||||||
id: 'maxResults',
|
|
||||||
title: 'Max Results',
|
|
||||||
type: 'slider',
|
|
||||||
min: 1,
|
|
||||||
max: 50,
|
|
||||||
step: 1,
|
|
||||||
integer: true,
|
|
||||||
condition: { field: 'operation', value: 'youtube_trending' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'videoCategoryId',
|
|
||||||
title: 'Category ID',
|
|
||||||
type: 'short-input',
|
|
||||||
placeholder: 'Use Get Video Categories to find IDs',
|
|
||||||
condition: { field: 'operation', value: 'youtube_trending' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'pageToken',
|
|
||||||
title: 'Page Token',
|
|
||||||
type: 'short-input',
|
|
||||||
placeholder: 'Token for pagination (from nextPageToken)',
|
|
||||||
condition: { field: 'operation', value: 'youtube_trending' },
|
|
||||||
},
|
|
||||||
// Get Video Details operation inputs
|
// Get Video Details operation inputs
|
||||||
{
|
{
|
||||||
id: 'videoId',
|
id: 'videoId',
|
||||||
@@ -243,14 +193,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
|||||||
required: true,
|
required: true,
|
||||||
condition: { field: 'operation', value: 'youtube_video_details' },
|
condition: { field: 'operation', value: 'youtube_video_details' },
|
||||||
},
|
},
|
||||||
// Get Video Categories operation inputs
|
|
||||||
{
|
|
||||||
id: 'hl',
|
|
||||||
title: 'Language',
|
|
||||||
type: 'short-input',
|
|
||||||
placeholder: 'en, es, fr (for category names)',
|
|
||||||
condition: { field: 'operation', value: 'youtube_video_categories' },
|
|
||||||
},
|
|
||||||
// Get Channel Info operation inputs
|
// Get Channel Info operation inputs
|
||||||
{
|
{
|
||||||
id: 'channelId',
|
id: 'channelId',
|
||||||
@@ -299,13 +241,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
|||||||
value: () => 'date',
|
value: () => 'date',
|
||||||
condition: { field: 'operation', value: 'youtube_channel_videos' },
|
condition: { field: 'operation', value: 'youtube_channel_videos' },
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'pageToken',
|
|
||||||
title: 'Page Token',
|
|
||||||
type: 'short-input',
|
|
||||||
placeholder: 'Token for pagination (from nextPageToken)',
|
|
||||||
condition: { field: 'operation', value: 'youtube_channel_videos' },
|
|
||||||
},
|
|
||||||
// Get Channel Playlists operation inputs
|
// Get Channel Playlists operation inputs
|
||||||
{
|
{
|
||||||
id: 'channelId',
|
id: 'channelId',
|
||||||
@@ -325,13 +260,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
|||||||
integer: true,
|
integer: true,
|
||||||
condition: { field: 'operation', value: 'youtube_channel_playlists' },
|
condition: { field: 'operation', value: 'youtube_channel_playlists' },
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'pageToken',
|
|
||||||
title: 'Page Token',
|
|
||||||
type: 'short-input',
|
|
||||||
placeholder: 'Token for pagination (from nextPageToken)',
|
|
||||||
condition: { field: 'operation', value: 'youtube_channel_playlists' },
|
|
||||||
},
|
|
||||||
// Get Playlist Items operation inputs
|
// Get Playlist Items operation inputs
|
||||||
{
|
{
|
||||||
id: 'playlistId',
|
id: 'playlistId',
|
||||||
@@ -351,13 +279,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
|||||||
integer: true,
|
integer: true,
|
||||||
condition: { field: 'operation', value: 'youtube_playlist_items' },
|
condition: { field: 'operation', value: 'youtube_playlist_items' },
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'pageToken',
|
|
||||||
title: 'Page Token',
|
|
||||||
type: 'short-input',
|
|
||||||
placeholder: 'Token for pagination (from nextPageToken)',
|
|
||||||
condition: { field: 'operation', value: 'youtube_playlist_items' },
|
|
||||||
},
|
|
||||||
// Get Video Comments operation inputs
|
// Get Video Comments operation inputs
|
||||||
{
|
{
|
||||||
id: 'videoId',
|
id: 'videoId',
|
||||||
@@ -388,13 +309,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
|||||||
value: () => 'relevance',
|
value: () => 'relevance',
|
||||||
condition: { field: 'operation', value: 'youtube_comments' },
|
condition: { field: 'operation', value: 'youtube_comments' },
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'pageToken',
|
|
||||||
title: 'Page Token',
|
|
||||||
type: 'short-input',
|
|
||||||
placeholder: 'Token for pagination (from nextPageToken)',
|
|
||||||
condition: { field: 'operation', value: 'youtube_comments' },
|
|
||||||
},
|
|
||||||
// API Key (common to all operations)
|
// API Key (common to all operations)
|
||||||
{
|
{
|
||||||
id: 'apiKey',
|
id: 'apiKey',
|
||||||
@@ -407,15 +321,13 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
|||||||
],
|
],
|
||||||
tools: {
|
tools: {
|
||||||
access: [
|
access: [
|
||||||
'youtube_channel_info',
|
|
||||||
'youtube_channel_playlists',
|
|
||||||
'youtube_channel_videos',
|
|
||||||
'youtube_comments',
|
|
||||||
'youtube_playlist_items',
|
|
||||||
'youtube_search',
|
'youtube_search',
|
||||||
'youtube_trending',
|
|
||||||
'youtube_video_categories',
|
|
||||||
'youtube_video_details',
|
'youtube_video_details',
|
||||||
|
'youtube_channel_info',
|
||||||
|
'youtube_channel_videos',
|
||||||
|
'youtube_channel_playlists',
|
||||||
|
'youtube_playlist_items',
|
||||||
|
'youtube_comments',
|
||||||
],
|
],
|
||||||
config: {
|
config: {
|
||||||
tool: (params) => {
|
tool: (params) => {
|
||||||
@@ -427,12 +339,8 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
|||||||
switch (params.operation) {
|
switch (params.operation) {
|
||||||
case 'youtube_search':
|
case 'youtube_search':
|
||||||
return 'youtube_search'
|
return 'youtube_search'
|
||||||
case 'youtube_trending':
|
|
||||||
return 'youtube_trending'
|
|
||||||
case 'youtube_video_details':
|
case 'youtube_video_details':
|
||||||
return 'youtube_video_details'
|
return 'youtube_video_details'
|
||||||
case 'youtube_video_categories':
|
|
||||||
return 'youtube_video_categories'
|
|
||||||
case 'youtube_channel_info':
|
case 'youtube_channel_info':
|
||||||
return 'youtube_channel_info'
|
return 'youtube_channel_info'
|
||||||
case 'youtube_channel_videos':
|
case 'youtube_channel_videos':
|
||||||
@@ -455,7 +363,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
|||||||
// Search Videos
|
// Search Videos
|
||||||
query: { type: 'string', description: 'Search query' },
|
query: { type: 'string', description: 'Search query' },
|
||||||
maxResults: { type: 'number', description: 'Maximum number of results' },
|
maxResults: { type: 'number', description: 'Maximum number of results' },
|
||||||
pageToken: { type: 'string', description: 'Page token for pagination' },
|
|
||||||
// Search Filters
|
// Search Filters
|
||||||
publishedAfter: { type: 'string', description: 'Published after date (RFC 3339)' },
|
publishedAfter: { type: 'string', description: 'Published after date (RFC 3339)' },
|
||||||
publishedBefore: { type: 'string', description: 'Published before date (RFC 3339)' },
|
publishedBefore: { type: 'string', description: 'Published before date (RFC 3339)' },
|
||||||
@@ -463,11 +370,9 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
|||||||
videoCategoryId: { type: 'string', description: 'YouTube category ID' },
|
videoCategoryId: { type: 'string', description: 'YouTube category ID' },
|
||||||
videoDefinition: { type: 'string', description: 'Video quality filter' },
|
videoDefinition: { type: 'string', description: 'Video quality filter' },
|
||||||
videoCaption: { type: 'string', description: 'Caption availability filter' },
|
videoCaption: { type: 'string', description: 'Caption availability filter' },
|
||||||
eventType: { type: 'string', description: 'Live stream filter (live/upcoming/completed)' },
|
|
||||||
regionCode: { type: 'string', description: 'Region code (ISO 3166-1)' },
|
regionCode: { type: 'string', description: 'Region code (ISO 3166-1)' },
|
||||||
relevanceLanguage: { type: 'string', description: 'Language code (ISO 639-1)' },
|
relevanceLanguage: { type: 'string', description: 'Language code (ISO 639-1)' },
|
||||||
safeSearch: { type: 'string', description: 'Safe search level' },
|
safeSearch: { type: 'string', description: 'Safe search level' },
|
||||||
hl: { type: 'string', description: 'Language for category names' },
|
|
||||||
// Video Details & Comments
|
// Video Details & Comments
|
||||||
videoId: { type: 'string', description: 'YouTube video ID' },
|
videoId: { type: 'string', description: 'YouTube video ID' },
|
||||||
// Channel Info
|
// Channel Info
|
||||||
@@ -479,7 +384,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
|||||||
order: { type: 'string', description: 'Sort order' },
|
order: { type: 'string', description: 'Sort order' },
|
||||||
},
|
},
|
||||||
outputs: {
|
outputs: {
|
||||||
// Search Videos, Trending, Playlist Items, Captions, Categories
|
// Search Videos & Playlist Items
|
||||||
items: { type: 'json', description: 'List of items returned' },
|
items: { type: 'json', description: 'List of items returned' },
|
||||||
totalResults: { type: 'number', description: 'Total number of results' },
|
totalResults: { type: 'number', description: 'Total number of results' },
|
||||||
nextPageToken: { type: 'string', description: 'Token for next page' },
|
nextPageToken: { type: 'string', description: 'Token for next page' },
|
||||||
@@ -494,33 +399,11 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
|||||||
viewCount: { type: 'number', description: 'View count' },
|
viewCount: { type: 'number', description: 'View count' },
|
||||||
likeCount: { type: 'number', description: 'Like count' },
|
likeCount: { type: 'number', description: 'Like count' },
|
||||||
commentCount: { type: 'number', description: 'Comment count' },
|
commentCount: { type: 'number', description: 'Comment count' },
|
||||||
favoriteCount: { type: 'number', description: 'Favorite count' },
|
|
||||||
thumbnail: { type: 'string', description: 'Thumbnail URL' },
|
thumbnail: { type: 'string', description: 'Thumbnail URL' },
|
||||||
tags: { type: 'json', description: 'Video tags' },
|
tags: { type: 'json', description: 'Video tags' },
|
||||||
categoryId: { type: 'string', description: 'Video category ID' },
|
|
||||||
definition: { type: 'string', description: 'Video definition (hd/sd)' },
|
|
||||||
caption: { type: 'string', description: 'Has captions (true/false)' },
|
|
||||||
licensedContent: { type: 'boolean', description: 'Is licensed content' },
|
|
||||||
privacyStatus: { type: 'string', description: 'Privacy status' },
|
|
||||||
liveBroadcastContent: { type: 'string', description: 'Live broadcast status' },
|
|
||||||
defaultLanguage: { type: 'string', description: 'Default language' },
|
|
||||||
defaultAudioLanguage: { type: 'string', description: 'Default audio language' },
|
|
||||||
// Live Streaming Details
|
|
||||||
isLiveContent: { type: 'boolean', description: 'Whether video is/was a live stream' },
|
|
||||||
scheduledStartTime: { type: 'string', description: 'Scheduled start time for live streams' },
|
|
||||||
actualStartTime: { type: 'string', description: 'Actual start time of live stream' },
|
|
||||||
actualEndTime: { type: 'string', description: 'End time of live stream' },
|
|
||||||
concurrentViewers: { type: 'number', description: 'Current viewers (live only)' },
|
|
||||||
activeLiveChatId: { type: 'string', description: 'Live chat ID' },
|
|
||||||
// Channel Info
|
// Channel Info
|
||||||
subscriberCount: { type: 'number', description: 'Subscriber count' },
|
subscriberCount: { type: 'number', description: 'Subscriber count' },
|
||||||
videoCount: { type: 'number', description: 'Total video count' },
|
videoCount: { type: 'number', description: 'Total video count' },
|
||||||
customUrl: { type: 'string', description: 'Channel custom URL' },
|
customUrl: { type: 'string', description: 'Channel custom URL' },
|
||||||
country: { type: 'string', description: 'Channel country' },
|
|
||||||
uploadsPlaylistId: { type: 'string', description: 'Uploads playlist ID' },
|
|
||||||
bannerImageUrl: { type: 'string', description: 'Channel banner URL' },
|
|
||||||
hiddenSubscriberCount: { type: 'boolean', description: 'Is subscriber count hidden' },
|
|
||||||
// Video Categories
|
|
||||||
assignable: { type: 'boolean', description: 'Whether category can be assigned' },
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
46
apps/sim/ee/LICENSE
Normal file
46
apps/sim/ee/LICENSE
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
The Sim Enterprise License (the "Enterprise License")
|
||||||
|
Copyright (c) 2026-present Sim Studio, Inc.
|
||||||
|
|
||||||
|
With regard to the Sim Software:
|
||||||
|
|
||||||
|
This software and associated documentation files (the "Software") may only be
|
||||||
|
used in production, if you (and any entity that you represent) have agreed to,
|
||||||
|
and are in compliance with, the Sim Terms of Service available at
|
||||||
|
https://sim.ai/terms (or other agreement governing the use of the Software,
|
||||||
|
as mutually agreed by you and Sim Studio, Inc. ("Sim")), and otherwise
|
||||||
|
have a valid Sim Enterprise subscription ("Enterprise Subscription")
|
||||||
|
for the correct number of seats as defined in your agreement.
|
||||||
|
|
||||||
|
Subject to the foregoing sentence, you are free to modify this Software and
|
||||||
|
publish patches to the Software. You agree that Sim and/or its licensors
|
||||||
|
(as applicable) retain all right, title and interest in and to all such
|
||||||
|
modifications and/or patches, and all such modifications and/or patches may
|
||||||
|
only be used, copied, modified, displayed, distributed, or otherwise exploited
|
||||||
|
with a valid Enterprise Subscription.
|
||||||
|
|
||||||
|
Notwithstanding the foregoing, you may copy and modify the Software for
|
||||||
|
development and testing purposes, without requiring a subscription. You agree
|
||||||
|
that Sim and/or its licensors (as applicable) retain all right, title
|
||||||
|
and interest in and to all such modifications.
|
||||||
|
|
||||||
|
You are not granted any other rights beyond what is expressly stated herein.
|
||||||
|
Subject to the foregoing, it is forbidden to copy, merge, publish, distribute,
|
||||||
|
sublicense, and/or sell the Software.
|
||||||
|
|
||||||
|
This Enterprise License applies only to the part of this Software that is not
|
||||||
|
distributed under the Apache License 2.0. Any part of this Software distributed
|
||||||
|
under the Apache License 2.0 is copyrighted under that license. The full text
|
||||||
|
of this Enterprise License shall be included in all copies or substantial
|
||||||
|
portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
|
For all third party components incorporated into the Sim Software, those
|
||||||
|
components are licensed under the original license provided by the owner of the
|
||||||
|
applicable component.
|
||||||
69
apps/sim/ee/README.md
Normal file
69
apps/sim/ee/README.md
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
# Sim Enterprise Edition
|
||||||
|
|
||||||
|
This directory contains enterprise features that require a valid Sim Enterprise license for production use.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **SSO**: SAML and OIDC single sign-on authentication
|
||||||
|
- **Access Control**: Permission groups and role-based access control
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
ee/
|
||||||
|
├── LICENSE
|
||||||
|
├── README.md
|
||||||
|
├── index.ts # Main barrel export
|
||||||
|
├── sso/
|
||||||
|
│ ├── index.ts
|
||||||
|
│ ├── components/ # SSO settings UI
|
||||||
|
│ ├── hooks/ # React Query hooks
|
||||||
|
│ └── lib/ # Utilities and constants
|
||||||
|
└── access-control/
|
||||||
|
├── index.ts
|
||||||
|
├── components/ # Access control settings UI
|
||||||
|
├── hooks/ # React Query hooks
|
||||||
|
└── lib/ # Types and utilities
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** API routes remain in `app/api/` as required by Next.js routing conventions:
|
||||||
|
- SSO API: `app/api/auth/sso/`
|
||||||
|
- Permission Groups API: `app/api/permission-groups/`
|
||||||
|
|
||||||
|
## Licensing
|
||||||
|
|
||||||
|
Code in this directory is **NOT** covered by the Apache 2.0 license. See [LICENSE](./LICENSE) for the Sim Enterprise License terms.
|
||||||
|
|
||||||
|
The rest of the Sim codebase outside this directory is licensed under Apache 2.0.
|
||||||
|
|
||||||
|
## For Open Source Users
|
||||||
|
|
||||||
|
You may delete this directory to use Sim under the Apache 2.0 license only. The application will continue to function without enterprise features.
|
||||||
|
|
||||||
|
## Development & Testing
|
||||||
|
|
||||||
|
You may copy and modify this software for development and testing purposes without requiring an Enterprise subscription. Production use requires a valid license.
|
||||||
|
|
||||||
|
## Enabling Enterprise Features
|
||||||
|
|
||||||
|
Enterprise features are controlled by environment variables and subscription status:
|
||||||
|
|
||||||
|
- `NEXT_PUBLIC_SSO_ENABLED` - Enable SSO for self-hosted instances
|
||||||
|
- `NEXT_PUBLIC_ACCESS_CONTROL_ENABLED` - Enable access control for self-hosted instances
|
||||||
|
|
||||||
|
On the hosted platform (sim.ai), these features are automatically available with an Enterprise subscription.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Import enterprise components
|
||||||
|
import { SSO, AccessControl } from '@/ee'
|
||||||
|
|
||||||
|
// Or import specific features
|
||||||
|
import { SSO, useSSOProviders } from '@/ee/sso'
|
||||||
|
import { AccessControl, usePermissionGroups } from '@/ee/access-control'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contact
|
||||||
|
|
||||||
|
For Enterprise licensing inquiries, contact [sales@sim.ai](mailto:sales@sim.ai).
|
||||||
@@ -25,11 +25,9 @@ import {
|
|||||||
import { Input as BaseInput, Skeleton } from '@/components/ui'
|
import { Input as BaseInput, Skeleton } from '@/components/ui'
|
||||||
import { useSession } from '@/lib/auth/auth-client'
|
import { useSession } from '@/lib/auth/auth-client'
|
||||||
import { getSubscriptionStatus } from '@/lib/billing/client'
|
import { getSubscriptionStatus } from '@/lib/billing/client'
|
||||||
import type { PermissionGroupConfig } from '@/lib/permission-groups/types'
|
|
||||||
import { getUserColor } from '@/lib/workspaces/colors'
|
import { getUserColor } from '@/lib/workspaces/colors'
|
||||||
import { getUserRole } from '@/lib/workspaces/organization'
|
import { getUserRole } from '@/lib/workspaces/organization'
|
||||||
import { getAllBlocks } from '@/blocks'
|
import { getAllBlocks } from '@/blocks'
|
||||||
import { useOrganization, useOrganizations } from '@/hooks/queries/organization'
|
|
||||||
import {
|
import {
|
||||||
type PermissionGroup,
|
type PermissionGroup,
|
||||||
useBulkAddPermissionGroupMembers,
|
useBulkAddPermissionGroupMembers,
|
||||||
@@ -39,7 +37,9 @@ import {
|
|||||||
usePermissionGroups,
|
usePermissionGroups,
|
||||||
useRemovePermissionGroupMember,
|
useRemovePermissionGroupMember,
|
||||||
useUpdatePermissionGroup,
|
useUpdatePermissionGroup,
|
||||||
} from '@/hooks/queries/permission-groups'
|
} from '@/ee/access-control/hooks/permission-groups'
|
||||||
|
import type { PermissionGroupConfig } from '@/ee/access-control/lib/types'
|
||||||
|
import { useOrganization, useOrganizations } from '@/hooks/queries/organization'
|
||||||
import { useSubscriptionData } from '@/hooks/queries/subscription'
|
import { useSubscriptionData } from '@/hooks/queries/subscription'
|
||||||
import { PROVIDER_DEFINITIONS } from '@/providers/models'
|
import { PROVIDER_DEFINITIONS } from '@/providers/models'
|
||||||
import { getAllProviderIds } from '@/providers/utils'
|
import { getAllProviderIds } from '@/providers/utils'
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
import type { PermissionGroupConfig } from '@/lib/permission-groups/types'
|
import type { PermissionGroupConfig } from '@/ee/access-control/lib/types'
|
||||||
import { fetchJson } from '@/hooks/selectors/helpers'
|
import { fetchJson } from '@/hooks/selectors/helpers'
|
||||||
|
|
||||||
export interface PermissionGroup {
|
export interface PermissionGroup {
|
||||||
26
apps/sim/ee/access-control/index.ts
Normal file
26
apps/sim/ee/access-control/index.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
export { AccessControl } from './components/access-control'
|
||||||
|
export {
|
||||||
|
type BulkAddMembersData,
|
||||||
|
type CreatePermissionGroupData,
|
||||||
|
type DeletePermissionGroupParams,
|
||||||
|
type PermissionGroup,
|
||||||
|
type PermissionGroupMember,
|
||||||
|
permissionGroupKeys,
|
||||||
|
type UpdatePermissionGroupData,
|
||||||
|
type UserPermissionConfig,
|
||||||
|
useAddPermissionGroupMember,
|
||||||
|
useBulkAddPermissionGroupMembers,
|
||||||
|
useCreatePermissionGroup,
|
||||||
|
useDeletePermissionGroup,
|
||||||
|
usePermissionGroup,
|
||||||
|
usePermissionGroupMembers,
|
||||||
|
usePermissionGroups,
|
||||||
|
useRemovePermissionGroupMember,
|
||||||
|
useUpdatePermissionGroup,
|
||||||
|
useUserPermissionConfig,
|
||||||
|
} from './hooks/permission-groups'
|
||||||
|
export type { PermissionGroupConfig } from './lib/types'
|
||||||
|
export {
|
||||||
|
DEFAULT_PERMISSION_GROUP_CONFIG,
|
||||||
|
parsePermissionGroupConfig,
|
||||||
|
} from './lib/types'
|
||||||
34
apps/sim/ee/index.ts
Normal file
34
apps/sim/ee/index.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* Sim Enterprise Edition
|
||||||
|
*
|
||||||
|
* This module contains enterprise features that require a valid
|
||||||
|
* Sim Enterprise license for production use.
|
||||||
|
*
|
||||||
|
* See LICENSE in this directory for terms.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type { PermissionGroupConfig } from './access-control'
|
||||||
|
// Access Control (Permission Groups)
|
||||||
|
export {
|
||||||
|
AccessControl,
|
||||||
|
type BulkAddMembersData,
|
||||||
|
type CreatePermissionGroupData,
|
||||||
|
type DeletePermissionGroupParams,
|
||||||
|
type PermissionGroup,
|
||||||
|
type PermissionGroupMember,
|
||||||
|
permissionGroupKeys,
|
||||||
|
type UpdatePermissionGroupData,
|
||||||
|
type UserPermissionConfig,
|
||||||
|
useAddPermissionGroupMember,
|
||||||
|
useBulkAddPermissionGroupMembers,
|
||||||
|
useCreatePermissionGroup,
|
||||||
|
useDeletePermissionGroup,
|
||||||
|
usePermissionGroup,
|
||||||
|
usePermissionGroupMembers,
|
||||||
|
usePermissionGroups,
|
||||||
|
useRemovePermissionGroupMember,
|
||||||
|
useUpdatePermissionGroup,
|
||||||
|
useUserPermissionConfig,
|
||||||
|
} from './access-control'
|
||||||
|
// SSO (Single Sign-On)
|
||||||
|
export { SSO, ssoKeys, useConfigureSSO, useDeleteSSO, useSSOProviders } from './sso'
|
||||||
@@ -11,55 +11,13 @@ import { isBillingEnabled } from '@/lib/core/config/feature-flags'
|
|||||||
import { cn } from '@/lib/core/utils/cn'
|
import { cn } from '@/lib/core/utils/cn'
|
||||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||||
import { getUserRole } from '@/lib/workspaces/organization/utils'
|
import { getUserRole } from '@/lib/workspaces/organization/utils'
|
||||||
|
import { useConfigureSSO, useSSOProviders } from '@/ee/sso/hooks/sso'
|
||||||
|
import { SSO_TRUSTED_PROVIDERS } from '@/ee/sso/lib/constants'
|
||||||
import { useOrganizations } from '@/hooks/queries/organization'
|
import { useOrganizations } from '@/hooks/queries/organization'
|
||||||
import { useConfigureSSO, useSSOProviders } from '@/hooks/queries/sso'
|
|
||||||
import { useSubscriptionData } from '@/hooks/queries/subscription'
|
import { useSubscriptionData } from '@/hooks/queries/subscription'
|
||||||
|
|
||||||
const logger = createLogger('SSO')
|
const logger = createLogger('SSO')
|
||||||
|
|
||||||
const TRUSTED_SSO_PROVIDERS = [
|
|
||||||
'okta',
|
|
||||||
'okta-saml',
|
|
||||||
'okta-prod',
|
|
||||||
'okta-dev',
|
|
||||||
'okta-staging',
|
|
||||||
'okta-test',
|
|
||||||
'azure-ad',
|
|
||||||
'azure-active-directory',
|
|
||||||
'azure-corp',
|
|
||||||
'azure-enterprise',
|
|
||||||
'adfs',
|
|
||||||
'adfs-company',
|
|
||||||
'adfs-corp',
|
|
||||||
'adfs-enterprise',
|
|
||||||
'auth0',
|
|
||||||
'auth0-prod',
|
|
||||||
'auth0-dev',
|
|
||||||
'auth0-staging',
|
|
||||||
'onelogin',
|
|
||||||
'onelogin-prod',
|
|
||||||
'onelogin-corp',
|
|
||||||
'jumpcloud',
|
|
||||||
'jumpcloud-prod',
|
|
||||||
'jumpcloud-corp',
|
|
||||||
'ping-identity',
|
|
||||||
'ping-federate',
|
|
||||||
'pingone',
|
|
||||||
'shibboleth',
|
|
||||||
'shibboleth-idp',
|
|
||||||
'google-workspace',
|
|
||||||
'google-sso',
|
|
||||||
'saml',
|
|
||||||
'saml2',
|
|
||||||
'saml-sso',
|
|
||||||
'oidc',
|
|
||||||
'oidc-sso',
|
|
||||||
'openid-connect',
|
|
||||||
'custom-sso',
|
|
||||||
'enterprise-sso',
|
|
||||||
'company-sso',
|
|
||||||
]
|
|
||||||
|
|
||||||
interface SSOProvider {
|
interface SSOProvider {
|
||||||
id: string
|
id: string
|
||||||
providerId: string
|
providerId: string
|
||||||
@@ -565,7 +523,7 @@ export function SSO() {
|
|||||||
<Combobox
|
<Combobox
|
||||||
value={formData.providerId}
|
value={formData.providerId}
|
||||||
onChange={(value: string) => handleInputChange('providerId', value)}
|
onChange={(value: string) => handleInputChange('providerId', value)}
|
||||||
options={TRUSTED_SSO_PROVIDERS.map((id) => ({
|
options={SSO_TRUSTED_PROVIDERS.map((id) => ({
|
||||||
label: id,
|
label: id,
|
||||||
value: id,
|
value: id,
|
||||||
}))}
|
}))}
|
||||||
8
apps/sim/ee/sso/index.ts
Normal file
8
apps/sim/ee/sso/index.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export { SSO } from './components/sso'
|
||||||
|
export {
|
||||||
|
ssoKeys,
|
||||||
|
useConfigureSSO,
|
||||||
|
useDeleteSSO,
|
||||||
|
useSSOProviders,
|
||||||
|
} from './hooks/sso'
|
||||||
|
export * from './lib/constants'
|
||||||
@@ -6,7 +6,6 @@ interface ChildWorkflowErrorOptions {
|
|||||||
childWorkflowName: string
|
childWorkflowName: string
|
||||||
childTraceSpans?: TraceSpan[]
|
childTraceSpans?: TraceSpan[]
|
||||||
executionResult?: ExecutionResult
|
executionResult?: ExecutionResult
|
||||||
childWorkflowSnapshotId?: string
|
|
||||||
cause?: Error
|
cause?: Error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,7 +16,6 @@ export class ChildWorkflowError extends Error {
|
|||||||
readonly childTraceSpans: TraceSpan[]
|
readonly childTraceSpans: TraceSpan[]
|
||||||
readonly childWorkflowName: string
|
readonly childWorkflowName: string
|
||||||
readonly executionResult?: ExecutionResult
|
readonly executionResult?: ExecutionResult
|
||||||
readonly childWorkflowSnapshotId?: string
|
|
||||||
|
|
||||||
constructor(options: ChildWorkflowErrorOptions) {
|
constructor(options: ChildWorkflowErrorOptions) {
|
||||||
super(options.message, { cause: options.cause })
|
super(options.message, { cause: options.cause })
|
||||||
@@ -25,7 +23,6 @@ export class ChildWorkflowError extends Error {
|
|||||||
this.childWorkflowName = options.childWorkflowName
|
this.childWorkflowName = options.childWorkflowName
|
||||||
this.childTraceSpans = options.childTraceSpans ?? []
|
this.childTraceSpans = options.childTraceSpans ?? []
|
||||||
this.executionResult = options.executionResult
|
this.executionResult = options.executionResult
|
||||||
this.childWorkflowSnapshotId = options.childWorkflowSnapshotId
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static isChildWorkflowError(error: unknown): error is ChildWorkflowError {
|
static isChildWorkflowError(error: unknown): error is ChildWorkflowError {
|
||||||
|
|||||||
@@ -237,9 +237,6 @@ export class BlockExecutor {
|
|||||||
if (ChildWorkflowError.isChildWorkflowError(error)) {
|
if (ChildWorkflowError.isChildWorkflowError(error)) {
|
||||||
errorOutput.childTraceSpans = error.childTraceSpans
|
errorOutput.childTraceSpans = error.childTraceSpans
|
||||||
errorOutput.childWorkflowName = error.childWorkflowName
|
errorOutput.childWorkflowName = error.childWorkflowName
|
||||||
if (error.childWorkflowSnapshotId) {
|
|
||||||
errorOutput.childWorkflowSnapshotId = error.childWorkflowSnapshotId
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.state.setBlockOutput(node.id, errorOutput, duration)
|
this.state.setBlockOutput(node.id, errorOutput, duration)
|
||||||
|
|||||||
@@ -198,7 +198,6 @@ describe('WorkflowBlockHandler', () => {
|
|||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
success: true,
|
success: true,
|
||||||
childWorkflowId: 'child-id',
|
|
||||||
childWorkflowName: 'Child Workflow',
|
childWorkflowName: 'Child Workflow',
|
||||||
result: { data: 'test result' },
|
result: { data: 'test result' },
|
||||||
childTraceSpans: [],
|
childTraceSpans: [],
|
||||||
@@ -236,7 +235,6 @@ describe('WorkflowBlockHandler', () => {
|
|||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
success: true,
|
success: true,
|
||||||
childWorkflowId: 'child-id',
|
|
||||||
childWorkflowName: 'Child Workflow',
|
childWorkflowName: 'Child Workflow',
|
||||||
result: { nested: 'data' },
|
result: { nested: 'data' },
|
||||||
childTraceSpans: [],
|
childTraceSpans: [],
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { snapshotService } from '@/lib/logs/execution/snapshot/service'
|
|
||||||
import { buildTraceSpans } from '@/lib/logs/execution/trace-spans/trace-spans'
|
import { buildTraceSpans } from '@/lib/logs/execution/trace-spans/trace-spans'
|
||||||
import type { TraceSpan } from '@/lib/logs/types'
|
import type { TraceSpan } from '@/lib/logs/types'
|
||||||
import type { BlockOutput } from '@/blocks/types'
|
import type { BlockOutput } from '@/blocks/types'
|
||||||
@@ -58,7 +57,6 @@ export class WorkflowBlockHandler implements BlockHandler {
|
|||||||
const workflowMetadata = workflows[workflowId]
|
const workflowMetadata = workflows[workflowId]
|
||||||
let childWorkflowName = workflowMetadata?.name || workflowId
|
let childWorkflowName = workflowMetadata?.name || workflowId
|
||||||
|
|
||||||
let childWorkflowSnapshotId: string | undefined
|
|
||||||
try {
|
try {
|
||||||
const currentDepth = (ctx.workflowId?.split('_sub_').length || 1) - 1
|
const currentDepth = (ctx.workflowId?.split('_sub_').length || 1) - 1
|
||||||
if (currentDepth >= DEFAULTS.MAX_WORKFLOW_DEPTH) {
|
if (currentDepth >= DEFAULTS.MAX_WORKFLOW_DEPTH) {
|
||||||
@@ -109,12 +107,6 @@ export class WorkflowBlockHandler implements BlockHandler {
|
|||||||
childWorkflowInput = inputs.input
|
childWorkflowInput = inputs.input
|
||||||
}
|
}
|
||||||
|
|
||||||
const childSnapshotResult = await snapshotService.createSnapshotWithDeduplication(
|
|
||||||
workflowId,
|
|
||||||
childWorkflow.workflowState
|
|
||||||
)
|
|
||||||
childWorkflowSnapshotId = childSnapshotResult.snapshot.id
|
|
||||||
|
|
||||||
const subExecutor = new Executor({
|
const subExecutor = new Executor({
|
||||||
workflow: childWorkflow.serializedState,
|
workflow: childWorkflow.serializedState,
|
||||||
workflowInput: childWorkflowInput,
|
workflowInput: childWorkflowInput,
|
||||||
@@ -147,8 +139,7 @@ export class WorkflowBlockHandler implements BlockHandler {
|
|||||||
workflowId,
|
workflowId,
|
||||||
childWorkflowName,
|
childWorkflowName,
|
||||||
duration,
|
duration,
|
||||||
childTraceSpans,
|
childTraceSpans
|
||||||
childWorkflowSnapshotId
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return mappedResult
|
return mappedResult
|
||||||
@@ -181,7 +172,6 @@ export class WorkflowBlockHandler implements BlockHandler {
|
|||||||
childWorkflowName,
|
childWorkflowName,
|
||||||
childTraceSpans,
|
childTraceSpans,
|
||||||
executionResult,
|
executionResult,
|
||||||
childWorkflowSnapshotId,
|
|
||||||
cause: error instanceof Error ? error : undefined,
|
cause: error instanceof Error ? error : undefined,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -289,10 +279,6 @@ export class WorkflowBlockHandler implements BlockHandler {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const workflowVariables = (workflowData.variables as Record<string, any>) || {}
|
const workflowVariables = (workflowData.variables as Record<string, any>) || {}
|
||||||
const workflowStateWithVariables = {
|
|
||||||
...workflowState,
|
|
||||||
variables: workflowVariables,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(workflowVariables).length > 0) {
|
if (Object.keys(workflowVariables).length > 0) {
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -304,7 +290,6 @@ export class WorkflowBlockHandler implements BlockHandler {
|
|||||||
name: workflowData.name,
|
name: workflowData.name,
|
||||||
serializedState: serializedWorkflow,
|
serializedState: serializedWorkflow,
|
||||||
variables: workflowVariables,
|
variables: workflowVariables,
|
||||||
workflowState: workflowStateWithVariables,
|
|
||||||
rawBlocks: workflowState.blocks,
|
rawBlocks: workflowState.blocks,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -373,16 +358,11 @@ export class WorkflowBlockHandler implements BlockHandler {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const workflowVariables = (wfData?.variables as Record<string, any>) || {}
|
const workflowVariables = (wfData?.variables as Record<string, any>) || {}
|
||||||
const workflowStateWithVariables = {
|
|
||||||
...deployedState,
|
|
||||||
variables: workflowVariables,
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: wfData?.name || DEFAULTS.WORKFLOW_NAME,
|
name: wfData?.name || DEFAULTS.WORKFLOW_NAME,
|
||||||
serializedState: serializedWorkflow,
|
serializedState: serializedWorkflow,
|
||||||
variables: workflowVariables,
|
variables: workflowVariables,
|
||||||
workflowState: workflowStateWithVariables,
|
|
||||||
rawBlocks: deployedState.blocks,
|
rawBlocks: deployedState.blocks,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -524,8 +504,7 @@ export class WorkflowBlockHandler implements BlockHandler {
|
|||||||
childWorkflowId: string,
|
childWorkflowId: string,
|
||||||
childWorkflowName: string,
|
childWorkflowName: string,
|
||||||
duration: number,
|
duration: number,
|
||||||
childTraceSpans?: WorkflowTraceSpan[],
|
childTraceSpans?: WorkflowTraceSpan[]
|
||||||
childWorkflowSnapshotId?: string
|
|
||||||
): BlockOutput {
|
): BlockOutput {
|
||||||
const success = childResult.success !== false
|
const success = childResult.success !== false
|
||||||
const result = childResult.output || {}
|
const result = childResult.output || {}
|
||||||
@@ -536,15 +515,12 @@ export class WorkflowBlockHandler implements BlockHandler {
|
|||||||
message: `"${childWorkflowName}" failed: ${childResult.error || 'Child workflow execution failed'}`,
|
message: `"${childWorkflowName}" failed: ${childResult.error || 'Child workflow execution failed'}`,
|
||||||
childWorkflowName,
|
childWorkflowName,
|
||||||
childTraceSpans: childTraceSpans || [],
|
childTraceSpans: childTraceSpans || [],
|
||||||
childWorkflowSnapshotId,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
childWorkflowName,
|
childWorkflowName,
|
||||||
childWorkflowId,
|
|
||||||
...(childWorkflowSnapshotId ? { childWorkflowSnapshotId } : {}),
|
|
||||||
result,
|
result,
|
||||||
childTraceSpans: childTraceSpans || [],
|
childTraceSpans: childTraceSpans || [],
|
||||||
} as Record<string, any>
|
} as Record<string, any>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { TraceSpan } from '@/lib/logs/types'
|
import type { TraceSpan } from '@/lib/logs/types'
|
||||||
import type { PermissionGroupConfig } from '@/lib/permission-groups/types'
|
|
||||||
import type { BlockOutput } from '@/blocks/types'
|
import type { BlockOutput } from '@/blocks/types'
|
||||||
|
import type { PermissionGroupConfig } from '@/ee/access-control/lib/types'
|
||||||
import type { RunFromBlockContext } from '@/executor/utils/run-from-block'
|
import type { RunFromBlockContext } from '@/executor/utils/run-from-block'
|
||||||
import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types'
|
import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types'
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { isAccessControlEnabled, isHosted } from '@/lib/core/config/feature-flag
|
|||||||
import {
|
import {
|
||||||
type PermissionGroupConfig,
|
type PermissionGroupConfig,
|
||||||
parsePermissionGroupConfig,
|
parsePermissionGroupConfig,
|
||||||
} from '@/lib/permission-groups/types'
|
} from '@/ee/access-control/lib/types'
|
||||||
import type { ExecutionContext } from '@/executor/types'
|
import type { ExecutionContext } from '@/executor/types'
|
||||||
import { getProviderFromModel } from '@/providers/utils'
|
import { getProviderFromModel } from '@/providers/utils'
|
||||||
|
|
||||||
|
|||||||
@@ -210,7 +210,6 @@ export interface ExecutionSnapshotData {
|
|||||||
executionId: string
|
executionId: string
|
||||||
workflowId: string
|
workflowId: string
|
||||||
workflowState: Record<string, unknown>
|
workflowState: Record<string, unknown>
|
||||||
childWorkflowSnapshots?: Record<string, Record<string, unknown>>
|
|
||||||
executionMetadata: {
|
executionMetadata: {
|
||||||
trigger: string
|
trigger: string
|
||||||
startedAt: string
|
startedAt: string
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { getEnv, isTruthy } from '@/lib/core/config/env'
|
import { getEnv, isTruthy } from '@/lib/core/config/env'
|
||||||
import { isAccessControlEnabled, isHosted } from '@/lib/core/config/feature-flags'
|
import { isAccessControlEnabled, isHosted } from '@/lib/core/config/feature-flags'
|
||||||
|
import { useUserPermissionConfig } from '@/ee/access-control/hooks/permission-groups'
|
||||||
import {
|
import {
|
||||||
DEFAULT_PERMISSION_GROUP_CONFIG,
|
DEFAULT_PERMISSION_GROUP_CONFIG,
|
||||||
type PermissionGroupConfig,
|
type PermissionGroupConfig,
|
||||||
} from '@/lib/permission-groups/types'
|
} from '@/ee/access-control/lib/types'
|
||||||
import { useOrganizations } from '@/hooks/queries/organization'
|
import { useOrganizations } from '@/hooks/queries/organization'
|
||||||
import { useUserPermissionConfig } from '@/hooks/queries/permission-groups'
|
|
||||||
|
|
||||||
export interface PermissionConfigResult {
|
export interface PermissionConfigResult {
|
||||||
config: PermissionGroupConfig
|
config: PermissionGroupConfig
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ import { sendEmail } from '@/lib/messaging/email/mailer'
|
|||||||
import { getFromEmailAddress, getPersonalEmailFrom } from '@/lib/messaging/email/utils'
|
import { getFromEmailAddress, getPersonalEmailFrom } from '@/lib/messaging/email/utils'
|
||||||
import { quickValidateEmail } from '@/lib/messaging/email/validation'
|
import { quickValidateEmail } from '@/lib/messaging/email/validation'
|
||||||
import { syncAllWebhooksForCredentialSet } from '@/lib/webhooks/utils.server'
|
import { syncAllWebhooksForCredentialSet } from '@/lib/webhooks/utils.server'
|
||||||
|
import { SSO_TRUSTED_PROVIDERS } from '@/ee/sso/lib/constants'
|
||||||
import { createAnonymousSession, ensureAnonymousUserExists } from './anonymous'
|
import { createAnonymousSession, ensureAnonymousUserExists } from './anonymous'
|
||||||
import { SSO_TRUSTED_PROVIDERS } from './sso/constants'
|
|
||||||
|
|
||||||
const logger = createLogger('Auth')
|
const logger = createLogger('Auth')
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { eq } from 'drizzle-orm'
|
import { eq } from 'drizzle-orm'
|
||||||
import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool'
|
import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool'
|
||||||
import { validateSelectorIds } from '@/lib/copilot/validation/selector-validator'
|
import { validateSelectorIds } from '@/lib/copilot/validation/selector-validator'
|
||||||
import type { PermissionGroupConfig } from '@/lib/permission-groups/types'
|
|
||||||
import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs'
|
import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs'
|
||||||
import { extractAndPersistCustomTools } from '@/lib/workflows/persistence/custom-tools-persistence'
|
import { extractAndPersistCustomTools } from '@/lib/workflows/persistence/custom-tools-persistence'
|
||||||
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
|
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
|
||||||
@@ -15,6 +14,7 @@ import { buildCanonicalIndex, isCanonicalPair } from '@/lib/workflows/subblocks/
|
|||||||
import { TriggerUtils } from '@/lib/workflows/triggers/triggers'
|
import { TriggerUtils } from '@/lib/workflows/triggers/triggers'
|
||||||
import { getAllBlocks, getBlock } from '@/blocks/registry'
|
import { getAllBlocks, getBlock } from '@/blocks/registry'
|
||||||
import type { BlockConfig, SubBlockConfig } from '@/blocks/types'
|
import type { BlockConfig, SubBlockConfig } from '@/blocks/types'
|
||||||
|
import type { PermissionGroupConfig } from '@/ee/access-control/lib/types'
|
||||||
import { EDGE, normalizeName, RESERVED_BLOCK_NAMES } from '@/executor/constants'
|
import { EDGE, normalizeName, RESERVED_BLOCK_NAMES } from '@/executor/constants'
|
||||||
import { getUserPermissionConfig } from '@/executor/utils/permission-check'
|
import { getUserPermissionConfig } from '@/executor/utils/permission-check'
|
||||||
import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/workflow/utils'
|
import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/workflow/utils'
|
||||||
|
|||||||
@@ -50,8 +50,6 @@ function prepareLogData(
|
|||||||
|
|
||||||
export async function emitWorkflowExecutionCompleted(log: WorkflowExecutionLog): Promise<void> {
|
export async function emitWorkflowExecutionCompleted(log: WorkflowExecutionLog): Promise<void> {
|
||||||
try {
|
try {
|
||||||
if (!log.workflowId) return
|
|
||||||
|
|
||||||
const workflowData = await db
|
const workflowData = await db
|
||||||
.select({ workspaceId: workflow.workspaceId })
|
.select({ workspaceId: workflow.workspaceId })
|
||||||
.from(workflow)
|
.from(workflow)
|
||||||
|
|||||||
@@ -293,10 +293,7 @@ export class ExecutionLogger implements IExecutionLoggerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Skip workflow lookup if workflow was deleted
|
const [wf] = await db.select().from(workflow).where(eq(workflow.id, updatedLog.workflowId))
|
||||||
const wf = updatedLog.workflowId
|
|
||||||
? (await db.select().from(workflow).where(eq(workflow.id, updatedLog.workflowId)))[0]
|
|
||||||
: undefined
|
|
||||||
if (wf) {
|
if (wf) {
|
||||||
const [usr] = await db
|
const [usr] = await db
|
||||||
.select({ id: userTable.id, email: userTable.email, name: userTable.name })
|
.select({ id: userTable.id, email: userTable.email, name: userTable.name })
|
||||||
@@ -464,7 +461,7 @@ export class ExecutionLogger implements IExecutionLoggerService {
|
|||||||
* Maintains same logic as original execution logger for billing consistency
|
* Maintains same logic as original execution logger for billing consistency
|
||||||
*/
|
*/
|
||||||
private async updateUserStats(
|
private async updateUserStats(
|
||||||
workflowId: string | null,
|
workflowId: string,
|
||||||
costSummary: {
|
costSummary: {
|
||||||
totalCost: number
|
totalCost: number
|
||||||
totalInputCost: number
|
totalInputCost: number
|
||||||
@@ -497,11 +494,6 @@ export class ExecutionLogger implements IExecutionLoggerService {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!workflowId) {
|
|
||||||
logger.debug('Workflow was deleted, skipping user stats update')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get the workflow record to get the userId
|
// Get the workflow record to get the userId
|
||||||
const [workflowRecord] = await db
|
const [workflowRecord] = await db
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { createHash } from 'crypto'
|
import { createHash } from 'crypto'
|
||||||
import { db } from '@sim/db'
|
import { db } from '@sim/db'
|
||||||
import { workflowExecutionLogs, workflowExecutionSnapshots } from '@sim/db/schema'
|
import { workflowExecutionSnapshots } from '@sim/db/schema'
|
||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { and, eq, lt, notExists } from 'drizzle-orm'
|
import { and, eq, lt } from 'drizzle-orm'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import type {
|
import type {
|
||||||
SnapshotService as ISnapshotService,
|
SnapshotService as ISnapshotService,
|
||||||
@@ -121,17 +121,7 @@ export class SnapshotService implements ISnapshotService {
|
|||||||
|
|
||||||
const deletedSnapshots = await db
|
const deletedSnapshots = await db
|
||||||
.delete(workflowExecutionSnapshots)
|
.delete(workflowExecutionSnapshots)
|
||||||
.where(
|
.where(lt(workflowExecutionSnapshots.createdAt, cutoffDate))
|
||||||
and(
|
|
||||||
lt(workflowExecutionSnapshots.createdAt, cutoffDate),
|
|
||||||
notExists(
|
|
||||||
db
|
|
||||||
.select({ id: workflowExecutionLogs.id })
|
|
||||||
.from(workflowExecutionLogs)
|
|
||||||
.where(eq(workflowExecutionLogs.stateSnapshotId, workflowExecutionSnapshots.id))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.returning({ id: workflowExecutionSnapshots.id })
|
.returning({ id: workflowExecutionSnapshots.id })
|
||||||
|
|
||||||
const deletedCount = deletedSnapshots.length
|
const deletedCount = deletedSnapshots.length
|
||||||
|
|||||||
@@ -112,26 +112,6 @@ export function buildTraceSpans(result: ExecutionResult): {
|
|||||||
const duration = log.durationMs || 0
|
const duration = log.durationMs || 0
|
||||||
|
|
||||||
let output = log.output || {}
|
let output = log.output || {}
|
||||||
let childWorkflowSnapshotId: string | undefined
|
|
||||||
let childWorkflowId: string | undefined
|
|
||||||
|
|
||||||
if (output && typeof output === 'object') {
|
|
||||||
const outputRecord = output as Record<string, unknown>
|
|
||||||
childWorkflowSnapshotId =
|
|
||||||
typeof outputRecord.childWorkflowSnapshotId === 'string'
|
|
||||||
? outputRecord.childWorkflowSnapshotId
|
|
||||||
: undefined
|
|
||||||
childWorkflowId =
|
|
||||||
typeof outputRecord.childWorkflowId === 'string' ? outputRecord.childWorkflowId : undefined
|
|
||||||
if (childWorkflowSnapshotId || childWorkflowId) {
|
|
||||||
const {
|
|
||||||
childWorkflowSnapshotId: _childSnapshotId,
|
|
||||||
childWorkflowId: _childWorkflowId,
|
|
||||||
...outputRest
|
|
||||||
} = outputRecord
|
|
||||||
output = outputRest
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (log.error) {
|
if (log.error) {
|
||||||
output = {
|
output = {
|
||||||
@@ -154,8 +134,6 @@ export function buildTraceSpans(result: ExecutionResult): {
|
|||||||
blockId: log.blockId,
|
blockId: log.blockId,
|
||||||
input: log.input || {},
|
input: log.input || {},
|
||||||
output: output,
|
output: output,
|
||||||
...(childWorkflowSnapshotId ? { childWorkflowSnapshotId } : {}),
|
|
||||||
...(childWorkflowId ? { childWorkflowId } : {}),
|
|
||||||
...(log.loopId && { loopId: log.loopId }),
|
...(log.loopId && { loopId: log.loopId }),
|
||||||
...(log.parallelId && { parallelId: log.parallelId }),
|
...(log.parallelId && { parallelId: log.parallelId }),
|
||||||
...(log.iterationIndex !== undefined && { iterationIndex: log.iterationIndex }),
|
...(log.iterationIndex !== undefined && { iterationIndex: log.iterationIndex }),
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export interface ExecutionStatus {
|
|||||||
|
|
||||||
export interface WorkflowExecutionSnapshot {
|
export interface WorkflowExecutionSnapshot {
|
||||||
id: string
|
id: string
|
||||||
workflowId: string | null
|
workflowId: string
|
||||||
stateHash: string
|
stateHash: string
|
||||||
stateData: WorkflowState
|
stateData: WorkflowState
|
||||||
createdAt: string
|
createdAt: string
|
||||||
@@ -80,7 +80,7 @@ export type WorkflowExecutionSnapshotSelect = WorkflowExecutionSnapshot
|
|||||||
|
|
||||||
export interface WorkflowExecutionLog {
|
export interface WorkflowExecutionLog {
|
||||||
id: string
|
id: string
|
||||||
workflowId: string | null
|
workflowId: string
|
||||||
executionId: string
|
executionId: string
|
||||||
stateSnapshotId: string
|
stateSnapshotId: string
|
||||||
level: 'info' | 'error'
|
level: 'info' | 'error'
|
||||||
@@ -178,8 +178,6 @@ export interface TraceSpan {
|
|||||||
blockId?: string
|
blockId?: string
|
||||||
input?: Record<string, unknown>
|
input?: Record<string, unknown>
|
||||||
output?: Record<string, unknown>
|
output?: Record<string, unknown>
|
||||||
childWorkflowSnapshotId?: string
|
|
||||||
childWorkflowId?: string
|
|
||||||
model?: string
|
model?: string
|
||||||
cost?: {
|
cost?: {
|
||||||
input?: number
|
input?: number
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ export interface TraceSpan {
|
|||||||
|
|
||||||
export interface WorkflowLog {
|
export interface WorkflowLog {
|
||||||
id: string
|
id: string
|
||||||
workflowId: string | null
|
workflowId: string
|
||||||
executionId?: string | null
|
executionId?: string | null
|
||||||
deploymentVersion?: number | null
|
deploymentVersion?: number | null
|
||||||
deploymentVersionName?: string | null
|
deploymentVersionName?: string | null
|
||||||
|
|||||||
@@ -1648,8 +1648,6 @@ import {
|
|||||||
youtubeCommentsTool,
|
youtubeCommentsTool,
|
||||||
youtubePlaylistItemsTool,
|
youtubePlaylistItemsTool,
|
||||||
youtubeSearchTool,
|
youtubeSearchTool,
|
||||||
youtubeTrendingTool,
|
|
||||||
youtubeVideoCategoriesTool,
|
|
||||||
youtubeVideoDetailsTool,
|
youtubeVideoDetailsTool,
|
||||||
} from '@/tools/youtube'
|
} from '@/tools/youtube'
|
||||||
import {
|
import {
|
||||||
@@ -1984,15 +1982,13 @@ export const tools: Record<string, ToolConfig> = {
|
|||||||
typeform_create_form: typeformCreateFormTool,
|
typeform_create_form: typeformCreateFormTool,
|
||||||
typeform_update_form: typeformUpdateFormTool,
|
typeform_update_form: typeformUpdateFormTool,
|
||||||
typeform_delete_form: typeformDeleteFormTool,
|
typeform_delete_form: typeformDeleteFormTool,
|
||||||
youtube_channel_info: youtubeChannelInfoTool,
|
|
||||||
youtube_channel_playlists: youtubeChannelPlaylistsTool,
|
|
||||||
youtube_channel_videos: youtubeChannelVideosTool,
|
|
||||||
youtube_comments: youtubeCommentsTool,
|
|
||||||
youtube_playlist_items: youtubePlaylistItemsTool,
|
|
||||||
youtube_search: youtubeSearchTool,
|
youtube_search: youtubeSearchTool,
|
||||||
youtube_trending: youtubeTrendingTool,
|
|
||||||
youtube_video_categories: youtubeVideoCategoriesTool,
|
|
||||||
youtube_video_details: youtubeVideoDetailsTool,
|
youtube_video_details: youtubeVideoDetailsTool,
|
||||||
|
youtube_channel_info: youtubeChannelInfoTool,
|
||||||
|
youtube_playlist_items: youtubePlaylistItemsTool,
|
||||||
|
youtube_comments: youtubeCommentsTool,
|
||||||
|
youtube_channel_videos: youtubeChannelVideosTool,
|
||||||
|
youtube_channel_playlists: youtubeChannelPlaylistsTool,
|
||||||
notion_read: notionReadTool,
|
notion_read: notionReadTool,
|
||||||
notion_read_database: notionReadDatabaseTool,
|
notion_read_database: notionReadDatabaseTool,
|
||||||
notion_write: notionWriteTool,
|
notion_write: notionWriteTool,
|
||||||
|
|||||||
@@ -7,9 +7,8 @@ export const youtubeChannelInfoTool: ToolConfig<
|
|||||||
> = {
|
> = {
|
||||||
id: 'youtube_channel_info',
|
id: 'youtube_channel_info',
|
||||||
name: 'YouTube Channel Info',
|
name: 'YouTube Channel Info',
|
||||||
description:
|
description: 'Get detailed information about a YouTube channel.',
|
||||||
'Get detailed information about a YouTube channel including statistics, branding, and content details.',
|
version: '1.0.0',
|
||||||
version: '1.1.0',
|
|
||||||
params: {
|
params: {
|
||||||
channelId: {
|
channelId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
@@ -34,11 +33,11 @@ export const youtubeChannelInfoTool: ToolConfig<
|
|||||||
request: {
|
request: {
|
||||||
url: (params: YouTubeChannelInfoParams) => {
|
url: (params: YouTubeChannelInfoParams) => {
|
||||||
let url =
|
let url =
|
||||||
'https://www.googleapis.com/youtube/v3/channels?part=snippet,statistics,contentDetails,brandingSettings'
|
'https://www.googleapis.com/youtube/v3/channels?part=snippet,statistics,contentDetails'
|
||||||
if (params.channelId) {
|
if (params.channelId) {
|
||||||
url += `&id=${encodeURIComponent(params.channelId)}`
|
url += `&id=${params.channelId}`
|
||||||
} else if (params.username) {
|
} else if (params.username) {
|
||||||
url += `&forUsername=${encodeURIComponent(params.username)}`
|
url += `&forUsername=${params.username}`
|
||||||
}
|
}
|
||||||
url += `&key=${params.apiKey}`
|
url += `&key=${params.apiKey}`
|
||||||
return url
|
return url
|
||||||
@@ -64,11 +63,6 @@ export const youtubeChannelInfoTool: ToolConfig<
|
|||||||
viewCount: 0,
|
viewCount: 0,
|
||||||
publishedAt: '',
|
publishedAt: '',
|
||||||
thumbnail: '',
|
thumbnail: '',
|
||||||
customUrl: null,
|
|
||||||
country: null,
|
|
||||||
uploadsPlaylistId: null,
|
|
||||||
bannerImageUrl: null,
|
|
||||||
hiddenSubscriberCount: false,
|
|
||||||
},
|
},
|
||||||
error: 'Channel not found',
|
error: 'Channel not found',
|
||||||
}
|
}
|
||||||
@@ -78,23 +72,19 @@ export const youtubeChannelInfoTool: ToolConfig<
|
|||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
output: {
|
output: {
|
||||||
channelId: item.id ?? '',
|
channelId: item.id,
|
||||||
title: item.snippet?.title ?? '',
|
title: item.snippet?.title || '',
|
||||||
description: item.snippet?.description ?? '',
|
description: item.snippet?.description || '',
|
||||||
subscriberCount: Number(item.statistics?.subscriberCount || 0),
|
subscriberCount: Number(item.statistics?.subscriberCount || 0),
|
||||||
videoCount: Number(item.statistics?.videoCount || 0),
|
videoCount: Number(item.statistics?.videoCount || 0),
|
||||||
viewCount: Number(item.statistics?.viewCount || 0),
|
viewCount: Number(item.statistics?.viewCount || 0),
|
||||||
publishedAt: item.snippet?.publishedAt ?? '',
|
publishedAt: item.snippet?.publishedAt || '',
|
||||||
thumbnail:
|
thumbnail:
|
||||||
item.snippet?.thumbnails?.high?.url ||
|
item.snippet?.thumbnails?.high?.url ||
|
||||||
item.snippet?.thumbnails?.medium?.url ||
|
item.snippet?.thumbnails?.medium?.url ||
|
||||||
item.snippet?.thumbnails?.default?.url ||
|
item.snippet?.thumbnails?.default?.url ||
|
||||||
'',
|
'',
|
||||||
customUrl: item.snippet?.customUrl ?? null,
|
customUrl: item.snippet?.customUrl,
|
||||||
country: item.snippet?.country ?? null,
|
|
||||||
uploadsPlaylistId: item.contentDetails?.relatedPlaylists?.uploads ?? null,
|
|
||||||
bannerImageUrl: item.brandingSettings?.image?.bannerExternalUrl ?? null,
|
|
||||||
hiddenSubscriberCount: item.statistics?.hiddenSubscriberCount ?? false,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -114,11 +104,11 @@ export const youtubeChannelInfoTool: ToolConfig<
|
|||||||
},
|
},
|
||||||
subscriberCount: {
|
subscriberCount: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
description: 'Number of subscribers (0 if hidden)',
|
description: 'Number of subscribers',
|
||||||
},
|
},
|
||||||
videoCount: {
|
videoCount: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
description: 'Number of public videos',
|
description: 'Number of videos',
|
||||||
},
|
},
|
||||||
viewCount: {
|
viewCount: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
@@ -130,31 +120,12 @@ export const youtubeChannelInfoTool: ToolConfig<
|
|||||||
},
|
},
|
||||||
thumbnail: {
|
thumbnail: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Channel thumbnail/avatar URL',
|
description: 'Channel thumbnail URL',
|
||||||
},
|
},
|
||||||
customUrl: {
|
customUrl: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Channel custom URL (handle)',
|
description: 'Channel custom URL',
|
||||||
optional: true,
|
optional: true,
|
||||||
},
|
},
|
||||||
country: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'Country the channel is associated with',
|
|
||||||
optional: true,
|
|
||||||
},
|
|
||||||
uploadsPlaylistId: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'Playlist ID containing all channel uploads (use with playlist_items)',
|
|
||||||
optional: true,
|
|
||||||
},
|
|
||||||
bannerImageUrl: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'Channel banner image URL',
|
|
||||||
optional: true,
|
|
||||||
},
|
|
||||||
hiddenSubscriberCount: {
|
|
||||||
type: 'boolean',
|
|
||||||
description: 'Whether the subscriber count is hidden',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ export const youtubeChannelPlaylistsTool: ToolConfig<
|
|||||||
> = {
|
> = {
|
||||||
id: 'youtube_channel_playlists',
|
id: 'youtube_channel_playlists',
|
||||||
name: 'YouTube Channel Playlists',
|
name: 'YouTube Channel Playlists',
|
||||||
description: 'Get all public playlists from a specific YouTube channel.',
|
description: 'Get all playlists from a specific YouTube channel.',
|
||||||
version: '1.1.0',
|
version: '1.0.0',
|
||||||
params: {
|
params: {
|
||||||
channelId: {
|
channelId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
@@ -47,7 +47,7 @@ export const youtubeChannelPlaylistsTool: ToolConfig<
|
|||||||
)}&key=${params.apiKey}`
|
)}&key=${params.apiKey}`
|
||||||
url += `&maxResults=${Number(params.maxResults || 10)}`
|
url += `&maxResults=${Number(params.maxResults || 10)}`
|
||||||
if (params.pageToken) {
|
if (params.pageToken) {
|
||||||
url += `&pageToken=${encodeURIComponent(params.pageToken)}`
|
url += `&pageToken=${params.pageToken}`
|
||||||
}
|
}
|
||||||
return url
|
return url
|
||||||
},
|
},
|
||||||
@@ -60,49 +60,36 @@ export const youtubeChannelPlaylistsTool: ToolConfig<
|
|||||||
transformResponse: async (response: Response): Promise<YouTubeChannelPlaylistsResponse> => {
|
transformResponse: async (response: Response): Promise<YouTubeChannelPlaylistsResponse> => {
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
|
|
||||||
if (data.error) {
|
if (!data.items) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
output: {
|
output: {
|
||||||
items: [],
|
items: [],
|
||||||
totalResults: 0,
|
totalResults: 0,
|
||||||
nextPageToken: null,
|
|
||||||
},
|
|
||||||
error: data.error.message || 'Failed to fetch channel playlists',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data.items || data.items.length === 0) {
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
output: {
|
|
||||||
items: [],
|
|
||||||
totalResults: 0,
|
|
||||||
nextPageToken: null,
|
|
||||||
},
|
},
|
||||||
|
error: 'No playlists found',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const items = (data.items || []).map((item: any) => ({
|
const items = (data.items || []).map((item: any) => ({
|
||||||
playlistId: item.id ?? '',
|
playlistId: item.id,
|
||||||
title: item.snippet?.title ?? '',
|
title: item.snippet?.title || '',
|
||||||
description: item.snippet?.description ?? '',
|
description: item.snippet?.description || '',
|
||||||
thumbnail:
|
thumbnail:
|
||||||
item.snippet?.thumbnails?.medium?.url ||
|
item.snippet?.thumbnails?.medium?.url ||
|
||||||
item.snippet?.thumbnails?.default?.url ||
|
item.snippet?.thumbnails?.default?.url ||
|
||||||
item.snippet?.thumbnails?.high?.url ||
|
item.snippet?.thumbnails?.high?.url ||
|
||||||
'',
|
'',
|
||||||
itemCount: Number(item.contentDetails?.itemCount || 0),
|
itemCount: item.contentDetails?.itemCount || 0,
|
||||||
publishedAt: item.snippet?.publishedAt ?? '',
|
publishedAt: item.snippet?.publishedAt || '',
|
||||||
channelTitle: item.snippet?.channelTitle ?? '',
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
output: {
|
output: {
|
||||||
items,
|
items,
|
||||||
totalResults: data.pageInfo?.totalResults || items.length,
|
totalResults: data.pageInfo?.totalResults || 0,
|
||||||
nextPageToken: data.nextPageToken ?? null,
|
nextPageToken: data.nextPageToken,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -120,7 +107,6 @@ export const youtubeChannelPlaylistsTool: ToolConfig<
|
|||||||
thumbnail: { type: 'string', description: 'Playlist thumbnail URL' },
|
thumbnail: { type: 'string', description: 'Playlist thumbnail URL' },
|
||||||
itemCount: { type: 'number', description: 'Number of videos in playlist' },
|
itemCount: { type: 'number', description: 'Number of videos in playlist' },
|
||||||
publishedAt: { type: 'string', description: 'Playlist creation date' },
|
publishedAt: { type: 'string', description: 'Playlist creation date' },
|
||||||
channelTitle: { type: 'string', description: 'Channel name' },
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,9 +10,8 @@ export const youtubeChannelVideosTool: ToolConfig<
|
|||||||
> = {
|
> = {
|
||||||
id: 'youtube_channel_videos',
|
id: 'youtube_channel_videos',
|
||||||
name: 'YouTube Channel Videos',
|
name: 'YouTube Channel Videos',
|
||||||
description:
|
description: 'Get all videos from a specific YouTube channel, with sorting options.',
|
||||||
'Search for videos from a specific YouTube channel with sorting options. For complete channel video list, use channel_info to get uploadsPlaylistId, then use playlist_items.',
|
version: '1.0.0',
|
||||||
version: '1.1.0',
|
|
||||||
params: {
|
params: {
|
||||||
channelId: {
|
channelId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
@@ -31,8 +30,7 @@ export const youtubeChannelVideosTool: ToolConfig<
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
required: false,
|
required: false,
|
||||||
visibility: 'user-or-llm',
|
visibility: 'user-or-llm',
|
||||||
description:
|
description: 'Sort order: "date" (newest first), "rating", "relevance", "title", "viewCount"',
|
||||||
'Sort order: "date" (newest first, default), "rating", "relevance", "title", "viewCount"',
|
|
||||||
},
|
},
|
||||||
pageToken: {
|
pageToken: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
@@ -54,9 +52,11 @@ export const youtubeChannelVideosTool: ToolConfig<
|
|||||||
params.channelId
|
params.channelId
|
||||||
)}&key=${params.apiKey}`
|
)}&key=${params.apiKey}`
|
||||||
url += `&maxResults=${Number(params.maxResults || 10)}`
|
url += `&maxResults=${Number(params.maxResults || 10)}`
|
||||||
url += `&order=${params.order || 'date'}`
|
if (params.order) {
|
||||||
|
url += `&order=${params.order}`
|
||||||
|
}
|
||||||
if (params.pageToken) {
|
if (params.pageToken) {
|
||||||
url += `&pageToken=${encodeURIComponent(params.pageToken)}`
|
url += `&pageToken=${params.pageToken}`
|
||||||
}
|
}
|
||||||
return url
|
return url
|
||||||
},
|
},
|
||||||
@@ -68,38 +68,23 @@ export const youtubeChannelVideosTool: ToolConfig<
|
|||||||
|
|
||||||
transformResponse: async (response: Response): Promise<YouTubeChannelVideosResponse> => {
|
transformResponse: async (response: Response): Promise<YouTubeChannelVideosResponse> => {
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
|
|
||||||
if (data.error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
output: {
|
|
||||||
items: [],
|
|
||||||
totalResults: 0,
|
|
||||||
nextPageToken: null,
|
|
||||||
},
|
|
||||||
error: data.error.message || 'Failed to fetch channel videos',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const items = (data.items || []).map((item: any) => ({
|
const items = (data.items || []).map((item: any) => ({
|
||||||
videoId: item.id?.videoId ?? '',
|
videoId: item.id?.videoId,
|
||||||
title: item.snippet?.title ?? '',
|
title: item.snippet?.title,
|
||||||
description: item.snippet?.description ?? '',
|
description: item.snippet?.description,
|
||||||
thumbnail:
|
thumbnail:
|
||||||
item.snippet?.thumbnails?.medium?.url ||
|
item.snippet?.thumbnails?.medium?.url ||
|
||||||
item.snippet?.thumbnails?.default?.url ||
|
item.snippet?.thumbnails?.default?.url ||
|
||||||
item.snippet?.thumbnails?.high?.url ||
|
item.snippet?.thumbnails?.high?.url ||
|
||||||
'',
|
'',
|
||||||
publishedAt: item.snippet?.publishedAt ?? '',
|
publishedAt: item.snippet?.publishedAt || '',
|
||||||
channelTitle: item.snippet?.channelTitle ?? '',
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
output: {
|
output: {
|
||||||
items,
|
items,
|
||||||
totalResults: data.pageInfo?.totalResults || items.length,
|
totalResults: data.pageInfo?.totalResults || 0,
|
||||||
nextPageToken: data.nextPageToken ?? null,
|
nextPageToken: data.nextPageToken,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -116,7 +101,6 @@ export const youtubeChannelVideosTool: ToolConfig<
|
|||||||
description: { type: 'string', description: 'Video description' },
|
description: { type: 'string', description: 'Video description' },
|
||||||
thumbnail: { type: 'string', description: 'Video thumbnail URL' },
|
thumbnail: { type: 'string', description: 'Video thumbnail URL' },
|
||||||
publishedAt: { type: 'string', description: 'Video publish date' },
|
publishedAt: { type: 'string', description: 'Video publish date' },
|
||||||
channelTitle: { type: 'string', description: 'Channel name' },
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import type { YouTubeCommentsParams, YouTubeCommentsResponse } from '@/tools/you
|
|||||||
export const youtubeCommentsTool: ToolConfig<YouTubeCommentsParams, YouTubeCommentsResponse> = {
|
export const youtubeCommentsTool: ToolConfig<YouTubeCommentsParams, YouTubeCommentsResponse> = {
|
||||||
id: 'youtube_comments',
|
id: 'youtube_comments',
|
||||||
name: 'YouTube Video Comments',
|
name: 'YouTube Video Comments',
|
||||||
description: 'Get top-level comments from a YouTube video with author details and engagement.',
|
description: 'Get comments from a YouTube video.',
|
||||||
version: '1.1.0',
|
version: '1.0.0',
|
||||||
params: {
|
params: {
|
||||||
videoId: {
|
videoId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
@@ -18,14 +18,14 @@ export const youtubeCommentsTool: ToolConfig<YouTubeCommentsParams, YouTubeComme
|
|||||||
required: false,
|
required: false,
|
||||||
visibility: 'user-only',
|
visibility: 'user-only',
|
||||||
default: 20,
|
default: 20,
|
||||||
description: 'Maximum number of comments to return (1-100)',
|
description: 'Maximum number of comments to return',
|
||||||
},
|
},
|
||||||
order: {
|
order: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
required: false,
|
required: false,
|
||||||
visibility: 'user-or-llm',
|
visibility: 'user-only',
|
||||||
default: 'relevance',
|
default: 'relevance',
|
||||||
description: 'Order of comments: "time" (newest first) or "relevance" (most relevant first)',
|
description: 'Order of comments: time or relevance',
|
||||||
},
|
},
|
||||||
pageToken: {
|
pageToken: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
@@ -43,11 +43,11 @@ export const youtubeCommentsTool: ToolConfig<YouTubeCommentsParams, YouTubeComme
|
|||||||
|
|
||||||
request: {
|
request: {
|
||||||
url: (params: YouTubeCommentsParams) => {
|
url: (params: YouTubeCommentsParams) => {
|
||||||
let url = `https://www.googleapis.com/youtube/v3/commentThreads?part=snippet,replies&videoId=${encodeURIComponent(params.videoId)}&key=${params.apiKey}`
|
let url = `https://www.googleapis.com/youtube/v3/commentThreads?part=snippet,replies&videoId=${params.videoId}&key=${params.apiKey}`
|
||||||
url += `&maxResults=${Number(params.maxResults || 20)}`
|
url += `&maxResults=${Number(params.maxResults || 20)}`
|
||||||
url += `&order=${params.order || 'relevance'}`
|
url += `&order=${params.order || 'relevance'}`
|
||||||
if (params.pageToken) {
|
if (params.pageToken) {
|
||||||
url += `&pageToken=${encodeURIComponent(params.pageToken)}`
|
url += `&pageToken=${params.pageToken}`
|
||||||
}
|
}
|
||||||
return url
|
return url
|
||||||
},
|
},
|
||||||
@@ -60,31 +60,18 @@ export const youtubeCommentsTool: ToolConfig<YouTubeCommentsParams, YouTubeComme
|
|||||||
transformResponse: async (response: Response): Promise<YouTubeCommentsResponse> => {
|
transformResponse: async (response: Response): Promise<YouTubeCommentsResponse> => {
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
|
|
||||||
if (data.error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
output: {
|
|
||||||
items: [],
|
|
||||||
totalResults: 0,
|
|
||||||
nextPageToken: null,
|
|
||||||
},
|
|
||||||
error: data.error.message || 'Failed to fetch comments',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const items = (data.items || []).map((item: any) => {
|
const items = (data.items || []).map((item: any) => {
|
||||||
const topLevelComment = item.snippet?.topLevelComment?.snippet
|
const topLevelComment = item.snippet?.topLevelComment?.snippet
|
||||||
return {
|
return {
|
||||||
commentId: item.snippet?.topLevelComment?.id ?? item.id ?? '',
|
commentId: item.snippet?.topLevelComment?.id || item.id,
|
||||||
authorDisplayName: topLevelComment?.authorDisplayName ?? '',
|
authorDisplayName: topLevelComment?.authorDisplayName || '',
|
||||||
authorChannelUrl: topLevelComment?.authorChannelUrl ?? '',
|
authorChannelUrl: topLevelComment?.authorChannelUrl || '',
|
||||||
authorProfileImageUrl: topLevelComment?.authorProfileImageUrl ?? '',
|
textDisplay: topLevelComment?.textDisplay || '',
|
||||||
textDisplay: topLevelComment?.textDisplay ?? '',
|
textOriginal: topLevelComment?.textOriginal || '',
|
||||||
textOriginal: topLevelComment?.textOriginal ?? '',
|
likeCount: topLevelComment?.likeCount || 0,
|
||||||
likeCount: Number(topLevelComment?.likeCount || 0),
|
publishedAt: topLevelComment?.publishedAt || '',
|
||||||
publishedAt: topLevelComment?.publishedAt ?? '',
|
updatedAt: topLevelComment?.updatedAt || '',
|
||||||
updatedAt: topLevelComment?.updatedAt ?? '',
|
replyCount: item.snippet?.totalReplyCount || 0,
|
||||||
replyCount: Number(item.snippet?.totalReplyCount || 0),
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -92,8 +79,8 @@ export const youtubeCommentsTool: ToolConfig<YouTubeCommentsParams, YouTubeComme
|
|||||||
success: true,
|
success: true,
|
||||||
output: {
|
output: {
|
||||||
items,
|
items,
|
||||||
totalResults: data.pageInfo?.totalResults || items.length,
|
totalResults: data.pageInfo?.totalResults || 0,
|
||||||
nextPageToken: data.nextPageToken ?? null,
|
nextPageToken: data.nextPageToken,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -101,29 +88,25 @@ export const youtubeCommentsTool: ToolConfig<YouTubeCommentsParams, YouTubeComme
|
|||||||
outputs: {
|
outputs: {
|
||||||
items: {
|
items: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
description: 'Array of top-level comments from the video',
|
description: 'Array of comments from the video',
|
||||||
items: {
|
items: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
commentId: { type: 'string', description: 'Comment ID' },
|
commentId: { type: 'string', description: 'Comment ID' },
|
||||||
authorDisplayName: { type: 'string', description: 'Comment author display name' },
|
authorDisplayName: { type: 'string', description: 'Comment author name' },
|
||||||
authorChannelUrl: { type: 'string', description: 'Comment author channel URL' },
|
authorChannelUrl: { type: 'string', description: 'Comment author channel URL' },
|
||||||
authorProfileImageUrl: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'Comment author profile image URL',
|
|
||||||
},
|
|
||||||
textDisplay: { type: 'string', description: 'Comment text (HTML formatted)' },
|
textDisplay: { type: 'string', description: 'Comment text (HTML formatted)' },
|
||||||
textOriginal: { type: 'string', description: 'Comment text (plain text)' },
|
textOriginal: { type: 'string', description: 'Comment text (plain text)' },
|
||||||
likeCount: { type: 'number', description: 'Number of likes on the comment' },
|
likeCount: { type: 'number', description: 'Number of likes' },
|
||||||
publishedAt: { type: 'string', description: 'When the comment was posted' },
|
publishedAt: { type: 'string', description: 'Comment publish date' },
|
||||||
updatedAt: { type: 'string', description: 'When the comment was last edited' },
|
updatedAt: { type: 'string', description: 'Comment last updated date' },
|
||||||
replyCount: { type: 'number', description: 'Number of replies to this comment' },
|
replyCount: { type: 'number', description: 'Number of replies', optional: true },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
totalResults: {
|
totalResults: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
description: 'Total number of comment threads available',
|
description: 'Total number of comments',
|
||||||
},
|
},
|
||||||
nextPageToken: {
|
nextPageToken: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import { youtubeChannelVideosTool } from '@/tools/youtube/channel_videos'
|
|||||||
import { youtubeCommentsTool } from '@/tools/youtube/comments'
|
import { youtubeCommentsTool } from '@/tools/youtube/comments'
|
||||||
import { youtubePlaylistItemsTool } from '@/tools/youtube/playlist_items'
|
import { youtubePlaylistItemsTool } from '@/tools/youtube/playlist_items'
|
||||||
import { youtubeSearchTool } from '@/tools/youtube/search'
|
import { youtubeSearchTool } from '@/tools/youtube/search'
|
||||||
import { youtubeTrendingTool } from '@/tools/youtube/trending'
|
|
||||||
import { youtubeVideoCategoriesTool } from '@/tools/youtube/video_categories'
|
|
||||||
import { youtubeVideoDetailsTool } from '@/tools/youtube/video_details'
|
import { youtubeVideoDetailsTool } from '@/tools/youtube/video_details'
|
||||||
|
|
||||||
export { youtubeSearchTool }
|
export { youtubeSearchTool }
|
||||||
@@ -15,5 +13,3 @@ export { youtubePlaylistItemsTool }
|
|||||||
export { youtubeCommentsTool }
|
export { youtubeCommentsTool }
|
||||||
export { youtubeChannelVideosTool }
|
export { youtubeChannelVideosTool }
|
||||||
export { youtubeChannelPlaylistsTool }
|
export { youtubeChannelPlaylistsTool }
|
||||||
export { youtubeTrendingTool }
|
|
||||||
export { youtubeVideoCategoriesTool }
|
|
||||||
|
|||||||
@@ -10,23 +10,21 @@ export const youtubePlaylistItemsTool: ToolConfig<
|
|||||||
> = {
|
> = {
|
||||||
id: 'youtube_playlist_items',
|
id: 'youtube_playlist_items',
|
||||||
name: 'YouTube Playlist Items',
|
name: 'YouTube Playlist Items',
|
||||||
description:
|
description: 'Get videos from a YouTube playlist.',
|
||||||
'Get videos from a YouTube playlist. Can be used with a channel uploads playlist to get all channel videos.',
|
version: '1.0.0',
|
||||||
version: '1.1.0',
|
|
||||||
params: {
|
params: {
|
||||||
playlistId: {
|
playlistId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
required: true,
|
required: true,
|
||||||
visibility: 'user-or-llm',
|
visibility: 'user-or-llm',
|
||||||
description:
|
description: 'YouTube playlist ID',
|
||||||
'YouTube playlist ID. Use uploadsPlaylistId from channel_info to get all channel videos.',
|
|
||||||
},
|
},
|
||||||
maxResults: {
|
maxResults: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
required: false,
|
required: false,
|
||||||
visibility: 'user-only',
|
visibility: 'user-only',
|
||||||
default: 10,
|
default: 10,
|
||||||
description: 'Maximum number of videos to return (1-50)',
|
description: 'Maximum number of videos to return',
|
||||||
},
|
},
|
||||||
pageToken: {
|
pageToken: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
@@ -44,10 +42,10 @@ export const youtubePlaylistItemsTool: ToolConfig<
|
|||||||
|
|
||||||
request: {
|
request: {
|
||||||
url: (params: YouTubePlaylistItemsParams) => {
|
url: (params: YouTubePlaylistItemsParams) => {
|
||||||
let url = `https://www.googleapis.com/youtube/v3/playlistItems?part=snippet,contentDetails&playlistId=${encodeURIComponent(params.playlistId)}&key=${params.apiKey}`
|
let url = `https://www.googleapis.com/youtube/v3/playlistItems?part=snippet,contentDetails&playlistId=${params.playlistId}&key=${params.apiKey}`
|
||||||
url += `&maxResults=${Number(params.maxResults || 10)}`
|
url += `&maxResults=${Number(params.maxResults || 10)}`
|
||||||
if (params.pageToken) {
|
if (params.pageToken) {
|
||||||
url += `&pageToken=${encodeURIComponent(params.pageToken)}`
|
url += `&pageToken=${params.pageToken}`
|
||||||
}
|
}
|
||||||
return url
|
return url
|
||||||
},
|
},
|
||||||
@@ -60,40 +58,26 @@ export const youtubePlaylistItemsTool: ToolConfig<
|
|||||||
transformResponse: async (response: Response): Promise<YouTubePlaylistItemsResponse> => {
|
transformResponse: async (response: Response): Promise<YouTubePlaylistItemsResponse> => {
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
|
|
||||||
if (data.error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
output: {
|
|
||||||
items: [],
|
|
||||||
totalResults: 0,
|
|
||||||
nextPageToken: null,
|
|
||||||
},
|
|
||||||
error: data.error.message || 'Failed to fetch playlist items',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const items = (data.items || []).map((item: any, index: number) => ({
|
const items = (data.items || []).map((item: any, index: number) => ({
|
||||||
videoId: item.contentDetails?.videoId ?? item.snippet?.resourceId?.videoId ?? '',
|
videoId: item.contentDetails?.videoId || item.snippet?.resourceId?.videoId,
|
||||||
title: item.snippet?.title ?? '',
|
title: item.snippet?.title || '',
|
||||||
description: item.snippet?.description ?? '',
|
description: item.snippet?.description || '',
|
||||||
thumbnail:
|
thumbnail:
|
||||||
item.snippet?.thumbnails?.medium?.url ||
|
item.snippet?.thumbnails?.medium?.url ||
|
||||||
item.snippet?.thumbnails?.default?.url ||
|
item.snippet?.thumbnails?.default?.url ||
|
||||||
item.snippet?.thumbnails?.high?.url ||
|
item.snippet?.thumbnails?.high?.url ||
|
||||||
'',
|
'',
|
||||||
publishedAt: item.snippet?.publishedAt ?? '',
|
publishedAt: item.snippet?.publishedAt || '',
|
||||||
channelTitle: item.snippet?.channelTitle ?? '',
|
channelTitle: item.snippet?.channelTitle || '',
|
||||||
position: item.snippet?.position ?? index,
|
position: item.snippet?.position ?? index,
|
||||||
videoOwnerChannelId: item.snippet?.videoOwnerChannelId ?? null,
|
|
||||||
videoOwnerChannelTitle: item.snippet?.videoOwnerChannelTitle ?? null,
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
output: {
|
output: {
|
||||||
items,
|
items,
|
||||||
totalResults: data.pageInfo?.totalResults || items.length,
|
totalResults: data.pageInfo?.totalResults || 0,
|
||||||
nextPageToken: data.nextPageToken ?? null,
|
nextPageToken: data.nextPageToken,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -110,18 +94,8 @@ export const youtubePlaylistItemsTool: ToolConfig<
|
|||||||
description: { type: 'string', description: 'Video description' },
|
description: { type: 'string', description: 'Video description' },
|
||||||
thumbnail: { type: 'string', description: 'Video thumbnail URL' },
|
thumbnail: { type: 'string', description: 'Video thumbnail URL' },
|
||||||
publishedAt: { type: 'string', description: 'Date added to playlist' },
|
publishedAt: { type: 'string', description: 'Date added to playlist' },
|
||||||
channelTitle: { type: 'string', description: 'Playlist owner channel name' },
|
channelTitle: { type: 'string', description: 'Channel name' },
|
||||||
position: { type: 'number', description: 'Position in playlist (0-indexed)' },
|
position: { type: 'number', description: 'Position in playlist' },
|
||||||
videoOwnerChannelId: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'Channel ID of the video owner',
|
|
||||||
optional: true,
|
|
||||||
},
|
|
||||||
videoOwnerChannelTitle: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'Channel name of the video owner',
|
|
||||||
optional: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ export const youtubeSearchTool: ToolConfig<YouTubeSearchParams, YouTubeSearchRes
|
|||||||
id: 'youtube_search',
|
id: 'youtube_search',
|
||||||
name: 'YouTube Search',
|
name: 'YouTube Search',
|
||||||
description:
|
description:
|
||||||
'Search for videos on YouTube using the YouTube Data API. Supports advanced filtering by channel, date range, duration, category, quality, captions, live streams, and more.',
|
'Search for videos on YouTube using the YouTube Data API. Supports advanced filtering by channel, date range, duration, category, quality, captions, and more.',
|
||||||
version: '1.2.0',
|
version: '1.0.0',
|
||||||
params: {
|
params: {
|
||||||
query: {
|
query: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
@@ -21,18 +21,13 @@ export const youtubeSearchTool: ToolConfig<YouTubeSearchParams, YouTubeSearchRes
|
|||||||
default: 5,
|
default: 5,
|
||||||
description: 'Maximum number of videos to return (1-50)',
|
description: 'Maximum number of videos to return (1-50)',
|
||||||
},
|
},
|
||||||
pageToken: {
|
|
||||||
type: 'string',
|
|
||||||
required: false,
|
|
||||||
visibility: 'user-only',
|
|
||||||
description: 'Page token for pagination (use nextPageToken from previous response)',
|
|
||||||
},
|
|
||||||
apiKey: {
|
apiKey: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
required: true,
|
required: true,
|
||||||
visibility: 'user-only',
|
visibility: 'user-only',
|
||||||
description: 'YouTube API Key',
|
description: 'YouTube API Key',
|
||||||
},
|
},
|
||||||
|
// Priority 1: Essential filters
|
||||||
channelId: {
|
channelId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
required: false,
|
required: false,
|
||||||
@@ -71,9 +66,9 @@ export const youtubeSearchTool: ToolConfig<YouTubeSearchParams, YouTubeSearchRes
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
required: false,
|
required: false,
|
||||||
visibility: 'user-or-llm',
|
visibility: 'user-or-llm',
|
||||||
description:
|
description: 'Filter by YouTube category ID (e.g., "10" for Music, "20" for Gaming)',
|
||||||
'Filter by YouTube category ID (e.g., "10" for Music, "20" for Gaming). Use video_categories to list IDs.',
|
|
||||||
},
|
},
|
||||||
|
// Priority 2: Very useful filters
|
||||||
videoDefinition: {
|
videoDefinition: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
required: false,
|
required: false,
|
||||||
@@ -87,13 +82,6 @@ export const youtubeSearchTool: ToolConfig<YouTubeSearchParams, YouTubeSearchRes
|
|||||||
description:
|
description:
|
||||||
'Filter by caption availability: "closedCaption" (has captions), "none" (no captions), "any"',
|
'Filter by caption availability: "closedCaption" (has captions), "none" (no captions), "any"',
|
||||||
},
|
},
|
||||||
eventType: {
|
|
||||||
type: 'string',
|
|
||||||
required: false,
|
|
||||||
visibility: 'user-or-llm',
|
|
||||||
description:
|
|
||||||
'Filter by live broadcast status: "live" (currently live), "upcoming" (scheduled), "completed" (past streams)',
|
|
||||||
},
|
|
||||||
regionCode: {
|
regionCode: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
required: false,
|
required: false,
|
||||||
@@ -122,9 +110,7 @@ export const youtubeSearchTool: ToolConfig<YouTubeSearchParams, YouTubeSearchRes
|
|||||||
)}`
|
)}`
|
||||||
url += `&maxResults=${Number(params.maxResults || 5)}`
|
url += `&maxResults=${Number(params.maxResults || 5)}`
|
||||||
|
|
||||||
if (params.pageToken) {
|
// Add Priority 1 filters if provided
|
||||||
url += `&pageToken=${encodeURIComponent(params.pageToken)}`
|
|
||||||
}
|
|
||||||
if (params.channelId) {
|
if (params.channelId) {
|
||||||
url += `&channelId=${encodeURIComponent(params.channelId)}`
|
url += `&channelId=${encodeURIComponent(params.channelId)}`
|
||||||
}
|
}
|
||||||
@@ -143,15 +129,14 @@ export const youtubeSearchTool: ToolConfig<YouTubeSearchParams, YouTubeSearchRes
|
|||||||
if (params.videoCategoryId) {
|
if (params.videoCategoryId) {
|
||||||
url += `&videoCategoryId=${params.videoCategoryId}`
|
url += `&videoCategoryId=${params.videoCategoryId}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add Priority 2 filters if provided
|
||||||
if (params.videoDefinition) {
|
if (params.videoDefinition) {
|
||||||
url += `&videoDefinition=${params.videoDefinition}`
|
url += `&videoDefinition=${params.videoDefinition}`
|
||||||
}
|
}
|
||||||
if (params.videoCaption) {
|
if (params.videoCaption) {
|
||||||
url += `&videoCaption=${params.videoCaption}`
|
url += `&videoCaption=${params.videoCaption}`
|
||||||
}
|
}
|
||||||
if (params.eventType) {
|
|
||||||
url += `&eventType=${params.eventType}`
|
|
||||||
}
|
|
||||||
if (params.regionCode) {
|
if (params.regionCode) {
|
||||||
url += `®ionCode=${params.regionCode}`
|
url += `®ionCode=${params.regionCode}`
|
||||||
}
|
}
|
||||||
@@ -172,39 +157,22 @@ export const youtubeSearchTool: ToolConfig<YouTubeSearchParams, YouTubeSearchRes
|
|||||||
|
|
||||||
transformResponse: async (response: Response): Promise<YouTubeSearchResponse> => {
|
transformResponse: async (response: Response): Promise<YouTubeSearchResponse> => {
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
|
|
||||||
if (data.error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
output: {
|
|
||||||
items: [],
|
|
||||||
totalResults: 0,
|
|
||||||
nextPageToken: null,
|
|
||||||
},
|
|
||||||
error: data.error.message || 'Search failed',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const items = (data.items || []).map((item: any) => ({
|
const items = (data.items || []).map((item: any) => ({
|
||||||
videoId: item.id?.videoId ?? '',
|
videoId: item.id?.videoId,
|
||||||
title: item.snippet?.title ?? '',
|
title: item.snippet?.title,
|
||||||
description: item.snippet?.description ?? '',
|
description: item.snippet?.description,
|
||||||
thumbnail:
|
thumbnail:
|
||||||
item.snippet?.thumbnails?.default?.url ||
|
item.snippet?.thumbnails?.default?.url ||
|
||||||
item.snippet?.thumbnails?.medium?.url ||
|
item.snippet?.thumbnails?.medium?.url ||
|
||||||
item.snippet?.thumbnails?.high?.url ||
|
item.snippet?.thumbnails?.high?.url ||
|
||||||
'',
|
'',
|
||||||
channelId: item.snippet?.channelId ?? '',
|
|
||||||
channelTitle: item.snippet?.channelTitle ?? '',
|
|
||||||
publishedAt: item.snippet?.publishedAt ?? '',
|
|
||||||
liveBroadcastContent: item.snippet?.liveBroadcastContent ?? 'none',
|
|
||||||
}))
|
}))
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
output: {
|
output: {
|
||||||
items,
|
items,
|
||||||
totalResults: data.pageInfo?.totalResults || 0,
|
totalResults: data.pageInfo?.totalResults || 0,
|
||||||
nextPageToken: data.nextPageToken ?? null,
|
nextPageToken: data.nextPageToken,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -220,13 +188,6 @@ export const youtubeSearchTool: ToolConfig<YouTubeSearchParams, YouTubeSearchRes
|
|||||||
title: { type: 'string', description: 'Video title' },
|
title: { type: 'string', description: 'Video title' },
|
||||||
description: { type: 'string', description: 'Video description' },
|
description: { type: 'string', description: 'Video description' },
|
||||||
thumbnail: { type: 'string', description: 'Video thumbnail URL' },
|
thumbnail: { type: 'string', description: 'Video thumbnail URL' },
|
||||||
channelId: { type: 'string', description: 'Channel ID that uploaded the video' },
|
|
||||||
channelTitle: { type: 'string', description: 'Channel name' },
|
|
||||||
publishedAt: { type: 'string', description: 'Video publish date' },
|
|
||||||
liveBroadcastContent: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'Live broadcast status: "none", "live", or "upcoming"',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,139 +0,0 @@
|
|||||||
import type { ToolConfig } from '@/tools/types'
|
|
||||||
import type { YouTubeTrendingParams, YouTubeTrendingResponse } from '@/tools/youtube/types'
|
|
||||||
|
|
||||||
export const youtubeTrendingTool: ToolConfig<YouTubeTrendingParams, YouTubeTrendingResponse> = {
|
|
||||||
id: 'youtube_trending',
|
|
||||||
name: 'YouTube Trending Videos',
|
|
||||||
description:
|
|
||||||
'Get the most popular/trending videos on YouTube. Can filter by region and video category.',
|
|
||||||
version: '1.0.0',
|
|
||||||
params: {
|
|
||||||
regionCode: {
|
|
||||||
type: 'string',
|
|
||||||
required: false,
|
|
||||||
visibility: 'user-or-llm',
|
|
||||||
description:
|
|
||||||
'ISO 3166-1 alpha-2 country code to get trending videos for (e.g., "US", "GB", "JP"). Defaults to US.',
|
|
||||||
},
|
|
||||||
videoCategoryId: {
|
|
||||||
type: 'string',
|
|
||||||
required: false,
|
|
||||||
visibility: 'user-or-llm',
|
|
||||||
description:
|
|
||||||
'Filter by video category ID (e.g., "10" for Music, "20" for Gaming, "17" for Sports)',
|
|
||||||
},
|
|
||||||
maxResults: {
|
|
||||||
type: 'number',
|
|
||||||
required: false,
|
|
||||||
visibility: 'user-only',
|
|
||||||
default: 10,
|
|
||||||
description: 'Maximum number of trending videos to return (1-50)',
|
|
||||||
},
|
|
||||||
pageToken: {
|
|
||||||
type: 'string',
|
|
||||||
required: false,
|
|
||||||
visibility: 'user-only',
|
|
||||||
description: 'Page token for pagination',
|
|
||||||
},
|
|
||||||
apiKey: {
|
|
||||||
type: 'string',
|
|
||||||
required: true,
|
|
||||||
visibility: 'user-only',
|
|
||||||
description: 'YouTube API Key',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
request: {
|
|
||||||
url: (params: YouTubeTrendingParams) => {
|
|
||||||
let url = `https://www.googleapis.com/youtube/v3/videos?part=snippet,statistics,contentDetails&chart=mostPopular&key=${params.apiKey}`
|
|
||||||
url += `&maxResults=${Number(params.maxResults || 10)}`
|
|
||||||
url += `®ionCode=${params.regionCode || 'US'}`
|
|
||||||
if (params.videoCategoryId) {
|
|
||||||
url += `&videoCategoryId=${params.videoCategoryId}`
|
|
||||||
}
|
|
||||||
if (params.pageToken) {
|
|
||||||
url += `&pageToken=${encodeURIComponent(params.pageToken)}`
|
|
||||||
}
|
|
||||||
return url
|
|
||||||
},
|
|
||||||
method: 'GET',
|
|
||||||
headers: () => ({
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
|
|
||||||
transformResponse: async (response: Response): Promise<YouTubeTrendingResponse> => {
|
|
||||||
const data = await response.json()
|
|
||||||
|
|
||||||
if (data.error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
output: {
|
|
||||||
items: [],
|
|
||||||
totalResults: 0,
|
|
||||||
nextPageToken: null,
|
|
||||||
},
|
|
||||||
error: data.error.message || 'Failed to fetch trending videos',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const items = (data.items || []).map((item: any) => ({
|
|
||||||
videoId: item.id ?? '',
|
|
||||||
title: item.snippet?.title ?? '',
|
|
||||||
description: item.snippet?.description ?? '',
|
|
||||||
thumbnail:
|
|
||||||
item.snippet?.thumbnails?.high?.url ||
|
|
||||||
item.snippet?.thumbnails?.medium?.url ||
|
|
||||||
item.snippet?.thumbnails?.default?.url ||
|
|
||||||
'',
|
|
||||||
channelId: item.snippet?.channelId ?? '',
|
|
||||||
channelTitle: item.snippet?.channelTitle ?? '',
|
|
||||||
publishedAt: item.snippet?.publishedAt ?? '',
|
|
||||||
viewCount: Number(item.statistics?.viewCount || 0),
|
|
||||||
likeCount: Number(item.statistics?.likeCount || 0),
|
|
||||||
commentCount: Number(item.statistics?.commentCount || 0),
|
|
||||||
duration: item.contentDetails?.duration ?? '',
|
|
||||||
}))
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
output: {
|
|
||||||
items,
|
|
||||||
totalResults: data.pageInfo?.totalResults || items.length,
|
|
||||||
nextPageToken: data.nextPageToken ?? null,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
outputs: {
|
|
||||||
items: {
|
|
||||||
type: 'array',
|
|
||||||
description: 'Array of trending videos',
|
|
||||||
items: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
videoId: { type: 'string', description: 'YouTube video ID' },
|
|
||||||
title: { type: 'string', description: 'Video title' },
|
|
||||||
description: { type: 'string', description: 'Video description' },
|
|
||||||
thumbnail: { type: 'string', description: 'Video thumbnail URL' },
|
|
||||||
channelId: { type: 'string', description: 'Channel ID' },
|
|
||||||
channelTitle: { type: 'string', description: 'Channel name' },
|
|
||||||
publishedAt: { type: 'string', description: 'Video publish date' },
|
|
||||||
viewCount: { type: 'number', description: 'Number of views' },
|
|
||||||
likeCount: { type: 'number', description: 'Number of likes' },
|
|
||||||
commentCount: { type: 'number', description: 'Number of comments' },
|
|
||||||
duration: { type: 'string', description: 'Video duration in ISO 8601 format' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
totalResults: {
|
|
||||||
type: 'number',
|
|
||||||
description: 'Total number of trending videos available',
|
|
||||||
},
|
|
||||||
nextPageToken: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'Token for accessing the next page of results',
|
|
||||||
optional: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -16,7 +16,6 @@ export interface YouTubeSearchParams {
|
|||||||
regionCode?: string
|
regionCode?: string
|
||||||
relevanceLanguage?: string
|
relevanceLanguage?: string
|
||||||
safeSearch?: 'moderate' | 'none' | 'strict'
|
safeSearch?: 'moderate' | 'none' | 'strict'
|
||||||
eventType?: 'completed' | 'live' | 'upcoming'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface YouTubeSearchResponse extends ToolResponse {
|
export interface YouTubeSearchResponse extends ToolResponse {
|
||||||
@@ -26,13 +25,9 @@ export interface YouTubeSearchResponse extends ToolResponse {
|
|||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
thumbnail: string
|
thumbnail: string
|
||||||
channelId: string
|
|
||||||
channelTitle: string
|
|
||||||
publishedAt: string
|
|
||||||
liveBroadcastContent: string
|
|
||||||
}>
|
}>
|
||||||
totalResults: number
|
totalResults: number
|
||||||
nextPageToken?: string | null
|
nextPageToken?: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,24 +48,8 @@ export interface YouTubeVideoDetailsResponse extends ToolResponse {
|
|||||||
viewCount: number
|
viewCount: number
|
||||||
likeCount: number
|
likeCount: number
|
||||||
commentCount: number
|
commentCount: number
|
||||||
favoriteCount: number
|
|
||||||
thumbnail: string
|
thumbnail: string
|
||||||
tags: string[]
|
tags?: string[]
|
||||||
categoryId: string | null
|
|
||||||
definition: string | null
|
|
||||||
caption: string | null
|
|
||||||
licensedContent: boolean | null
|
|
||||||
privacyStatus: string | null
|
|
||||||
liveBroadcastContent: string | null
|
|
||||||
defaultLanguage: string | null
|
|
||||||
defaultAudioLanguage: string | null
|
|
||||||
// Live streaming details
|
|
||||||
isLiveContent: boolean
|
|
||||||
scheduledStartTime: string | null
|
|
||||||
actualStartTime: string | null
|
|
||||||
actualEndTime: string | null
|
|
||||||
concurrentViewers: number | null
|
|
||||||
activeLiveChatId: string | null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,11 +69,7 @@ export interface YouTubeChannelInfoResponse extends ToolResponse {
|
|||||||
viewCount: number
|
viewCount: number
|
||||||
publishedAt: string
|
publishedAt: string
|
||||||
thumbnail: string
|
thumbnail: string
|
||||||
customUrl: string | null
|
customUrl?: string
|
||||||
country: string | null
|
|
||||||
uploadsPlaylistId: string | null
|
|
||||||
bannerImageUrl: string | null
|
|
||||||
hiddenSubscriberCount: boolean
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,11 +90,9 @@ export interface YouTubePlaylistItemsResponse extends ToolResponse {
|
|||||||
publishedAt: string
|
publishedAt: string
|
||||||
channelTitle: string
|
channelTitle: string
|
||||||
position: number
|
position: number
|
||||||
videoOwnerChannelId: string | null
|
|
||||||
videoOwnerChannelTitle: string | null
|
|
||||||
}>
|
}>
|
||||||
totalResults: number
|
totalResults: number
|
||||||
nextPageToken?: string | null
|
nextPageToken?: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,16 +110,15 @@ export interface YouTubeCommentsResponse extends ToolResponse {
|
|||||||
commentId: string
|
commentId: string
|
||||||
authorDisplayName: string
|
authorDisplayName: string
|
||||||
authorChannelUrl: string
|
authorChannelUrl: string
|
||||||
authorProfileImageUrl: string
|
|
||||||
textDisplay: string
|
textDisplay: string
|
||||||
textOriginal: string
|
textOriginal: string
|
||||||
likeCount: number
|
likeCount: number
|
||||||
publishedAt: string
|
publishedAt: string
|
||||||
updatedAt: string
|
updatedAt: string
|
||||||
replyCount: number
|
replyCount?: number
|
||||||
}>
|
}>
|
||||||
totalResults: number
|
totalResults: number
|
||||||
nextPageToken?: string | null
|
nextPageToken?: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,10 +138,9 @@ export interface YouTubeChannelVideosResponse extends ToolResponse {
|
|||||||
description: string
|
description: string
|
||||||
thumbnail: string
|
thumbnail: string
|
||||||
publishedAt: string
|
publishedAt: string
|
||||||
channelTitle: string
|
|
||||||
}>
|
}>
|
||||||
totalResults: number
|
totalResults: number
|
||||||
nextPageToken?: string | null
|
nextPageToken?: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,55 +160,9 @@ export interface YouTubeChannelPlaylistsResponse extends ToolResponse {
|
|||||||
thumbnail: string
|
thumbnail: string
|
||||||
itemCount: number
|
itemCount: number
|
||||||
publishedAt: string
|
publishedAt: string
|
||||||
channelTitle: string
|
|
||||||
}>
|
|
||||||
totalResults: number
|
|
||||||
nextPageToken?: string | null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface YouTubeTrendingParams {
|
|
||||||
apiKey: string
|
|
||||||
regionCode?: string
|
|
||||||
videoCategoryId?: string
|
|
||||||
maxResults?: number
|
|
||||||
pageToken?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface YouTubeTrendingResponse extends ToolResponse {
|
|
||||||
output: {
|
|
||||||
items: Array<{
|
|
||||||
videoId: string
|
|
||||||
title: string
|
|
||||||
description: string
|
|
||||||
thumbnail: string
|
|
||||||
channelId: string
|
|
||||||
channelTitle: string
|
|
||||||
publishedAt: string
|
|
||||||
viewCount: number
|
|
||||||
likeCount: number
|
|
||||||
commentCount: number
|
|
||||||
duration: string
|
|
||||||
}>
|
|
||||||
totalResults: number
|
|
||||||
nextPageToken?: string | null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface YouTubeVideoCategoriesParams {
|
|
||||||
apiKey: string
|
|
||||||
regionCode?: string
|
|
||||||
hl?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface YouTubeVideoCategoriesResponse extends ToolResponse {
|
|
||||||
output: {
|
|
||||||
items: Array<{
|
|
||||||
categoryId: string
|
|
||||||
title: string
|
|
||||||
assignable: boolean
|
|
||||||
}>
|
}>
|
||||||
totalResults: number
|
totalResults: number
|
||||||
|
nextPageToken?: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,5 +174,3 @@ export type YouTubeResponse =
|
|||||||
| YouTubeCommentsResponse
|
| YouTubeCommentsResponse
|
||||||
| YouTubeChannelVideosResponse
|
| YouTubeChannelVideosResponse
|
||||||
| YouTubeChannelPlaylistsResponse
|
| YouTubeChannelPlaylistsResponse
|
||||||
| YouTubeTrendingResponse
|
|
||||||
| YouTubeVideoCategoriesResponse
|
|
||||||
|
|||||||
@@ -1,108 +0,0 @@
|
|||||||
import type { ToolConfig } from '@/tools/types'
|
|
||||||
import type {
|
|
||||||
YouTubeVideoCategoriesParams,
|
|
||||||
YouTubeVideoCategoriesResponse,
|
|
||||||
} from '@/tools/youtube/types'
|
|
||||||
|
|
||||||
export const youtubeVideoCategoriesTool: ToolConfig<
|
|
||||||
YouTubeVideoCategoriesParams,
|
|
||||||
YouTubeVideoCategoriesResponse
|
|
||||||
> = {
|
|
||||||
id: 'youtube_video_categories',
|
|
||||||
name: 'YouTube Video Categories',
|
|
||||||
description:
|
|
||||||
'Get a list of video categories available on YouTube. Use this to discover valid category IDs for filtering search and trending results.',
|
|
||||||
version: '1.0.0',
|
|
||||||
params: {
|
|
||||||
regionCode: {
|
|
||||||
type: 'string',
|
|
||||||
required: false,
|
|
||||||
visibility: 'user-or-llm',
|
|
||||||
description:
|
|
||||||
'ISO 3166-1 alpha-2 country code to get categories for (e.g., "US", "GB", "JP"). Defaults to US.',
|
|
||||||
},
|
|
||||||
hl: {
|
|
||||||
type: 'string',
|
|
||||||
required: false,
|
|
||||||
visibility: 'user-only',
|
|
||||||
description: 'Language for category titles (e.g., "en", "es", "fr"). Defaults to English.',
|
|
||||||
},
|
|
||||||
apiKey: {
|
|
||||||
type: 'string',
|
|
||||||
required: true,
|
|
||||||
visibility: 'user-only',
|
|
||||||
description: 'YouTube API Key',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
request: {
|
|
||||||
url: (params: YouTubeVideoCategoriesParams) => {
|
|
||||||
let url = `https://www.googleapis.com/youtube/v3/videoCategories?part=snippet&key=${params.apiKey}`
|
|
||||||
url += `®ionCode=${params.regionCode || 'US'}`
|
|
||||||
if (params.hl) {
|
|
||||||
url += `&hl=${params.hl}`
|
|
||||||
}
|
|
||||||
return url
|
|
||||||
},
|
|
||||||
method: 'GET',
|
|
||||||
headers: () => ({
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
|
|
||||||
transformResponse: async (response: Response): Promise<YouTubeVideoCategoriesResponse> => {
|
|
||||||
const data = await response.json()
|
|
||||||
|
|
||||||
if (data.error) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
output: {
|
|
||||||
items: [],
|
|
||||||
totalResults: 0,
|
|
||||||
},
|
|
||||||
error: data.error.message || 'Failed to fetch video categories',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const items = (data.items || [])
|
|
||||||
.filter((item: any) => item.snippet?.assignable !== false)
|
|
||||||
.map((item: any) => ({
|
|
||||||
categoryId: item.id ?? '',
|
|
||||||
title: item.snippet?.title ?? '',
|
|
||||||
assignable: item.snippet?.assignable ?? false,
|
|
||||||
}))
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
output: {
|
|
||||||
items,
|
|
||||||
totalResults: items.length,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
outputs: {
|
|
||||||
items: {
|
|
||||||
type: 'array',
|
|
||||||
description: 'Array of video categories available in the specified region',
|
|
||||||
items: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
categoryId: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'Category ID to use in search/trending filters (e.g., "10" for Music)',
|
|
||||||
},
|
|
||||||
title: { type: 'string', description: 'Human-readable category name' },
|
|
||||||
assignable: {
|
|
||||||
type: 'boolean',
|
|
||||||
description: 'Whether videos can be tagged with this category',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
totalResults: {
|
|
||||||
type: 'number',
|
|
||||||
description: 'Total number of categories available',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -7,9 +7,8 @@ export const youtubeVideoDetailsTool: ToolConfig<
|
|||||||
> = {
|
> = {
|
||||||
id: 'youtube_video_details',
|
id: 'youtube_video_details',
|
||||||
name: 'YouTube Video Details',
|
name: 'YouTube Video Details',
|
||||||
description:
|
description: 'Get detailed information about a specific YouTube video.',
|
||||||
'Get detailed information about a specific YouTube video including statistics, content details, live streaming info, and metadata.',
|
version: '1.0.0',
|
||||||
version: '1.2.0',
|
|
||||||
params: {
|
params: {
|
||||||
videoId: {
|
videoId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
@@ -27,7 +26,7 @@ export const youtubeVideoDetailsTool: ToolConfig<
|
|||||||
|
|
||||||
request: {
|
request: {
|
||||||
url: (params: YouTubeVideoDetailsParams) => {
|
url: (params: YouTubeVideoDetailsParams) => {
|
||||||
return `https://www.googleapis.com/youtube/v3/videos?part=snippet,statistics,contentDetails,status,liveStreamingDetails&id=${encodeURIComponent(params.videoId)}&key=${params.apiKey}`
|
return `https://www.googleapis.com/youtube/v3/videos?part=snippet,statistics,contentDetails&id=${params.videoId}&key=${params.apiKey}`
|
||||||
},
|
},
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: () => ({
|
headers: () => ({
|
||||||
@@ -52,68 +51,32 @@ export const youtubeVideoDetailsTool: ToolConfig<
|
|||||||
viewCount: 0,
|
viewCount: 0,
|
||||||
likeCount: 0,
|
likeCount: 0,
|
||||||
commentCount: 0,
|
commentCount: 0,
|
||||||
favoriteCount: 0,
|
|
||||||
thumbnail: '',
|
thumbnail: '',
|
||||||
tags: [],
|
|
||||||
categoryId: null,
|
|
||||||
definition: null,
|
|
||||||
caption: null,
|
|
||||||
licensedContent: null,
|
|
||||||
privacyStatus: null,
|
|
||||||
liveBroadcastContent: null,
|
|
||||||
defaultLanguage: null,
|
|
||||||
defaultAudioLanguage: null,
|
|
||||||
isLiveContent: false,
|
|
||||||
scheduledStartTime: null,
|
|
||||||
actualStartTime: null,
|
|
||||||
actualEndTime: null,
|
|
||||||
concurrentViewers: null,
|
|
||||||
activeLiveChatId: null,
|
|
||||||
},
|
},
|
||||||
error: 'Video not found',
|
error: 'Video not found',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const item = data.items[0]
|
const item = data.items[0]
|
||||||
const liveDetails = item.liveStreamingDetails
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
output: {
|
output: {
|
||||||
videoId: item.id ?? '',
|
videoId: item.id,
|
||||||
title: item.snippet?.title ?? '',
|
title: item.snippet?.title || '',
|
||||||
description: item.snippet?.description ?? '',
|
description: item.snippet?.description || '',
|
||||||
channelId: item.snippet?.channelId ?? '',
|
channelId: item.snippet?.channelId || '',
|
||||||
channelTitle: item.snippet?.channelTitle ?? '',
|
channelTitle: item.snippet?.channelTitle || '',
|
||||||
publishedAt: item.snippet?.publishedAt ?? '',
|
publishedAt: item.snippet?.publishedAt || '',
|
||||||
duration: item.contentDetails?.duration ?? '',
|
duration: item.contentDetails?.duration || '',
|
||||||
viewCount: Number(item.statistics?.viewCount || 0),
|
viewCount: Number(item.statistics?.viewCount || 0),
|
||||||
likeCount: Number(item.statistics?.likeCount || 0),
|
likeCount: Number(item.statistics?.likeCount || 0),
|
||||||
commentCount: Number(item.statistics?.commentCount || 0),
|
commentCount: Number(item.statistics?.commentCount || 0),
|
||||||
favoriteCount: Number(item.statistics?.favoriteCount || 0),
|
|
||||||
thumbnail:
|
thumbnail:
|
||||||
item.snippet?.thumbnails?.high?.url ||
|
item.snippet?.thumbnails?.high?.url ||
|
||||||
item.snippet?.thumbnails?.medium?.url ||
|
item.snippet?.thumbnails?.medium?.url ||
|
||||||
item.snippet?.thumbnails?.default?.url ||
|
item.snippet?.thumbnails?.default?.url ||
|
||||||
'',
|
'',
|
||||||
tags: item.snippet?.tags ?? [],
|
tags: item.snippet?.tags || [],
|
||||||
categoryId: item.snippet?.categoryId ?? null,
|
|
||||||
definition: item.contentDetails?.definition ?? null,
|
|
||||||
caption: item.contentDetails?.caption ?? null,
|
|
||||||
licensedContent: item.contentDetails?.licensedContent ?? null,
|
|
||||||
privacyStatus: item.status?.privacyStatus ?? null,
|
|
||||||
liveBroadcastContent: item.snippet?.liveBroadcastContent ?? null,
|
|
||||||
defaultLanguage: item.snippet?.defaultLanguage ?? null,
|
|
||||||
defaultAudioLanguage: item.snippet?.defaultAudioLanguage ?? null,
|
|
||||||
// Live streaming details
|
|
||||||
isLiveContent: liveDetails !== undefined,
|
|
||||||
scheduledStartTime: liveDetails?.scheduledStartTime ?? null,
|
|
||||||
actualStartTime: liveDetails?.actualStartTime ?? null,
|
|
||||||
actualEndTime: liveDetails?.actualEndTime ?? null,
|
|
||||||
concurrentViewers: liveDetails?.concurrentViewers
|
|
||||||
? Number(liveDetails.concurrentViewers)
|
|
||||||
: null,
|
|
||||||
activeLiveChatId: liveDetails?.activeLiveChatId ?? null,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -145,7 +108,7 @@ export const youtubeVideoDetailsTool: ToolConfig<
|
|||||||
},
|
},
|
||||||
duration: {
|
duration: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Video duration in ISO 8601 format (e.g., "PT4M13S" for 4 min 13 sec)',
|
description: 'Video duration in ISO 8601 format',
|
||||||
},
|
},
|
||||||
viewCount: {
|
viewCount: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
@@ -159,10 +122,6 @@ export const youtubeVideoDetailsTool: ToolConfig<
|
|||||||
type: 'number',
|
type: 'number',
|
||||||
description: 'Number of comments',
|
description: 'Number of comments',
|
||||||
},
|
},
|
||||||
favoriteCount: {
|
|
||||||
type: 'number',
|
|
||||||
description: 'Number of times added to favorites',
|
|
||||||
},
|
|
||||||
thumbnail: {
|
thumbnail: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Video thumbnail URL',
|
description: 'Video thumbnail URL',
|
||||||
@@ -173,74 +132,6 @@ export const youtubeVideoDetailsTool: ToolConfig<
|
|||||||
items: {
|
items: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
},
|
},
|
||||||
},
|
|
||||||
categoryId: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'YouTube video category ID',
|
|
||||||
optional: true,
|
|
||||||
},
|
|
||||||
definition: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'Video definition: "hd" or "sd"',
|
|
||||||
optional: true,
|
|
||||||
},
|
|
||||||
caption: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'Whether captions are available: "true" or "false"',
|
|
||||||
optional: true,
|
|
||||||
},
|
|
||||||
licensedContent: {
|
|
||||||
type: 'boolean',
|
|
||||||
description: 'Whether the video is licensed content',
|
|
||||||
optional: true,
|
|
||||||
},
|
|
||||||
privacyStatus: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'Video privacy status: "public", "private", or "unlisted"',
|
|
||||||
optional: true,
|
|
||||||
},
|
|
||||||
liveBroadcastContent: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'Live broadcast status: "live", "upcoming", or "none"',
|
|
||||||
optional: true,
|
|
||||||
},
|
|
||||||
defaultLanguage: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'Default language of the video metadata',
|
|
||||||
optional: true,
|
|
||||||
},
|
|
||||||
defaultAudioLanguage: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'Default audio language of the video',
|
|
||||||
optional: true,
|
|
||||||
},
|
|
||||||
isLiveContent: {
|
|
||||||
type: 'boolean',
|
|
||||||
description: 'Whether this video is or was a live stream',
|
|
||||||
},
|
|
||||||
scheduledStartTime: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'Scheduled start time for upcoming live streams (ISO 8601)',
|
|
||||||
optional: true,
|
|
||||||
},
|
|
||||||
actualStartTime: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'When the live stream actually started (ISO 8601)',
|
|
||||||
optional: true,
|
|
||||||
},
|
|
||||||
actualEndTime: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'When the live stream ended (ISO 8601)',
|
|
||||||
optional: true,
|
|
||||||
},
|
|
||||||
concurrentViewers: {
|
|
||||||
type: 'number',
|
|
||||||
description: 'Current number of viewers (only for active live streams)',
|
|
||||||
optional: true,
|
|
||||||
},
|
|
||||||
activeLiveChatId: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'Live chat ID for the stream (only for active live streams)',
|
|
||||||
optional: true,
|
optional: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
ALTER TABLE "workflow_execution_logs" DROP CONSTRAINT "workflow_execution_logs_workflow_id_workflow_id_fk";
|
|
||||||
--> statement-breakpoint
|
|
||||||
ALTER TABLE "workflow_execution_snapshots" DROP CONSTRAINT "workflow_execution_snapshots_workflow_id_workflow_id_fk";
|
|
||||||
--> statement-breakpoint
|
|
||||||
ALTER TABLE "workflow_execution_logs" ALTER COLUMN "workflow_id" DROP NOT NULL;--> statement-breakpoint
|
|
||||||
ALTER TABLE "workflow_execution_snapshots" ALTER COLUMN "workflow_id" DROP NOT NULL;--> statement-breakpoint
|
|
||||||
ALTER TABLE "workflow_execution_logs" ADD CONSTRAINT "workflow_execution_logs_workflow_id_workflow_id_fk" FOREIGN KEY ("workflow_id") REFERENCES "public"."workflow"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "workflow_execution_snapshots" ADD CONSTRAINT "workflow_execution_snapshots_workflow_id_workflow_id_fk" FOREIGN KEY ("workflow_id") REFERENCES "public"."workflow"("id") ON DELETE set null ON UPDATE no action;
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1037,13 +1037,6 @@
|
|||||||
"when": 1769626313827,
|
"when": 1769626313827,
|
||||||
"tag": "0148_aberrant_venom",
|
"tag": "0148_aberrant_venom",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 149,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1769656977701,
|
|
||||||
"tag": "0149_next_cerise",
|
|
||||||
"breakpoints": true
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -268,7 +268,9 @@ export const workflowExecutionSnapshots = pgTable(
|
|||||||
'workflow_execution_snapshots',
|
'workflow_execution_snapshots',
|
||||||
{
|
{
|
||||||
id: text('id').primaryKey(),
|
id: text('id').primaryKey(),
|
||||||
workflowId: text('workflow_id').references(() => workflow.id, { onDelete: 'set null' }),
|
workflowId: text('workflow_id')
|
||||||
|
.notNull()
|
||||||
|
.references(() => workflow.id, { onDelete: 'cascade' }),
|
||||||
stateHash: text('state_hash').notNull(),
|
stateHash: text('state_hash').notNull(),
|
||||||
stateData: jsonb('state_data').notNull(),
|
stateData: jsonb('state_data').notNull(),
|
||||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||||
@@ -288,7 +290,9 @@ export const workflowExecutionLogs = pgTable(
|
|||||||
'workflow_execution_logs',
|
'workflow_execution_logs',
|
||||||
{
|
{
|
||||||
id: text('id').primaryKey(),
|
id: text('id').primaryKey(),
|
||||||
workflowId: text('workflow_id').references(() => workflow.id, { onDelete: 'set null' }),
|
workflowId: text('workflow_id')
|
||||||
|
.notNull()
|
||||||
|
.references(() => workflow.id, { onDelete: 'cascade' }),
|
||||||
workspaceId: text('workspace_id')
|
workspaceId: text('workspace_id')
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => workspace.id, { onDelete: 'cascade' }),
|
.references(() => workspace.id, { onDelete: 'cascade' }),
|
||||||
|
|||||||
Reference in New Issue
Block a user