UNPKG

@net3/queuer

Version:

346 lines (304 loc) 11.3 kB
import { createAction, Property, ActionContext, StoreScope, DynamicPropsValue, } from '@activepieces/pieces-framework'; import { PauseType } from '@activepieces/shared'; import { QueueManager } from '../common/queue-manager'; import { MCPManager } from '../common/mcp-manager'; /** * Standalone helper function to execute a queued item. * This is called after the pause resumes. */ async function executeQueuedItem(context: ActionContext, queueItemId: string) { let debugInfo: any = { step: 'starting', queueItemId }; try { debugInfo.step = 'getQueueItem'; let item = await QueueManager.getQueueItem(context, queueItemId); if (!item) { throw new Error(`Queue item not found for ID: ${queueItemId} [Step: getQueueItem]`); } debugInfo.item = { queueId: item.queueId, status: item.status, releaseTime: item.releaseTime }; /* Validate position / repause if needed */ debugInfo.step = 'validateQueuePosition'; const validation = await QueueManager.validateQueuePosition( context, queueItemId, ); debugInfo.validation = validation; if (validation.shouldRepause && validation.newReleaseTime) { debugInfo.step = 'repause'; debugInfo.newReleaseTime = validation.newReleaseTime; await context.run.pause({ pauseMetadata: { type: PauseType.DELAY, resumeDateTime: new Date(validation.newReleaseTime).toUTCString(), }, }); // After pause, re-run this function return await executeQueuedItem(context, queueItemId); } if (!validation.isValid) { throw new Error(`Queue validation failed: ${validation.reason} [Step: validateQueuePosition] [Debug: ${JSON.stringify(debugInfo)}]`); } /* Execute */ debugInfo.step = 'getQueueConfiguration'; const queue = await QueueManager.getQueueConfiguration(context, item.queueId); if (!queue) { throw new Error(`Queue config missing for ID: ${item.queueId} [Step: getQueueConfiguration] [Debug: ${JSON.stringify(debugInfo)}]`); } debugInfo.queue = { id: queue.id, mcpToolName: queue.mcpToolName }; debugInfo.step = 'updateItemStatus-processing'; await QueueManager.updateItemStatus(context, queueItemId, 'processing'); // Get MCP server URL from auth debugInfo.step = 'checkAuth'; const authConfig = context.auth as { mcpServerUrl?: string }; if (!authConfig.mcpServerUrl) { throw new Error(`MCP Server URL is required in auth configuration [Step: checkAuth] [Auth: ${JSON.stringify(authConfig)}] [Debug: ${JSON.stringify(debugInfo)}]`); } debugInfo.mcpServerUrl = authConfig.mcpServerUrl; const toolName = queue.mcpToolName; if (!toolName) { throw new Error(`Queue configuration missing tool name [Step: checkAuth] [Queue: ${JSON.stringify(queue)}] [Debug: ${JSON.stringify(debugInfo)}]`); } debugInfo.toolName = toolName; debugInfo.step = 'executeMCPAction'; const config = { mcpServerUrl: authConfig.mcpServerUrl }; const result = await MCPManager.executeMCPAction(toolName, item.actionConfig, config); debugInfo.mcpResult = { success: result.success, hasError: !!result.error }; if (result.success) { debugInfo.step = 'cleanup-success'; await QueueManager.incrementUsageCounters(context, queue.id); await QueueManager.markExecutionComplete(context, queue.id, queueItemId); await QueueManager.updateItemStatus(context, queueItemId, 'completed'); await QueueManager.cleanupQueueItem( context, queue.id, queueItemId, item.releaseTime, ); const finalStatus = await QueueManager.getQueueStatus(context, queue.id); return { success: true, queueItemId, executedAt: new Date().toISOString(), result: result.result, status: finalStatus, debugInfo: debugInfo, }; } else { debugInfo.step = 'cleanup-failure'; debugInfo.mcpError = result.error; await QueueManager.markExecutionComplete(context, queue.id, queueItemId); await QueueManager.updateItemStatus( context, queueItemId, 'failed', result.error, ); await QueueManager.cleanupQueueItem( context, queue.id, queueItemId, item.releaseTime, ); throw new Error(`MCP execution failed: ${result.error || 'Unknown error'} [Step: executeMCPAction] [Debug: ${JSON.stringify(debugInfo)}]`); } } catch (error) { // Add debug context to the original error for post-pause execution debugging if (error instanceof Error) { error.message = `${error.message} [DEBUG - LastStep: ${debugInfo.step}, Context: ${JSON.stringify(debugInfo)}]`; } throw error; } } /** * Helper to generate dynamic properties based on MCP tool schema */ async function generateDynamicProps(data: DynamicPropsValue): Promise<Record<string, any>> { const queueId = data.queueId as string | undefined; if (!queueId) return {}; try { // Extract tool name from queue ID (format: mcp_toolname_queue) const toolNameMatch = queueId.match(/^mcp_(.+)_queue$/); if (!toolNameMatch) { throw new Error(`Invalid queue ID format: ${queueId}`); } const toolName = toolNameMatch[1]; const authConfig = data.auth as { mcpServerUrl?: string }; if (!authConfig.mcpServerUrl) { throw new Error('MCP Server URL not found in auth config'); } const config = { mcpServerUrl: authConfig.mcpServerUrl }; const tools = await MCPManager.listMCPTools(config); const tool = tools.find((t: any) => t.name === toolName); if (!tool) { throw new Error(`Tool not found: ${toolName}`); } if (!tool.inputSchema || !tool.inputSchema.properties) { throw new Error(`Tool has no input schema: ${toolName}`); } const dynamicProps: Record<string, any> = {}; const schema = tool.inputSchema; const required = schema.required || []; // Generate properties based on the tool's input schema Object.keys(schema.properties).forEach(propName => { const propDef = schema.properties[propName]; const isRequired = required.includes(propName); const displayName = propName.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase()); const description = propDef.description || `${propName} parameter for ${tool.name}`; if (propDef.type === 'string') { dynamicProps[propName] = Property.ShortText({ displayName, description, required: isRequired, }); } else if (propDef.type === 'number' || propDef.type === 'integer') { dynamicProps[propName] = Property.Number({ displayName, description, required: isRequired, }); } else if (propDef.type === 'boolean') { dynamicProps[propName] = Property.Checkbox({ displayName, description, required: isRequired, }); } else if (propDef.type === 'array') { dynamicProps[propName] = Property.Array({ displayName, description, required: isRequired, }); } else { // For objects or unknown types, use JSON dynamicProps[propName] = Property.Json({ displayName, description, required: isRequired, }); } }); return dynamicProps; } catch (error) { // Return empty object if dynamic props generation fails // This allows the form to load, error will surface during execution return {}; } } export const addToQueue = createAction({ name: 'add_to_queue', displayName: 'Add to Queue', description: 'Add an item to an existing queue and pause until execution time', props: { queueId: Property.Dropdown({ displayName: 'Select Queue', description: 'Available MCP tool queues (run Create/Update Queue first to create queues)', required: true, refreshers: [], options: async ({ auth }) => { const authConfig = auth as { mcpServerUrl?: string }; if (!authConfig.mcpServerUrl) { return { options: [], placeholder: 'MCP Server URL required in auth configuration' }; } try { const config = { mcpServerUrl: authConfig.mcpServerUrl }; const tools = await MCPManager.listMCPTools(config); if (tools.length === 0) { return { options: [], placeholder: 'No MCP tools found' }; } // Generate queue options based on available tools // The actual existence check will happen during execution const queueOptions = tools.map((tool: any) => ({ label: `Queue: ${tool.name} - ${tool.description || 'No description'}`, value: `mcp_${tool.name}_queue`, })); return { options: queueOptions, placeholder: 'Select a tool queue' }; } catch (error) { return { options: [], placeholder: `Failed to connect to MCP server: ${error instanceof Error ? error.message : String(error)}` }; } }, }), toolParameters: Property.DynamicProperties({ displayName: 'Tool Parameters', description: 'Parameters for the MCP tool (generated from tool schema)', required: false, refreshers: ['queueId'], props: generateDynamicProps, }), }, async run(context: ActionContext) { const { queueId, ...otherProps } = context.propsValue; if (!queueId) { throw new Error('Queue ID is required'); } const queue = await QueueManager.getQueueConfiguration( context, queueId as string, ); if (!queue) { throw new Error( `Queue "${queueId}" not found. Run Create/Update Queue first.`, ); } const limits = await QueueManager.checkRateLimits(context, queueId as string); const { delayMs } = await QueueManager.calculateItemDelay( context, queue, limits, ); // Extract the dynamic properties as action config (excluding queueId) const actionConfig = otherProps; const toolName = queue.mcpToolName; if (!toolName) { throw new Error('Queue configuration missing tool name'); } const { queueItemId, releaseTime } = await QueueManager.addItemToQueue( context, queueId as string, toolName, actionConfig, delayMs, ); await context.store.put( 'current_queue_item_id', queueItemId, StoreScope.PROJECT, ); await context.run.pause({ pauseMetadata: { type: PauseType.DELAY, resumeDateTime: new Date(releaseTime).toUTCString(), }, }); /* ============ RESUME ============ */ const storedQueueItemId = await context.store.get<string>( 'current_queue_item_id', StoreScope.PROJECT, ); if (!storedQueueItemId) { throw new Error('Could not find queue item ID after resume'); } return await executeQueuedItem(context, storedQueueItemId); }, });