mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-28 03:00:29 -04:00
Restore and mass delete tools
This commit is contained in:
@@ -70,6 +70,7 @@ export interface ToolCatalogEntry {
|
||||
| 'rename_workflow'
|
||||
| 'research'
|
||||
| 'respond'
|
||||
| 'restore_resource'
|
||||
| 'revert_to_version'
|
||||
| 'run'
|
||||
| 'run_block'
|
||||
@@ -158,6 +159,7 @@ export interface ToolCatalogEntry {
|
||||
| 'rename_workflow'
|
||||
| 'research'
|
||||
| 'respond'
|
||||
| 'restore_resource'
|
||||
| 'revert_to_version'
|
||||
| 'run'
|
||||
| 'run_block'
|
||||
@@ -479,9 +481,13 @@ export const DeleteFile: ToolCatalogEntry = {
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
fileId: { type: 'string', description: 'Canonical workspace file ID of the file to delete.' },
|
||||
fileIds: {
|
||||
type: 'array',
|
||||
description: 'Canonical workspace file IDs of the files to delete.',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
},
|
||||
required: ['fileId'],
|
||||
required: ['fileIds'],
|
||||
},
|
||||
resultSchema: {
|
||||
type: 'object',
|
||||
@@ -501,8 +507,14 @@ export const DeleteFolder: ToolCatalogEntry = {
|
||||
mode: 'async',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: { folderId: { type: 'string', description: 'The folder ID to delete.' } },
|
||||
required: ['folderId'],
|
||||
properties: {
|
||||
folderIds: {
|
||||
type: 'array',
|
||||
description: 'The folder IDs to delete.',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
},
|
||||
required: ['folderIds'],
|
||||
},
|
||||
requiresConfirmation: true,
|
||||
requiredPermission: 'write',
|
||||
@@ -515,8 +527,14 @@ export const DeleteWorkflow: ToolCatalogEntry = {
|
||||
mode: 'async',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: { workflowId: { type: 'string', description: 'The workflow ID to delete.' } },
|
||||
required: ['workflowId'],
|
||||
properties: {
|
||||
workflowIds: {
|
||||
type: 'array',
|
||||
description: 'The workflow IDs to delete.',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
},
|
||||
required: ['workflowIds'],
|
||||
},
|
||||
requiresConfirmation: true,
|
||||
requiredPermission: 'write',
|
||||
@@ -1176,7 +1194,7 @@ export const Glob: ToolCatalogEntry = {
|
||||
'Optional target-only UI phrase for the search row. The UI verb is supplied for you, so pass text like "workflow configs" or "knowledge bases", not a full sentence like "Finding workflow configs".',
|
||||
},
|
||||
},
|
||||
required: ['pattern'],
|
||||
required: ['pattern', 'toolTitle'],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1221,7 +1239,7 @@ export const Grep: ToolCatalogEntry = {
|
||||
'Optional target-only UI phrase for the search row. The UI verb is supplied for you, so pass text like "Slack integrations" or "deployed workflows", not a full sentence like "Searching for Slack integrations".',
|
||||
},
|
||||
},
|
||||
required: ['pattern'],
|
||||
required: ['pattern', 'toolTitle'],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1322,22 +1340,21 @@ export const KnowledgeBase: ToolCatalogEntry = {
|
||||
description:
|
||||
'Tag definition IDs to opt out of (optional for add_connector). See tagDefinitions in the connector schema.',
|
||||
},
|
||||
documentId: {
|
||||
type: 'string',
|
||||
description: 'Document ID (required for delete_document, update_document)',
|
||||
documentId: { type: 'string', description: 'Document ID (required for update_document)' },
|
||||
documentIds: {
|
||||
type: 'array',
|
||||
description: 'Document IDs (for batch delete_document)',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
enabled: {
|
||||
type: 'boolean',
|
||||
description: 'Enable/disable a document (optional for update_document)',
|
||||
},
|
||||
fileId: {
|
||||
type: 'string',
|
||||
fileIds: {
|
||||
type: 'array',
|
||||
description:
|
||||
'Canonical workspace file ID to add as a document (preferred for add_file). Discover via read("files/{name}/meta.json") or glob("files/by-id/*/meta.json").',
|
||||
},
|
||||
filePath: {
|
||||
type: 'string',
|
||||
description: 'Legacy workspace file reference for add_file. Prefer fileId.',
|
||||
'Canonical workspace file IDs to add as documents (for add_file). Discover via read("files/{name}/meta.json") or glob("files/by-id/*/meta.json").',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
filename: {
|
||||
type: 'string',
|
||||
@@ -1348,6 +1365,11 @@ export const KnowledgeBase: ToolCatalogEntry = {
|
||||
description:
|
||||
'Knowledge base ID (required for get, query, add_file, list_tags, create_tag, get_tag_usage)',
|
||||
},
|
||||
knowledgeBaseIds: {
|
||||
type: 'array',
|
||||
description: 'Knowledge base IDs (for batch delete)',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
description: "Name of the knowledge base (required for 'create')",
|
||||
@@ -1470,9 +1492,11 @@ export const ManageCredential: ToolCatalogEntry = {
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
credentialId: {
|
||||
type: 'string',
|
||||
description: 'The credential ID (from environment/credentials.json)',
|
||||
credentialId: { type: 'string', description: 'The credential ID (required for rename)' },
|
||||
credentialIds: {
|
||||
type: 'array',
|
||||
description: 'Array of credential IDs (for batch delete)',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
displayName: { type: 'string', description: 'New display name (required for rename)' },
|
||||
operation: {
|
||||
@@ -1481,7 +1505,7 @@ export const ManageCredential: ToolCatalogEntry = {
|
||||
enum: ['rename', 'delete'],
|
||||
},
|
||||
},
|
||||
required: ['operation', 'credentialId'],
|
||||
required: ['operation'],
|
||||
},
|
||||
requiresConfirmation: true,
|
||||
requiredPermission: 'admin',
|
||||
@@ -1542,7 +1566,12 @@ export const ManageCustomTool: ToolCatalogEntry = {
|
||||
toolId: {
|
||||
type: 'string',
|
||||
description:
|
||||
"The ID of the custom tool (required for edit/delete). Must be the exact toolId from the get_workflow_data custom tool response - do not guess or construct it. DO NOT PROVIDE THE TOOL ID IF THE OPERATION IS 'ADD'.",
|
||||
"The ID of the custom tool (required for edit). Must be the exact toolId from the get_workflow_data custom tool response - do not guess or construct it. DO NOT PROVIDE THE TOOL ID IF THE OPERATION IS 'ADD'.",
|
||||
},
|
||||
toolIds: {
|
||||
type: 'array',
|
||||
description: 'Array of custom tool IDs (for batch delete)',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
},
|
||||
required: ['operation'],
|
||||
@@ -1564,7 +1593,12 @@ export const ManageJob: ToolCatalogEntry = {
|
||||
'Operation-specific arguments. For create: {title, prompt, cron?, time?, timezone?, lifecycle?, successCondition?, maxRuns?}. For get/delete: {jobId}. For update: {jobId, title?, prompt?, cron?, timezone?, status?, lifecycle?, successCondition?, maxRuns?}. For list: no args needed.',
|
||||
properties: {
|
||||
cron: { type: 'string', description: 'Cron expression for recurring jobs' },
|
||||
jobId: { type: 'string', description: 'Job ID (required for get, update, delete)' },
|
||||
jobId: { type: 'string', description: 'Job ID (required for get, update)' },
|
||||
jobIds: {
|
||||
type: 'array',
|
||||
description: 'Array of job IDs (for batch delete)',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
lifecycle: {
|
||||
type: 'string',
|
||||
description:
|
||||
@@ -1702,9 +1736,11 @@ export const MaterializeFile: ToolCatalogEntry = {
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
fileName: {
|
||||
type: 'string',
|
||||
description: 'The name of the uploaded file to materialize (e.g. "report.pdf")',
|
||||
fileNames: {
|
||||
type: 'array',
|
||||
description:
|
||||
'The names of the uploaded files to materialize (e.g. ["report.pdf", "data.csv"])',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
knowledgeBaseId: {
|
||||
type: 'string',
|
||||
@@ -1724,7 +1760,7 @@ export const MaterializeFile: ToolCatalogEntry = {
|
||||
'Custom name for the table (only used with operation "table"). Defaults to the file name without extension.',
|
||||
},
|
||||
},
|
||||
required: ['fileName'],
|
||||
required: ['fileNames'],
|
||||
},
|
||||
requiredPermission: 'write',
|
||||
}
|
||||
@@ -1761,9 +1797,13 @@ export const MoveWorkflow: ToolCatalogEntry = {
|
||||
type: 'string',
|
||||
description: 'Target folder ID. Omit or pass empty string to move to workspace root.',
|
||||
},
|
||||
workflowId: { type: 'string', description: 'The workflow ID to move.' },
|
||||
workflowIds: {
|
||||
type: 'array',
|
||||
description: 'The workflow IDs to move.',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
},
|
||||
required: ['workflowId'],
|
||||
required: ['workflowIds'],
|
||||
},
|
||||
requiredPermission: 'write',
|
||||
}
|
||||
@@ -1813,14 +1853,24 @@ export const OpenResource: ToolCatalogEntry = {
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'The resource ID to open.' },
|
||||
type: {
|
||||
type: 'string',
|
||||
description: 'The resource type to open.',
|
||||
enum: ['workflow', 'table', 'knowledgebase', 'file'],
|
||||
resources: {
|
||||
type: 'array',
|
||||
description: 'Array of resources to open. Each item must have type and id.',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'The resource ID.' },
|
||||
type: {
|
||||
type: 'string',
|
||||
description: 'The resource type.',
|
||||
enum: ['workflow', 'table', 'knowledgebase', 'file'],
|
||||
},
|
||||
},
|
||||
required: ['type', 'id'],
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ['type', 'id'],
|
||||
required: ['resources'],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1948,6 +1998,27 @@ export const Respond: ToolCatalogEntry = {
|
||||
hidden: true,
|
||||
}
|
||||
|
||||
export const RestoreResource: ToolCatalogEntry = {
|
||||
id: 'restore_resource',
|
||||
name: 'restore_resource',
|
||||
executor: 'sim',
|
||||
mode: 'async',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'The canonical resource ID to restore.' },
|
||||
type: {
|
||||
type: 'string',
|
||||
description: 'The resource type to restore.',
|
||||
enum: ['workflow', 'table', 'file', 'knowledgebase', 'folder'],
|
||||
},
|
||||
},
|
||||
required: ['type', 'id'],
|
||||
},
|
||||
requiresConfirmation: true,
|
||||
requiredPermission: 'admin',
|
||||
}
|
||||
|
||||
export const RevertToVersion: ToolCatalogEntry = {
|
||||
id: 'revert_to_version',
|
||||
name: 'revert_to_version',
|
||||
@@ -2208,7 +2279,7 @@ export const SearchOnline: ToolCatalogEntry = {
|
||||
'Optional target-only UI phrase for the search row. The UI verb is supplied for you, so pass text like "pricing changes" or "Slack webhook docs", not a full sentence like "Searching online for pricing changes".',
|
||||
},
|
||||
},
|
||||
required: ['query'],
|
||||
required: ['query', 'toolTitle'],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2534,7 +2605,13 @@ export const UserTable: ToolCatalogEntry = {
|
||||
},
|
||||
tableId: {
|
||||
type: 'string',
|
||||
description: "Table ID (required for most operations except 'create')",
|
||||
description:
|
||||
"Table ID (required for most operations except 'create' and batch 'delete')",
|
||||
},
|
||||
tableIds: {
|
||||
type: 'array',
|
||||
description: 'Array of table IDs (for batch delete)',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
unique: {
|
||||
type: 'boolean',
|
||||
@@ -3030,6 +3107,7 @@ export const TOOL_CATALOG: Record<string, ToolCatalogEntry> = {
|
||||
[RenameWorkflow.id]: RenameWorkflow,
|
||||
[Research.id]: Research,
|
||||
[Respond.id]: Respond,
|
||||
[RestoreResource.id]: RestoreResource,
|
||||
[RevertToVersion.id]: RevertToVersion,
|
||||
[Run.id]: Run,
|
||||
[RunBlock.id]: RunBlock,
|
||||
|
||||
@@ -289,12 +289,15 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
fileId: {
|
||||
type: 'string',
|
||||
description: 'Canonical workspace file ID of the file to delete.',
|
||||
fileIds: {
|
||||
type: 'array',
|
||||
description: 'Canonical workspace file IDs of the files to delete.',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ['fileId'],
|
||||
required: ['fileIds'],
|
||||
},
|
||||
resultSchema: {
|
||||
type: 'object',
|
||||
@@ -315,12 +318,15 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
folderId: {
|
||||
type: 'string',
|
||||
description: 'The folder ID to delete.',
|
||||
folderIds: {
|
||||
type: 'array',
|
||||
description: 'The folder IDs to delete.',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ['folderId'],
|
||||
required: ['folderIds'],
|
||||
},
|
||||
resultSchema: undefined,
|
||||
},
|
||||
@@ -328,12 +334,15 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
workflowId: {
|
||||
type: 'string',
|
||||
description: 'The workflow ID to delete.',
|
||||
workflowIds: {
|
||||
type: 'array',
|
||||
description: 'The workflow IDs to delete.',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ['workflowId'],
|
||||
required: ['workflowIds'],
|
||||
},
|
||||
resultSchema: undefined,
|
||||
},
|
||||
@@ -966,7 +975,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
|
||||
'Optional target-only UI phrase for the search row. The UI verb is supplied for you, so pass text like "workflow configs" or "knowledge bases", not a full sentence like "Finding workflow configs".',
|
||||
},
|
||||
},
|
||||
required: ['pattern'],
|
||||
required: ['pattern', 'toolTitle'],
|
||||
},
|
||||
resultSchema: undefined,
|
||||
},
|
||||
@@ -1013,7 +1022,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
|
||||
'Optional target-only UI phrase for the search row. The UI verb is supplied for you, so pass text like "Slack integrations" or "deployed workflows", not a full sentence like "Searching for Slack integrations".',
|
||||
},
|
||||
},
|
||||
required: ['pattern'],
|
||||
required: ['pattern', 'toolTitle'],
|
||||
},
|
||||
resultSchema: undefined,
|
||||
},
|
||||
@@ -1108,20 +1117,26 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
|
||||
},
|
||||
documentId: {
|
||||
type: 'string',
|
||||
description: 'Document ID (required for delete_document, update_document)',
|
||||
description: 'Document ID (required for update_document)',
|
||||
},
|
||||
documentIds: {
|
||||
type: 'array',
|
||||
description: 'Document IDs (for batch delete_document)',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
enabled: {
|
||||
type: 'boolean',
|
||||
description: 'Enable/disable a document (optional for update_document)',
|
||||
},
|
||||
fileId: {
|
||||
type: 'string',
|
||||
fileIds: {
|
||||
type: 'array',
|
||||
description:
|
||||
'Canonical workspace file ID to add as a document (preferred for add_file). Discover via read("files/{name}/meta.json") or glob("files/by-id/*/meta.json").',
|
||||
},
|
||||
filePath: {
|
||||
type: 'string',
|
||||
description: 'Legacy workspace file reference for add_file. Prefer fileId.',
|
||||
'Canonical workspace file IDs to add as documents (for add_file). Discover via read("files/{name}/meta.json") or glob("files/by-id/*/meta.json").',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
filename: {
|
||||
type: 'string',
|
||||
@@ -1132,6 +1147,13 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
|
||||
description:
|
||||
'Knowledge base ID (required for get, query, add_file, list_tags, create_tag, get_tag_usage)',
|
||||
},
|
||||
knowledgeBaseIds: {
|
||||
type: 'array',
|
||||
description: 'Knowledge base IDs (for batch delete)',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
description: "Name of the knowledge base (required for 'create')",
|
||||
@@ -1259,7 +1281,14 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
|
||||
properties: {
|
||||
credentialId: {
|
||||
type: 'string',
|
||||
description: 'The credential ID (from environment/credentials.json)',
|
||||
description: 'The credential ID (required for rename)',
|
||||
},
|
||||
credentialIds: {
|
||||
type: 'array',
|
||||
description: 'Array of credential IDs (for batch delete)',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
displayName: {
|
||||
type: 'string',
|
||||
@@ -1271,7 +1300,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
|
||||
enum: ['rename', 'delete'],
|
||||
},
|
||||
},
|
||||
required: ['operation', 'credentialId'],
|
||||
required: ['operation'],
|
||||
},
|
||||
resultSchema: undefined,
|
||||
},
|
||||
@@ -1340,7 +1369,14 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
|
||||
toolId: {
|
||||
type: 'string',
|
||||
description:
|
||||
"The ID of the custom tool (required for edit/delete). Must be the exact toolId from the get_workflow_data custom tool response - do not guess or construct it. DO NOT PROVIDE THE TOOL ID IF THE OPERATION IS 'ADD'.",
|
||||
"The ID of the custom tool (required for edit). Must be the exact toolId from the get_workflow_data custom tool response - do not guess or construct it. DO NOT PROVIDE THE TOOL ID IF THE OPERATION IS 'ADD'.",
|
||||
},
|
||||
toolIds: {
|
||||
type: 'array',
|
||||
description: 'Array of custom tool IDs (for batch delete)',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ['operation'],
|
||||
@@ -1362,7 +1398,14 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
|
||||
},
|
||||
jobId: {
|
||||
type: 'string',
|
||||
description: 'Job ID (required for get, update, delete)',
|
||||
description: 'Job ID (required for get, update)',
|
||||
},
|
||||
jobIds: {
|
||||
type: 'array',
|
||||
description: 'Array of job IDs (for batch delete)',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
lifecycle: {
|
||||
type: 'string',
|
||||
@@ -1497,9 +1540,13 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
fileName: {
|
||||
type: 'string',
|
||||
description: 'The name of the uploaded file to materialize (e.g. "report.pdf")',
|
||||
fileNames: {
|
||||
type: 'array',
|
||||
description:
|
||||
'The names of the uploaded files to materialize (e.g. ["report.pdf", "data.csv"])',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
knowledgeBaseId: {
|
||||
type: 'string',
|
||||
@@ -1519,7 +1566,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
|
||||
'Custom name for the table (only used with operation "table"). Defaults to the file name without extension.',
|
||||
},
|
||||
},
|
||||
required: ['fileName'],
|
||||
required: ['fileNames'],
|
||||
},
|
||||
resultSchema: undefined,
|
||||
},
|
||||
@@ -1549,12 +1596,15 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
|
||||
type: 'string',
|
||||
description: 'Target folder ID. Omit or pass empty string to move to workspace root.',
|
||||
},
|
||||
workflowId: {
|
||||
type: 'string',
|
||||
description: 'The workflow ID to move.',
|
||||
workflowIds: {
|
||||
type: 'array',
|
||||
description: 'The workflow IDs to move.',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ['workflowId'],
|
||||
required: ['workflowIds'],
|
||||
},
|
||||
resultSchema: undefined,
|
||||
},
|
||||
@@ -1590,17 +1640,27 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: 'The resource ID to open.',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
description: 'The resource type to open.',
|
||||
enum: ['workflow', 'table', 'knowledgebase', 'file'],
|
||||
resources: {
|
||||
type: 'array',
|
||||
description: 'Array of resources to open. Each item must have type and id.',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: 'The resource ID.',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
description: 'The resource type.',
|
||||
enum: ['workflow', 'table', 'knowledgebase', 'file'],
|
||||
},
|
||||
},
|
||||
required: ['type', 'id'],
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ['type', 'id'],
|
||||
required: ['resources'],
|
||||
},
|
||||
resultSchema: undefined,
|
||||
},
|
||||
@@ -1730,6 +1790,24 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
|
||||
},
|
||||
resultSchema: undefined,
|
||||
},
|
||||
restore_resource: {
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: 'The canonical resource ID to restore.',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
description: 'The resource type to restore.',
|
||||
enum: ['workflow', 'table', 'file', 'knowledgebase', 'folder'],
|
||||
},
|
||||
},
|
||||
required: ['type', 'id'],
|
||||
},
|
||||
resultSchema: undefined,
|
||||
},
|
||||
revert_to_version: {
|
||||
parameters: {
|
||||
type: 'object',
|
||||
@@ -1974,7 +2052,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
|
||||
'Optional target-only UI phrase for the search row. The UI verb is supplied for you, so pass text like "pricing changes" or "Slack webhook docs", not a full sentence like "Searching online for pricing changes".',
|
||||
},
|
||||
},
|
||||
required: ['query'],
|
||||
required: ['query', 'toolTitle'],
|
||||
},
|
||||
resultSchema: undefined,
|
||||
},
|
||||
@@ -2305,7 +2383,15 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
|
||||
},
|
||||
tableId: {
|
||||
type: 'string',
|
||||
description: "Table ID (required for most operations except 'create')",
|
||||
description:
|
||||
"Table ID (required for most operations except 'create' and batch 'delete')",
|
||||
},
|
||||
tableIds: {
|
||||
type: 'array',
|
||||
description: 'Array of table IDs (for batch delete)',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
unique: {
|
||||
type: 'boolean',
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
Read as ReadTool,
|
||||
Redeploy,
|
||||
RenameWorkflow,
|
||||
RestoreResource,
|
||||
RevertToVersion,
|
||||
RunBlock,
|
||||
RunFromBlock,
|
||||
@@ -80,6 +81,7 @@ import { executeMaterializeFile } from '../tools/handlers/materialize-file'
|
||||
import { executeOAuthGetAuthLink, executeOAuthRequestAccess } from '../tools/handlers/oauth'
|
||||
import { executeGetPlatformActions } from '../tools/handlers/platform'
|
||||
import { executeOpenResource } from '../tools/handlers/resources'
|
||||
import { executeRestoreResource } from '../tools/handlers/restore-resource'
|
||||
import { executeVfsGlob, executeVfsGrep, executeVfsRead } from '../tools/handlers/vfs'
|
||||
import {
|
||||
executeCreateFolder,
|
||||
@@ -176,6 +178,7 @@ function buildHandlerMap(): Record<string, ToolHandler> {
|
||||
[OauthGetAuthLink.id]: h(executeOAuthGetAuthLink),
|
||||
[OauthRequestAccess.id]: h(executeOAuthRequestAccess),
|
||||
[OpenResource.id]: h(executeOpenResource),
|
||||
[RestoreResource.id]: h(executeRestoreResource),
|
||||
[GetPlatformActions.id]: h(executeGetPlatformActions),
|
||||
[MaterializeFile.id]: h(executeMaterializeFile),
|
||||
[FunctionExecute.id]: h(executeFunctionExecute),
|
||||
|
||||
@@ -184,6 +184,7 @@ interface ManageJobParams {
|
||||
operation: 'create' | 'list' | 'get' | 'update' | 'delete'
|
||||
args?: {
|
||||
jobId?: string
|
||||
jobIds?: string[]
|
||||
title?: string
|
||||
prompt?: string
|
||||
cron?: string
|
||||
@@ -423,43 +424,46 @@ export async function executeManageJob(
|
||||
}
|
||||
|
||||
case 'delete': {
|
||||
if (!args?.jobId) {
|
||||
return { success: false, error: 'jobId is required for delete operation' }
|
||||
const jobIds = args?.jobIds ?? (args?.jobId ? [args.jobId] : [])
|
||||
if (jobIds.length === 0) {
|
||||
return { success: false, error: 'jobId or jobIds is required for delete operation' }
|
||||
}
|
||||
|
||||
try {
|
||||
const [existing] = await db
|
||||
.select({ id: workflowSchedule.id })
|
||||
.from(workflowSchedule)
|
||||
.where(
|
||||
and(eq(workflowSchedule.id, args.jobId), ACTIVE_JOB_CONDITION(context.workspaceId))
|
||||
)
|
||||
.limit(1)
|
||||
const deleted: string[] = []
|
||||
const notFound: string[] = []
|
||||
|
||||
if (!existing) {
|
||||
return { success: false, error: `Job not found: ${args.jobId}` }
|
||||
for (const jobId of jobIds) {
|
||||
const [existing] = await db
|
||||
.select({ id: workflowSchedule.id })
|
||||
.from(workflowSchedule)
|
||||
.where(and(eq(workflowSchedule.id, jobId), ACTIVE_JOB_CONDITION(context.workspaceId)))
|
||||
.limit(1)
|
||||
|
||||
if (!existing) {
|
||||
notFound.push(jobId)
|
||||
continue
|
||||
}
|
||||
|
||||
await db.delete(workflowSchedule).where(eq(workflowSchedule.id, jobId))
|
||||
deleted.push(jobId)
|
||||
|
||||
logger.info('Job deleted', { jobId })
|
||||
|
||||
recordAudit({
|
||||
workspaceId: context.workspaceId || null,
|
||||
actorId: context.userId,
|
||||
action: AuditAction.SCHEDULE_UPDATED,
|
||||
resourceType: AuditResourceType.SCHEDULE,
|
||||
resourceId: jobId,
|
||||
description: `Deleted job`,
|
||||
metadata: { operation: 'delete' },
|
||||
})
|
||||
}
|
||||
|
||||
await db.delete(workflowSchedule).where(eq(workflowSchedule.id, args.jobId))
|
||||
|
||||
logger.info('Job deleted', { jobId: args.jobId })
|
||||
|
||||
recordAudit({
|
||||
workspaceId: context.workspaceId || null,
|
||||
actorId: context.userId,
|
||||
action: AuditAction.SCHEDULE_UPDATED,
|
||||
resourceType: AuditResourceType.SCHEDULE,
|
||||
resourceId: args.jobId,
|
||||
description: `Deleted job`,
|
||||
metadata: { operation: 'delete' },
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
jobId: args.jobId,
|
||||
message: 'Job deleted successfully',
|
||||
},
|
||||
success: deleted.length > 0,
|
||||
output: { deleted, notFound },
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('Failed to delete job', {
|
||||
|
||||
@@ -7,30 +7,69 @@ export function executeManageCredential(
|
||||
rawParams: Record<string, unknown>,
|
||||
_context: ExecutionContext
|
||||
): Promise<ToolCallResult> {
|
||||
const params = rawParams as { operation: string; credentialId: string; displayName?: string }
|
||||
const { operation, credentialId, displayName } = params
|
||||
if (!credentialId) return Promise.resolve({ success: false, error: 'credentialId is required' })
|
||||
const params = rawParams as {
|
||||
operation: string
|
||||
credentialId?: string
|
||||
credentialIds?: string[]
|
||||
displayName?: string
|
||||
}
|
||||
const { operation, displayName } = params
|
||||
return (async () => {
|
||||
try {
|
||||
const [row] = await db
|
||||
.select({ id: credential.id, type: credential.type, displayName: credential.displayName })
|
||||
.from(credential)
|
||||
.where(eq(credential.id, credentialId))
|
||||
.limit(1)
|
||||
if (!row) return { success: false, error: 'Credential not found' }
|
||||
if (row.type !== 'oauth')
|
||||
return { success: false, error: 'Only OAuth credentials can be managed with this tool.' }
|
||||
switch (operation) {
|
||||
case 'rename':
|
||||
case 'rename': {
|
||||
const credentialId = params.credentialId
|
||||
if (!credentialId) return { success: false, error: 'credentialId is required for rename' }
|
||||
if (!displayName) return { success: false, error: 'displayName is required for rename' }
|
||||
const [row] = await db
|
||||
.select({
|
||||
id: credential.id,
|
||||
type: credential.type,
|
||||
displayName: credential.displayName,
|
||||
})
|
||||
.from(credential)
|
||||
.where(eq(credential.id, credentialId))
|
||||
.limit(1)
|
||||
if (!row) return { success: false, error: 'Credential not found' }
|
||||
if (row.type !== 'oauth')
|
||||
return {
|
||||
success: false,
|
||||
error: 'Only OAuth credentials can be managed with this tool.',
|
||||
}
|
||||
await db
|
||||
.update(credential)
|
||||
.set({ displayName, updatedAt: new Date() })
|
||||
.where(eq(credential.id, credentialId))
|
||||
return { success: true, output: { credentialId, displayName } }
|
||||
case 'delete':
|
||||
await db.delete(credential).where(eq(credential.id, credentialId))
|
||||
return { success: true, output: { credentialId, deleted: true } }
|
||||
}
|
||||
case 'delete': {
|
||||
const ids: string[] =
|
||||
params.credentialIds ?? (params.credentialId ? [params.credentialId] : [])
|
||||
if (ids.length === 0)
|
||||
return { success: false, error: 'credentialId or credentialIds is required for delete' }
|
||||
|
||||
const deleted: string[] = []
|
||||
const failed: string[] = []
|
||||
|
||||
for (const id of ids) {
|
||||
const [row] = await db
|
||||
.select({ id: credential.id, type: credential.type })
|
||||
.from(credential)
|
||||
.where(eq(credential.id, id))
|
||||
.limit(1)
|
||||
if (!row || row.type !== 'oauth') {
|
||||
failed.push(id)
|
||||
continue
|
||||
}
|
||||
await db.delete(credential).where(eq(credential.id, id))
|
||||
deleted.push(id)
|
||||
}
|
||||
|
||||
return {
|
||||
success: deleted.length > 0,
|
||||
output: { deleted, failed },
|
||||
}
|
||||
}
|
||||
default:
|
||||
return {
|
||||
success: false,
|
||||
|
||||
@@ -23,6 +23,7 @@ interface ManageCustomToolSchema {
|
||||
interface ManageCustomToolParams {
|
||||
operation?: string
|
||||
toolId?: string
|
||||
toolIds?: string[]
|
||||
schema?: ManageCustomToolSchema
|
||||
code?: string
|
||||
title?: string
|
||||
@@ -159,26 +160,35 @@ export async function executeManageCustomTool(
|
||||
}
|
||||
|
||||
if (operation === 'delete') {
|
||||
if (!params.toolId) {
|
||||
return { success: false, error: "'toolId' is required for operation 'delete'" }
|
||||
const toolIds: string[] = params.toolIds ?? (params.toolId ? [params.toolId] : [])
|
||||
if (toolIds.length === 0) {
|
||||
return { success: false, error: "'toolId' or 'toolIds' is required for operation 'delete'" }
|
||||
}
|
||||
|
||||
const deleted = await deleteCustomTool({
|
||||
toolId: params.toolId,
|
||||
userId: context.userId,
|
||||
workspaceId,
|
||||
})
|
||||
if (!deleted) {
|
||||
return { success: false, error: `Custom tool not found: ${params.toolId}` }
|
||||
const deleted: string[] = []
|
||||
const notFound: string[] = []
|
||||
|
||||
for (const toolId of toolIds) {
|
||||
const result = await deleteCustomTool({
|
||||
toolId,
|
||||
userId: context.userId,
|
||||
workspaceId,
|
||||
})
|
||||
if (result) {
|
||||
deleted.push(toolId)
|
||||
} else {
|
||||
notFound.push(toolId)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
success: deleted.length > 0,
|
||||
output: {
|
||||
success: true,
|
||||
success: deleted.length > 0,
|
||||
operation,
|
||||
toolId: params.toolId,
|
||||
message: 'Deleted custom tool',
|
||||
deleted,
|
||||
notFound,
|
||||
message: `Deleted ${deleted.length} custom tool(s)`,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,9 +186,12 @@ export async function executeMaterializeFile(
|
||||
params: Record<string, unknown>,
|
||||
context: ExecutionContext
|
||||
): Promise<ToolCallResult> {
|
||||
const fileName = params.fileName as string | undefined
|
||||
if (!fileName) {
|
||||
return { success: false, error: "Missing required parameter 'fileName'" }
|
||||
const fileNames: string[] =
|
||||
(params.fileNames as string[] | undefined) ??
|
||||
([params.fileName as string | undefined].filter(Boolean) as string[])
|
||||
|
||||
if (fileNames.length === 0) {
|
||||
return { success: false, error: "Missing required parameter 'fileNames'" }
|
||||
}
|
||||
|
||||
if (!context.chatId) {
|
||||
@@ -200,22 +203,37 @@ export async function executeMaterializeFile(
|
||||
}
|
||||
|
||||
const operation = (params.operation as string | undefined) || 'save'
|
||||
const succeeded: string[] = []
|
||||
const failed: Array<{ fileName: string; error: string }> = []
|
||||
|
||||
try {
|
||||
if (operation === 'import') {
|
||||
return await executeImport(fileName, context.chatId, context.workspaceId, context.userId)
|
||||
}
|
||||
return await executeSave(fileName, context.chatId)
|
||||
} catch (err) {
|
||||
logger.error('materialize_file failed', {
|
||||
fileName,
|
||||
operation,
|
||||
chatId: context.chatId,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
return {
|
||||
success: false,
|
||||
error: err instanceof Error ? err.message : 'Failed to materialize file',
|
||||
for (const fileName of fileNames) {
|
||||
try {
|
||||
if (operation === 'import') {
|
||||
await executeImport(fileName, context.chatId, context.workspaceId, context.userId)
|
||||
} else {
|
||||
await executeSave(fileName, context.chatId)
|
||||
}
|
||||
succeeded.push(fileName)
|
||||
} catch (err) {
|
||||
logger.error('materialize_file failed', {
|
||||
fileName,
|
||||
operation,
|
||||
chatId: context.chatId,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
failed.push({
|
||||
fileName,
|
||||
error: err instanceof Error ? err.message : 'Failed to materialize file',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: succeeded.length > 0,
|
||||
output: { succeeded, failed },
|
||||
error:
|
||||
failed.length > 0
|
||||
? `Failed to materialize: ${failed.map((f) => f.fileName).join(', ')}`
|
||||
: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,11 +172,11 @@ export interface UpdateWorkflowParams {
|
||||
}
|
||||
|
||||
export interface DeleteWorkflowParams {
|
||||
workflowId: string
|
||||
workflowIds: string[]
|
||||
}
|
||||
|
||||
export interface MoveWorkflowParams {
|
||||
workflowId: string
|
||||
workflowIds: string[]
|
||||
folderId: string | null
|
||||
}
|
||||
|
||||
@@ -191,7 +191,7 @@ export interface RenameFolderParams {
|
||||
}
|
||||
|
||||
export interface DeleteFolderParams {
|
||||
folderId: string
|
||||
folderIds: string[]
|
||||
}
|
||||
|
||||
export interface UpdateWorkspaceMcpServerParams {
|
||||
@@ -207,7 +207,13 @@ export interface DeleteWorkspaceMcpServerParams {
|
||||
|
||||
export type OpenResourceType = MothershipResourceType
|
||||
|
||||
export interface OpenResourceItem {
|
||||
type?: OpenResourceType
|
||||
id?: string
|
||||
}
|
||||
|
||||
export interface OpenResourceParams {
|
||||
resources?: OpenResourceItem[]
|
||||
type?: OpenResourceType
|
||||
id?: string
|
||||
}
|
||||
|
||||
@@ -5,91 +5,104 @@ import { getTableById } from '@/lib/table/service'
|
||||
import { getWorkspaceFile } from '@/lib/uploads/contexts/workspace/workspace-file-manager'
|
||||
import { getWorkflowById } from '@/lib/workflows/utils'
|
||||
import { isUuid } from '@/executor/constants'
|
||||
import type { OpenResourceParams, ValidOpenResourceParams } from './param-types'
|
||||
import type { OpenResourceItem, OpenResourceParams, ValidOpenResourceParams } from './param-types'
|
||||
|
||||
const VALID_OPEN_RESOURCE_TYPES = new Set(Object.values(MothershipResourceType))
|
||||
|
||||
async function resolveResource(
|
||||
item: ValidOpenResourceParams,
|
||||
context: ExecutionContext
|
||||
): Promise<{ type: string; id: string; title: string } | { error: string }> {
|
||||
const resourceType = item.type
|
||||
let resourceId = item.id
|
||||
let title: string = resourceType
|
||||
|
||||
if (resourceType === 'file') {
|
||||
if (!context.workspaceId)
|
||||
return { error: 'Opening a workspace file requires workspace context.' }
|
||||
if (!isUuid(item.id))
|
||||
return { error: 'open_resource for files requires the canonical file UUID.' }
|
||||
const record = await getWorkspaceFile(context.workspaceId, item.id)
|
||||
if (!record) return { error: `No workspace file with id "${item.id}".` }
|
||||
resourceId = record.id
|
||||
title = record.name
|
||||
}
|
||||
if (resourceType === 'workflow') {
|
||||
const wf = await getWorkflowById(item.id)
|
||||
if (!wf) return { error: `No workflow with id "${item.id}".` }
|
||||
if (context.workspaceId && wf.workspaceId !== context.workspaceId)
|
||||
return { error: `Workflow not found in the current workspace.` }
|
||||
resourceId = wf.id
|
||||
title = wf.name
|
||||
}
|
||||
if (resourceType === 'table') {
|
||||
const tbl = await getTableById(item.id)
|
||||
if (!tbl) return { error: `No table with id "${item.id}".` }
|
||||
if (context.workspaceId && tbl.workspaceId !== context.workspaceId)
|
||||
return { error: `Table not found in the current workspace.` }
|
||||
resourceId = tbl.id
|
||||
title = tbl.name
|
||||
}
|
||||
if (resourceType === 'knowledgebase') {
|
||||
const kb = await getKnowledgeBaseById(item.id)
|
||||
if (!kb) return { error: `No knowledge base with id "${item.id}".` }
|
||||
if (context.workspaceId && kb.workspaceId !== context.workspaceId)
|
||||
return { error: `Knowledge base not found in the current workspace.` }
|
||||
resourceId = kb.id
|
||||
title = kb.name
|
||||
}
|
||||
|
||||
return { type: resourceType, id: resourceId, title }
|
||||
}
|
||||
|
||||
export async function executeOpenResource(
|
||||
rawParams: Record<string, unknown>,
|
||||
context: ExecutionContext
|
||||
): Promise<ToolCallResult> {
|
||||
const params = rawParams as OpenResourceParams
|
||||
const validated = validateOpenResourceParams(params)
|
||||
if (!validated.success) return { success: false, error: validated.error }
|
||||
|
||||
const resourceType = validated.params.type
|
||||
let resourceId = validated.params.id
|
||||
let title: string = resourceType
|
||||
const items: OpenResourceItem[] =
|
||||
params.resources ?? (params.type && params.id ? [{ type: params.type, id: params.id }] : [])
|
||||
|
||||
if (resourceType === 'file') {
|
||||
if (!context.workspaceId)
|
||||
return { success: false, error: 'Opening a workspace file requires workspace context.' }
|
||||
if (!isUuid(validated.params.id))
|
||||
return { success: false, error: 'open_resource for files requires the canonical file UUID.' }
|
||||
const record = await getWorkspaceFile(context.workspaceId, validated.params.id)
|
||||
if (!record)
|
||||
return { success: false, error: `No workspace file with id "${validated.params.id}".` }
|
||||
resourceId = record.id
|
||||
title = record.name
|
||||
if (items.length === 0) {
|
||||
return { success: false, error: 'resources array is required' }
|
||||
}
|
||||
if (resourceType === 'workflow') {
|
||||
const wf = await getWorkflowById(validated.params.id)
|
||||
if (!wf) return { success: false, error: `No workflow with id "${validated.params.id}".` }
|
||||
if (context.workspaceId && wf.workspaceId !== context.workspaceId)
|
||||
return { success: false, error: `Workflow not found in the current workspace.` }
|
||||
resourceId = wf.id
|
||||
title = wf.name
|
||||
}
|
||||
if (resourceType === 'table') {
|
||||
const tbl = await getTableById(validated.params.id)
|
||||
if (!tbl) return { success: false, error: `No table with id "${validated.params.id}".` }
|
||||
if (context.workspaceId && tbl.workspaceId !== context.workspaceId)
|
||||
return { success: false, error: `Table not found in the current workspace.` }
|
||||
resourceId = tbl.id
|
||||
title = tbl.name
|
||||
}
|
||||
if (resourceType === 'knowledgebase') {
|
||||
const kb = await getKnowledgeBaseById(validated.params.id)
|
||||
if (!kb) return { success: false, error: `No knowledge base with id "${validated.params.id}".` }
|
||||
if (context.workspaceId && kb.workspaceId !== context.workspaceId)
|
||||
return { success: false, error: `Knowledge base not found in the current workspace.` }
|
||||
resourceId = kb.id
|
||||
title = kb.name
|
||||
|
||||
const resources: Array<{ type: string; id: string; title: string }> = []
|
||||
const errors: string[] = []
|
||||
|
||||
for (const item of items) {
|
||||
const validated = validateOpenResourceItem(item)
|
||||
if (!validated.success) {
|
||||
errors.push(validated.error)
|
||||
continue
|
||||
}
|
||||
const result = await resolveResource(validated.params, context)
|
||||
if ('error' in result) {
|
||||
errors.push(result.error)
|
||||
} else {
|
||||
resources.push(result)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { message: `Opened ${resourceType} ${resourceId} for the user` },
|
||||
resources: [
|
||||
{
|
||||
type: resourceType,
|
||||
id: resourceId,
|
||||
title,
|
||||
},
|
||||
],
|
||||
success: resources.length > 0,
|
||||
output: { opened: resources.length, errors },
|
||||
resources,
|
||||
}
|
||||
}
|
||||
|
||||
function validateOpenResourceParams(
|
||||
params: OpenResourceParams
|
||||
function validateOpenResourceItem(
|
||||
item: OpenResourceItem
|
||||
): { success: true; params: ValidOpenResourceParams } | { success: false; error: string } {
|
||||
if (!params.type) {
|
||||
if (!item.type) {
|
||||
return { success: false, error: 'type is required' }
|
||||
}
|
||||
|
||||
if (!VALID_OPEN_RESOURCE_TYPES.has(params.type)) {
|
||||
return { success: false, error: `Invalid resource type: ${params.type}` }
|
||||
if (!VALID_OPEN_RESOURCE_TYPES.has(item.type)) {
|
||||
return { success: false, error: `Invalid resource type: ${item.type}` }
|
||||
}
|
||||
|
||||
if (!params.id) {
|
||||
return { success: false, error: `${params.type} resources require \`id\`` }
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
params: {
|
||||
type: params.type,
|
||||
id: params.id,
|
||||
},
|
||||
if (!item.id) {
|
||||
return { success: false, error: `${item.type} resources require \`id\`` }
|
||||
}
|
||||
return { success: true, params: { type: item.type, id: item.id } }
|
||||
}
|
||||
|
||||
106
apps/sim/lib/copilot/tools/handlers/restore-resource.ts
Normal file
106
apps/sim/lib/copilot/tools/handlers/restore-resource.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/request/types'
|
||||
import { generateId } from '@/lib/core/utils/uuid'
|
||||
import { restoreKnowledgeBase } from '@/lib/knowledge/service'
|
||||
import { getTableById, restoreTable } from '@/lib/table/service'
|
||||
import {
|
||||
getWorkspaceFile,
|
||||
restoreWorkspaceFile,
|
||||
} from '@/lib/uploads/contexts/workspace/workspace-file-manager'
|
||||
import { restoreWorkflow } from '@/lib/workflows/lifecycle'
|
||||
import { performRestoreFolder } from '@/lib/workflows/orchestration/folder-lifecycle'
|
||||
|
||||
const logger = createLogger('RestoreResource')
|
||||
|
||||
const VALID_TYPES = new Set(['workflow', 'table', 'file', 'knowledgebase', 'folder'])
|
||||
|
||||
export async function executeRestoreResource(
|
||||
rawParams: Record<string, unknown>,
|
||||
context: ExecutionContext
|
||||
): Promise<ToolCallResult> {
|
||||
const type = rawParams.type as string | undefined
|
||||
const id = rawParams.id as string | undefined
|
||||
|
||||
if (!type || !VALID_TYPES.has(type)) {
|
||||
return { success: false, error: `Invalid type. Must be one of: ${[...VALID_TYPES].join(', ')}` }
|
||||
}
|
||||
if (!id) {
|
||||
return { success: false, error: 'id is required' }
|
||||
}
|
||||
if (!context.workspaceId) {
|
||||
return { success: false, error: 'Workspace context required' }
|
||||
}
|
||||
|
||||
const requestId = generateId().slice(0, 8)
|
||||
|
||||
try {
|
||||
switch (type) {
|
||||
case 'workflow': {
|
||||
const result = await restoreWorkflow(id, { requestId })
|
||||
if (!result.restored) {
|
||||
return { success: false, error: 'Workflow not found or not archived' }
|
||||
}
|
||||
logger.info('Workflow restored via copilot', { workflowId: id })
|
||||
return {
|
||||
success: true,
|
||||
output: { type, id, name: result.workflow?.name },
|
||||
resources: [{ type: 'workflow', id, title: result.workflow?.name || id }],
|
||||
}
|
||||
}
|
||||
|
||||
case 'table': {
|
||||
await restoreTable(id, requestId)
|
||||
const table = await getTableById(id)
|
||||
const tableName = table?.name || id
|
||||
logger.info('Table restored via copilot', { tableId: id, name: tableName })
|
||||
return {
|
||||
success: true,
|
||||
output: { type, id, name: tableName },
|
||||
resources: [{ type: 'table', id, title: tableName }],
|
||||
}
|
||||
}
|
||||
|
||||
case 'file': {
|
||||
await restoreWorkspaceFile(context.workspaceId, id)
|
||||
const fileRecord = await getWorkspaceFile(context.workspaceId, id)
|
||||
const fileName = fileRecord?.name || id
|
||||
logger.info('File restored via copilot', { fileId: id, name: fileName })
|
||||
return {
|
||||
success: true,
|
||||
output: { type, id, name: fileName },
|
||||
resources: [{ type: 'file', id, title: fileName }],
|
||||
}
|
||||
}
|
||||
|
||||
case 'knowledgebase': {
|
||||
await restoreKnowledgeBase(id, requestId)
|
||||
logger.info('Knowledge base restored via copilot', { knowledgeBaseId: id })
|
||||
return {
|
||||
success: true,
|
||||
output: { type, id },
|
||||
}
|
||||
}
|
||||
|
||||
case 'folder': {
|
||||
const result = await performRestoreFolder({
|
||||
folderId: id,
|
||||
workspaceId: context.workspaceId,
|
||||
userId: context.userId,
|
||||
})
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.error || 'Failed to restore folder' }
|
||||
}
|
||||
logger.info('Folder restored via copilot', { folderId: id })
|
||||
return {
|
||||
success: true,
|
||||
output: { type, id, restoredItems: result.restoredItems },
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return { success: false, error: `Unsupported type: ${type}` }
|
||||
}
|
||||
} catch (error) {
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) }
|
||||
}
|
||||
}
|
||||
@@ -412,17 +412,27 @@ export async function executeMoveWorkflow(
|
||||
context: ExecutionContext
|
||||
): Promise<ToolCallResult> {
|
||||
try {
|
||||
const workflowId = params.workflowId
|
||||
if (!workflowId) {
|
||||
return { success: false, error: 'workflowId is required' }
|
||||
const workflowIds = params.workflowIds
|
||||
if (!workflowIds || workflowIds.length === 0) {
|
||||
return { success: false, error: 'workflowIds is required' }
|
||||
}
|
||||
|
||||
await ensureWorkflowAccess(workflowId, context.userId, 'write')
|
||||
const folderId = params.folderId || null
|
||||
assertWorkflowMutationNotAborted(context)
|
||||
await updateWorkflowRecord(workflowId, { folderId })
|
||||
const moved: string[] = []
|
||||
const failed: string[] = []
|
||||
|
||||
return { success: true, output: { workflowId, folderId } }
|
||||
for (const workflowId of workflowIds) {
|
||||
try {
|
||||
await ensureWorkflowAccess(workflowId, context.userId, 'write')
|
||||
assertWorkflowMutationNotAborted(context)
|
||||
await updateWorkflowRecord(workflowId, { folderId })
|
||||
moved.push(workflowId)
|
||||
} catch {
|
||||
failed.push(workflowId)
|
||||
}
|
||||
}
|
||||
|
||||
return { success: moved.length > 0, output: { moved, failed, folderId } }
|
||||
} catch (error) {
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) }
|
||||
}
|
||||
@@ -661,26 +671,37 @@ export async function executeDeleteWorkflow(
|
||||
context: ExecutionContext
|
||||
): Promise<ToolCallResult> {
|
||||
try {
|
||||
const workflowId = params.workflowId
|
||||
if (!workflowId) {
|
||||
return { success: false, error: 'workflowId is required' }
|
||||
const workflowIds = params.workflowIds
|
||||
if (!workflowIds || workflowIds.length === 0) {
|
||||
return { success: false, error: 'workflowIds is required' }
|
||||
}
|
||||
|
||||
const { workflow: workflowRecord } = await ensureWorkflowAccess(
|
||||
workflowId,
|
||||
context.userId,
|
||||
'admin'
|
||||
)
|
||||
assertWorkflowMutationNotAborted(context)
|
||||
const deleted: Array<{ workflowId: string; name: string }> = []
|
||||
const failed: string[] = []
|
||||
|
||||
const result = await performDeleteWorkflow({ workflowId, userId: context.userId })
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.error || 'Failed to delete workflow' }
|
||||
for (const workflowId of workflowIds) {
|
||||
try {
|
||||
const { workflow: workflowRecord } = await ensureWorkflowAccess(
|
||||
workflowId,
|
||||
context.userId,
|
||||
'admin'
|
||||
)
|
||||
assertWorkflowMutationNotAborted(context)
|
||||
|
||||
const result = await performDeleteWorkflow({ workflowId, userId: context.userId })
|
||||
if (result.success) {
|
||||
deleted.push({ workflowId, name: workflowRecord.name })
|
||||
} else {
|
||||
failed.push(workflowId)
|
||||
}
|
||||
} catch {
|
||||
failed.push(workflowId)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { workflowId, name: workflowRecord.name, deleted: true },
|
||||
success: deleted.length > 0,
|
||||
output: { deleted, failed },
|
||||
}
|
||||
} catch (error) {
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) }
|
||||
@@ -692,34 +713,42 @@ export async function executeDeleteFolder(
|
||||
context: ExecutionContext
|
||||
): Promise<ToolCallResult> {
|
||||
try {
|
||||
const folderId = params.folderId
|
||||
if (!folderId) {
|
||||
return { success: false, error: 'folderId is required' }
|
||||
const folderIds = params.folderIds
|
||||
if (!folderIds || folderIds.length === 0) {
|
||||
return { success: false, error: 'folderIds is required' }
|
||||
}
|
||||
|
||||
const workspaceId = context.workspaceId || (await getDefaultWorkspaceId(context.userId))
|
||||
await ensureWorkspaceAccess(workspaceId, context.userId, 'admin')
|
||||
|
||||
const folders = await listFolders(workspaceId)
|
||||
const folder = folders.find((f) => f.folderId === folderId)
|
||||
if (!folder) {
|
||||
return { success: false, error: 'Folder not found' }
|
||||
const deleted: string[] = []
|
||||
const failed: string[] = []
|
||||
|
||||
for (const folderId of folderIds) {
|
||||
const folder = folders.find((f) => f.folderId === folderId)
|
||||
if (!folder) {
|
||||
failed.push(folderId)
|
||||
continue
|
||||
}
|
||||
|
||||
assertWorkflowMutationNotAborted(context)
|
||||
|
||||
const result = await performDeleteFolder({
|
||||
folderId,
|
||||
workspaceId,
|
||||
userId: context.userId,
|
||||
folderName: folder.folderName,
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
deleted.push(folderId)
|
||||
} else {
|
||||
failed.push(folderId)
|
||||
}
|
||||
}
|
||||
|
||||
assertWorkflowMutationNotAborted(context)
|
||||
|
||||
const result = await performDeleteFolder({
|
||||
folderId,
|
||||
workspaceId,
|
||||
userId: context.userId,
|
||||
folderName: folder.folderName,
|
||||
})
|
||||
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.error || 'Failed to delete folder' }
|
||||
}
|
||||
|
||||
return { success: true, output: { folderId, deleted: true, ...result.deletedItems } }
|
||||
return { success: deleted.length > 0, output: { deleted, failed } }
|
||||
} catch (error) {
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) }
|
||||
}
|
||||
|
||||
@@ -13,7 +13,8 @@ import {
|
||||
const logger = createLogger('DeleteFileServerTool')
|
||||
|
||||
interface DeleteFileArgs {
|
||||
fileId: string
|
||||
fileIds?: string[]
|
||||
fileId?: string
|
||||
args?: Record<string, unknown>
|
||||
}
|
||||
|
||||
@@ -34,27 +35,41 @@ export const deleteFileServerTool: BaseServerTool<DeleteFileArgs, DeleteFileResu
|
||||
}
|
||||
|
||||
const nested = params.args
|
||||
const fileId = params.fileId || (nested?.fileId as string) || ''
|
||||
const fileIds: string[] =
|
||||
params.fileIds ??
|
||||
(nested?.fileIds as string[] | undefined) ??
|
||||
[params.fileId || (nested?.fileId as string) || ''].filter(Boolean)
|
||||
|
||||
if (!fileId) return { success: false, message: 'fileId is required' }
|
||||
if (fileIds.length === 0) return { success: false, message: 'fileIds is required' }
|
||||
|
||||
const existingFile = await getWorkspaceFile(workspaceId, fileId)
|
||||
if (!existingFile) {
|
||||
return { success: false, message: `File with ID "${fileId}" not found` }
|
||||
const deleted: string[] = []
|
||||
const failed: string[] = []
|
||||
|
||||
for (const fileId of fileIds) {
|
||||
const existingFile = await getWorkspaceFile(workspaceId, fileId)
|
||||
if (!existingFile) {
|
||||
failed.push(fileId)
|
||||
continue
|
||||
}
|
||||
|
||||
assertServerToolNotAborted(context)
|
||||
await deleteWorkspaceFile(workspaceId, fileId)
|
||||
deleted.push(existingFile.name)
|
||||
|
||||
logger.info('File deleted via delete_file', {
|
||||
fileId,
|
||||
name: existingFile.name,
|
||||
userId: context.userId,
|
||||
})
|
||||
}
|
||||
|
||||
assertServerToolNotAborted(context)
|
||||
await deleteWorkspaceFile(workspaceId, fileId)
|
||||
|
||||
logger.info('File deleted via delete_file', {
|
||||
fileId,
|
||||
name: existingFile.name,
|
||||
userId: context.userId,
|
||||
})
|
||||
const parts: string[] = []
|
||||
if (deleted.length > 0) parts.push(`Deleted: ${deleted.join(', ')}`)
|
||||
if (failed.length > 0) parts.push(`Not found: ${failed.join(', ')}`)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `File "${existingFile.name}" deleted successfully`,
|
||||
success: deleted.length > 0,
|
||||
message: parts.join('. '),
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -246,12 +246,13 @@ export const knowledgeBaseServerTool: BaseServerTool<KnowledgeBaseArgs, Knowledg
|
||||
}
|
||||
}
|
||||
|
||||
const fileReference = args.fileId || args.filePath
|
||||
if (!fileReference) {
|
||||
const fileIds: string[] =
|
||||
args.fileIds ?? (args.fileId ? [args.fileId] : args.filePath ? [args.filePath] : [])
|
||||
if (fileIds.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
message:
|
||||
'fileId is required for add_file. Read files/{name}/meta.json or files/by-id/*/meta.json to get the canonical file ID.',
|
||||
'fileIds is required for add_file. Read files/{name}/meta.json or files/by-id/*/meta.json to get the canonical file IDs.',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,68 +265,74 @@ export const knowledgeBaseServerTool: BaseServerTool<KnowledgeBaseArgs, Knowledg
|
||||
}
|
||||
|
||||
const kbWorkspaceId: string = targetKb.workspaceId
|
||||
const fileRecord = await resolveWorkspaceFileReference(kbWorkspaceId, fileReference)
|
||||
const added: Array<{ documentId: string; filename: string }> = []
|
||||
const failedFiles: string[] = []
|
||||
|
||||
if (!fileRecord) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Workspace file not found: "${fileReference}"`,
|
||||
for (const fileRef of fileIds) {
|
||||
const fileRecord = await resolveWorkspaceFileReference(kbWorkspaceId, fileRef)
|
||||
if (!fileRecord) {
|
||||
failedFiles.push(fileRef)
|
||||
continue
|
||||
}
|
||||
|
||||
const presignedUrl = await StorageService.generatePresignedDownloadUrl(
|
||||
fileRecord.key,
|
||||
'workspace',
|
||||
5 * 60
|
||||
)
|
||||
|
||||
const requestId = generateId().slice(0, 8)
|
||||
assertNotAborted()
|
||||
const doc = await createSingleDocument(
|
||||
{
|
||||
filename: fileRecord.name,
|
||||
fileUrl: presignedUrl,
|
||||
fileSize: fileRecord.size,
|
||||
mimeType: fileRecord.type,
|
||||
},
|
||||
args.knowledgeBaseId,
|
||||
requestId
|
||||
)
|
||||
|
||||
processDocumentAsync(
|
||||
args.knowledgeBaseId,
|
||||
doc.id,
|
||||
{
|
||||
filename: fileRecord.name,
|
||||
fileUrl: presignedUrl,
|
||||
fileSize: fileRecord.size,
|
||||
mimeType: fileRecord.type,
|
||||
},
|
||||
{}
|
||||
).catch((err) => {
|
||||
logger.error('Background document processing failed', {
|
||||
documentId: doc.id,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
})
|
||||
|
||||
added.push({ documentId: doc.id, filename: fileRecord.name })
|
||||
|
||||
logger.info('Workspace file added to knowledge base via copilot', {
|
||||
knowledgeBaseId: args.knowledgeBaseId,
|
||||
documentId: doc.id,
|
||||
fileName: fileRecord.name,
|
||||
userId: context.userId,
|
||||
})
|
||||
}
|
||||
|
||||
const presignedUrl = await StorageService.generatePresignedDownloadUrl(
|
||||
fileRecord.key,
|
||||
'workspace',
|
||||
5 * 60
|
||||
)
|
||||
|
||||
const requestId = generateId().slice(0, 8)
|
||||
assertNotAborted()
|
||||
const doc = await createSingleDocument(
|
||||
{
|
||||
filename: fileRecord.name,
|
||||
fileUrl: presignedUrl,
|
||||
fileSize: fileRecord.size,
|
||||
mimeType: fileRecord.type,
|
||||
},
|
||||
args.knowledgeBaseId,
|
||||
requestId
|
||||
)
|
||||
|
||||
processDocumentAsync(
|
||||
args.knowledgeBaseId,
|
||||
doc.id,
|
||||
{
|
||||
filename: fileRecord.name,
|
||||
fileUrl: presignedUrl,
|
||||
fileSize: fileRecord.size,
|
||||
mimeType: fileRecord.type,
|
||||
},
|
||||
{}
|
||||
).catch((err) => {
|
||||
logger.error('Background document processing failed', {
|
||||
documentId: doc.id,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
})
|
||||
|
||||
logger.info('Workspace file added to knowledge base via copilot', {
|
||||
knowledgeBaseId: args.knowledgeBaseId,
|
||||
documentId: doc.id,
|
||||
fileName: fileRecord.name,
|
||||
userId: context.userId,
|
||||
})
|
||||
|
||||
const addedNames = added.map((a) => a.filename).join(', ')
|
||||
return {
|
||||
success: true,
|
||||
message: `File "${fileRecord.name}" added to knowledge base "${targetKb.name}". Processing started (chunking + embedding).`,
|
||||
success: added.length > 0,
|
||||
message:
|
||||
added.length > 0
|
||||
? `Added ${added.length} file(s) to "${targetKb.name}": ${addedNames}. Processing started.`
|
||||
: `No files could be added.`,
|
||||
data: {
|
||||
documentId: doc.id,
|
||||
knowledgeBaseId: args.knowledgeBaseId,
|
||||
knowledgeBaseName: targetKb.name,
|
||||
filename: fileRecord.name,
|
||||
fileSize: fileRecord.size,
|
||||
mimeType: fileRecord.type,
|
||||
added,
|
||||
failed: failedFiles,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -379,38 +386,44 @@ export const knowledgeBaseServerTool: BaseServerTool<KnowledgeBaseArgs, Knowledg
|
||||
}
|
||||
|
||||
case 'delete': {
|
||||
if (!args.knowledgeBaseId) {
|
||||
const kbIds: string[] =
|
||||
args.knowledgeBaseIds ?? (args.knowledgeBaseId ? [args.knowledgeBaseId] : [])
|
||||
if (kbIds.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Knowledge base ID is required for delete operation',
|
||||
message: 'knowledgeBaseId or knowledgeBaseIds is required for delete operation',
|
||||
}
|
||||
}
|
||||
|
||||
const kbToDelete = await getKnowledgeBaseById(args.knowledgeBaseId)
|
||||
if (!kbToDelete) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Knowledge base with ID "${args.knowledgeBaseId}" not found`,
|
||||
const deleted: Array<{ id: string; name: string }> = []
|
||||
const notFound: string[] = []
|
||||
|
||||
for (const kbId of kbIds) {
|
||||
const kbToDelete = await getKnowledgeBaseById(kbId)
|
||||
if (!kbToDelete) {
|
||||
notFound.push(kbId)
|
||||
continue
|
||||
}
|
||||
|
||||
const requestId = generateId().slice(0, 8)
|
||||
assertNotAborted()
|
||||
await deleteKnowledgeBase(kbId, requestId)
|
||||
deleted.push({ id: kbId, name: kbToDelete.name })
|
||||
|
||||
logger.info('Knowledge base deleted via copilot', {
|
||||
knowledgeBaseId: kbId,
|
||||
name: kbToDelete.name,
|
||||
userId: context.userId,
|
||||
})
|
||||
}
|
||||
|
||||
const requestId = generateId().slice(0, 8)
|
||||
assertNotAborted()
|
||||
await deleteKnowledgeBase(args.knowledgeBaseId, requestId)
|
||||
|
||||
logger.info('Knowledge base deleted via copilot', {
|
||||
knowledgeBaseId: args.knowledgeBaseId,
|
||||
name: kbToDelete.name,
|
||||
userId: context.userId,
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Knowledge base "${kbToDelete.name}" deleted successfully`,
|
||||
data: {
|
||||
id: args.knowledgeBaseId,
|
||||
name: kbToDelete.name,
|
||||
},
|
||||
success: deleted.length > 0,
|
||||
message:
|
||||
deleted.length > 0
|
||||
? `Deleted: ${deleted.map((d) => d.name).join(', ')}`
|
||||
: 'No knowledge bases found',
|
||||
data: { deleted, notFound },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -418,16 +431,32 @@ export const knowledgeBaseServerTool: BaseServerTool<KnowledgeBaseArgs, Knowledg
|
||||
if (!args.knowledgeBaseId) {
|
||||
return { success: false, message: 'knowledgeBaseId is required for delete_document' }
|
||||
}
|
||||
if (!args.documentId) {
|
||||
return { success: false, message: 'documentId is required for delete_document' }
|
||||
const docIds: string[] = args.documentIds ?? (args.documentId ? [args.documentId] : [])
|
||||
if (docIds.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'documentId or documentIds is required for delete_document',
|
||||
}
|
||||
}
|
||||
const requestId = generateId().slice(0, 8)
|
||||
assertNotAborted()
|
||||
const result = await deleteDocument(args.documentId, requestId)
|
||||
|
||||
const deleted: string[] = []
|
||||
const failed: string[] = []
|
||||
|
||||
for (const docId of docIds) {
|
||||
const requestId = generateId().slice(0, 8)
|
||||
assertNotAborted()
|
||||
const result = await deleteDocument(docId, requestId)
|
||||
if (result.success) {
|
||||
deleted.push(docId)
|
||||
} else {
|
||||
failed.push(docId)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: result.success,
|
||||
message: result.message,
|
||||
data: { documentId: args.documentId, knowledgeBaseId: args.knowledgeBaseId },
|
||||
success: deleted.length > 0,
|
||||
message: `Deleted ${deleted.length} document(s)${failed.length > 0 ? `, ${failed.length} failed` : ''}`,
|
||||
data: { knowledgeBaseId: args.knowledgeBaseId, deleted, failed },
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -335,28 +335,33 @@ export const userTableServerTool: BaseServerTool<UserTableArgs, UserTableResult>
|
||||
}
|
||||
|
||||
case 'delete': {
|
||||
if (!args.tableId) {
|
||||
return { success: false, message: 'Table ID is required' }
|
||||
const tableIds: string[] = args.tableIds ?? (args.tableId ? [args.tableId] : [])
|
||||
if (tableIds.length === 0) {
|
||||
return { success: false, message: 'tableId or tableIds is required' }
|
||||
}
|
||||
if (!workspaceId) {
|
||||
return { success: false, message: 'Workspace ID is required' }
|
||||
}
|
||||
|
||||
const table = await getTableById(args.tableId)
|
||||
if (!table) {
|
||||
return { success: false, message: `Table not found: ${args.tableId}` }
|
||||
}
|
||||
if (table.workspaceId !== workspaceId) {
|
||||
return { success: false, message: 'Table not found' }
|
||||
}
|
||||
const deleted: string[] = []
|
||||
const failed: string[] = []
|
||||
|
||||
const requestId = generateId().slice(0, 8)
|
||||
assertNotAborted()
|
||||
await deleteTable(args.tableId, requestId)
|
||||
for (const tableId of tableIds) {
|
||||
const table = await getTableById(tableId)
|
||||
if (!table || table.workspaceId !== workspaceId) {
|
||||
failed.push(tableId)
|
||||
continue
|
||||
}
|
||||
|
||||
const requestId = generateId().slice(0, 8)
|
||||
assertNotAborted()
|
||||
await deleteTable(tableId, requestId)
|
||||
deleted.push(tableId)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Deleted table ${args.tableId}`,
|
||||
success: deleted.length > 0,
|
||||
message: `Deleted ${deleted.length} table(s)${failed.length > 0 ? `, ${failed.length} not found` : ''}`,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,12 +10,13 @@ import {
|
||||
mcpServers as mcpServersTable,
|
||||
workflowDeploymentVersion,
|
||||
workflowExecutionLogs,
|
||||
workflowFolder,
|
||||
workflowMcpServer,
|
||||
workflowMcpTool,
|
||||
workflowSchedule,
|
||||
} from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, desc, eq, isNull, ne } from 'drizzle-orm'
|
||||
import { and, desc, eq, isNotNull, isNull, ne } from 'drizzle-orm'
|
||||
import { listApiKeys } from '@/lib/api-key/service'
|
||||
import { buildWorkspaceMd, type WorkspaceMdData } from '@/lib/copilot/chat/workspace-context'
|
||||
import { type FileReadResult, readFileRecord } from '@/lib/copilot/vfs/file-reader'
|
||||
@@ -390,6 +391,8 @@ export class WorkspaceVFS {
|
||||
})
|
||||
)
|
||||
|
||||
await this.materializeRecentlyDeleted(workspaceId, userId)
|
||||
|
||||
for (const [path, content] of getStaticComponentFiles()) {
|
||||
this.files.set(path, content)
|
||||
}
|
||||
@@ -401,16 +404,32 @@ export class WorkspaceVFS {
|
||||
})
|
||||
}
|
||||
|
||||
private activeFiles(): Map<string, string> {
|
||||
const filtered = new Map<string, string>()
|
||||
for (const [key, value] of this.files) {
|
||||
if (!key.startsWith('recently-deleted/')) {
|
||||
filtered.set(key, value)
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
private filesForPath(path?: string): Map<string, string> {
|
||||
if (path?.startsWith('recently-deleted')) return this.files
|
||||
return this.activeFiles()
|
||||
}
|
||||
|
||||
grep(
|
||||
pattern: string,
|
||||
path?: string,
|
||||
options?: GrepOptions
|
||||
): GrepMatch[] | string[] | ops.GrepCountEntry[] {
|
||||
return ops.grep(this.files, pattern, path, options)
|
||||
return ops.grep(this.filesForPath(path), pattern, path, options)
|
||||
}
|
||||
|
||||
glob(pattern: string): string[] {
|
||||
return ops.glob(this.files, pattern)
|
||||
const target = pattern.startsWith('recently-deleted') ? this.files : this.activeFiles()
|
||||
return ops.glob(target, pattern)
|
||||
}
|
||||
|
||||
read(path: string, offset?: number, limit?: number): ReadResult | null {
|
||||
@@ -418,7 +437,7 @@ export class WorkspaceVFS {
|
||||
}
|
||||
|
||||
list(path: string): DirEntry[] {
|
||||
return ops.list(this.files, path)
|
||||
return ops.list(this.filesForPath(path), path)
|
||||
}
|
||||
|
||||
suggestSimilar(missingPath: string, max?: number): string[] {
|
||||
@@ -431,14 +450,18 @@ export class WorkspaceVFS {
|
||||
* Returns null if the path doesn't match `files/{name}` / `files/by-id/{id}` or the file isn't found.
|
||||
*/
|
||||
async readFileContent(path: string): Promise<FileReadResult | null> {
|
||||
const match = path.match(/^files\/(.+?)(?:\/content)?$/)
|
||||
const deletedMatch = path.match(/^recently-deleted\/files\/(.+?)(?:\/content)?$/)
|
||||
const activeMatch = path.match(/^files\/(.+?)(?:\/content)?$/)
|
||||
const match = deletedMatch || activeMatch
|
||||
if (!match) return null
|
||||
const fileName = match[1]
|
||||
|
||||
if (fileName.endsWith('/meta.json') || path.endsWith('/meta.json')) return null
|
||||
|
||||
const scope = deletedMatch ? 'archived' : 'active'
|
||||
|
||||
try {
|
||||
const files = await listWorkspaceFiles(this._workspaceId)
|
||||
const files = await listWorkspaceFiles(this._workspaceId, { scope })
|
||||
const record = findWorkspaceFileRecord(files, fileName)
|
||||
if (!record) return null
|
||||
return readFileRecord(record)
|
||||
@@ -1209,6 +1232,103 @@ export class WorkspaceVFS {
|
||||
}
|
||||
}
|
||||
|
||||
private async materializeRecentlyDeleted(workspaceId: string, userId: string): Promise<void> {
|
||||
try {
|
||||
const [archivedWorkflows, archivedFolders, archivedTables, archivedFiles, archivedKBs] =
|
||||
await Promise.all([
|
||||
listWorkflows(workspaceId, { scope: 'archived' }),
|
||||
db
|
||||
.select({
|
||||
id: workflowFolder.id,
|
||||
name: workflowFolder.name,
|
||||
archivedAt: workflowFolder.archivedAt,
|
||||
})
|
||||
.from(workflowFolder)
|
||||
.where(
|
||||
and(eq(workflowFolder.workspaceId, workspaceId), isNotNull(workflowFolder.archivedAt))
|
||||
),
|
||||
listTables(workspaceId, { scope: 'archived' }),
|
||||
listWorkspaceFiles(workspaceId, { scope: 'archived' }),
|
||||
getKnowledgeBases(userId, workspaceId, 'archived'),
|
||||
])
|
||||
|
||||
for (const wf of archivedWorkflows) {
|
||||
const safeName = sanitizeName(wf.name)
|
||||
this.files.set(
|
||||
`recently-deleted/workflows/${safeName}/meta.json`,
|
||||
serializeWorkflowMeta(wf)
|
||||
)
|
||||
}
|
||||
|
||||
for (const folder of archivedFolders) {
|
||||
const safeName = sanitizeName(folder.name)
|
||||
this.files.set(
|
||||
`recently-deleted/folders/${safeName}/meta.json`,
|
||||
JSON.stringify(
|
||||
{ id: folder.id, name: folder.name, archivedAt: folder.archivedAt },
|
||||
null,
|
||||
2
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
for (const table of archivedTables) {
|
||||
const safeName = sanitizeName(table.name)
|
||||
this.files.set(
|
||||
`recently-deleted/tables/${safeName}/meta.json`,
|
||||
serializeTableMeta({
|
||||
id: table.id,
|
||||
name: table.name,
|
||||
description: table.description,
|
||||
schema: table.schema,
|
||||
rowCount: table.rowCount,
|
||||
maxRows: table.maxRows,
|
||||
createdAt: table.createdAt,
|
||||
updatedAt: table.updatedAt,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
for (const file of archivedFiles) {
|
||||
const safeName = sanitizeName(file.name)
|
||||
this.files.set(
|
||||
`recently-deleted/files/${safeName}/meta.json`,
|
||||
serializeFileMeta({
|
||||
id: file.id,
|
||||
name: file.name,
|
||||
contentType: file.type,
|
||||
size: file.size,
|
||||
uploadedAt: file.uploadedAt,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
for (const kb of archivedKBs) {
|
||||
const safeName = sanitizeName(kb.name)
|
||||
this.files.set(
|
||||
`recently-deleted/knowledgebases/${safeName}/meta.json`,
|
||||
serializeKBMeta({
|
||||
id: kb.id,
|
||||
name: kb.name,
|
||||
description: kb.description,
|
||||
embeddingModel: kb.embeddingModel,
|
||||
embeddingDimension: kb.embeddingDimension,
|
||||
tokenCount: kb.tokenCount,
|
||||
createdAt: kb.createdAt,
|
||||
updatedAt: kb.updatedAt,
|
||||
documentCount: kb.docCount,
|
||||
connectorTypes: kb.connectorTypes,
|
||||
})
|
||||
)
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn('Failed to materialize recently deleted resources', {
|
||||
workspaceId,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Materialize environment data using shared service functions:
|
||||
* - getAccessibleEnvCredentials for workspace-scoped credentials
|
||||
|
||||
Reference in New Issue
Block a user