UNPKG

donobu

Version:

Create browser automations with an LLM agent and replay them as Playwright scripts.

270 lines 12.1 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.FlowsPersistenceFilesystem = void 0; const promises_1 = __importDefault(require("fs/promises")); const path_1 = __importDefault(require("path")); const FlowNotFoundException_1 = require("../exceptions/FlowNotFoundException"); const Logger_1 = require("../utils/Logger"); const JsonUtils_1 = require("../utils/JsonUtils"); /** * A persistence implementation that uses the filesystem. */ class FlowsPersistenceFilesystem { constructor(flowsDirectory) { this.flowsDirectory = flowsDirectory; } static async create(storageDirectory) { const flowsDirectory = path_1.default.join(storageDirectory, 'flows'); // Ensure directory exists await promises_1.default.mkdir(flowsDirectory, { recursive: true }).catch((err) => { Logger_1.appLogger.error('Failed to create flows directory', err); }); return new FlowsPersistenceFilesystem(flowsDirectory); } async saveMetadata(flowMetadata) { const flowDir = this.getFlowDirectory(flowMetadata.id); await promises_1.default.mkdir(flowDir, { recursive: true }); const filePath = path_1.default.join(flowDir, FlowsPersistenceFilesystem.METADATA_FILENAME); await promises_1.default.writeFile(filePath, JSON.stringify(flowMetadata, null, 2)); } async getMetadataByFlowId(flowId) { const filePath = path_1.default.join(this.getFlowDirectory(flowId), FlowsPersistenceFilesystem.METADATA_FILENAME); try { const content = await promises_1.default.readFile(filePath, 'utf-8'); const metadata = JSON.parse(content); if (typeof metadata.startedAt === 'string') { metadata.startedAt = Date.parse(metadata.startedAt) || 0; } if (typeof metadata.completedAt === 'string') { metadata.completedAt = Date.parse(metadata.completedAt) || 0; } return metadata; } catch (_e) { throw FlowNotFoundException_1.FlowNotFoundException.forId(flowId); } } /** * Get flow metadata by name using pagination for efficiency */ async getMetadataByFlowName(flowName) { if (!flowName) { throw FlowNotFoundException_1.FlowNotFoundException.forName('null'); } const batchSize = 100; let pageToken; do { // Get a batch of flows. const result = await this.getFlows({ limit: batchSize, pageToken, }); // Search in the current batch. const matchingFlow = result.items.find((flow) => flow.name === flowName); if (matchingFlow) { return matchingFlow; } // Continue to next page if available. pageToken = result.nextPageToken; } while (pageToken); // If we get here, we've searched all flows and found nothing. throw FlowNotFoundException_1.FlowNotFoundException.forName(flowName); } async savePngScreenShot(flowId, bytes) { const filename = `${new Date().toISOString()}${FlowsPersistenceFilesystem.SCREENSHOT_FILENAME_SUFFIX}`; const screenShotPath = path_1.default.join(this.getFlowDirectory(flowId), filename); await promises_1.default.writeFile(screenShotPath, bytes); return filename; } async getPngScreenShot(flowId, screenShotId) { return this.getFlowFile(flowId, screenShotId); } async saveToolCall(flowId, toolCall) { const filePath = path_1.default.join(this.getFlowDirectory(flowId), `${toolCall.startedAt}${FlowsPersistenceFilesystem.TOOL_CALL_FILENAME_SUFFIX}`); await promises_1.default.writeFile(filePath, JSON.stringify(toolCall, null, 2)); } async getToolCalls(flowId) { const flowDir = this.getFlowDirectory(flowId); try { const files = await promises_1.default.readdir(flowDir); const toolCallFiles = files.filter((file) => file.endsWith(FlowsPersistenceFilesystem.TOOL_CALL_FILENAME_SUFFIX)); const toolCalls = await Promise.all(toolCallFiles.sort().map(async (file) => { const content = await promises_1.default.readFile(path_1.default.join(flowDir, file), 'utf-8'); const toolCall = JSON.parse(content); if (typeof toolCall.startedAt === 'string') { toolCall.startedAt = Date.parse(toolCall.startedAt) || 0; } if (typeof toolCall.completedAt === 'string') { toolCall.completedAt = Date.parse(toolCall.completedAt) || 0; } return toolCall; })); return toolCalls.sort((a, b) => a.startedAt - b.startedAt); } catch (_e) { throw FlowNotFoundException_1.FlowNotFoundException.forId(flowId); } } /** * Get flows with pagination and filtering from filesystem storage */ async getFlows(query) { const { limit = 20, pageToken, name, runMode, state, startedAfter, startedBefore, } = query || {}; // Validate inputs. const validLimit = Math.min(Math.max(1, limit), 100); // Get all flow directories. const flowDirs = await this.getAllFlowDirectories(); // Load metadata for all flows. const metadataPromises = flowDirs.map(async (dir) => { try { const metadataPath = path_1.default.join(dir, FlowsPersistenceFilesystem.METADATA_FILENAME); const content = await promises_1.default.readFile(metadataPath, 'utf-8'); const metadata = JSON.parse(content); if (typeof metadata.startedAt === 'string') { metadata.startedAt = Date.parse(metadata.startedAt) || 0; } if (typeof metadata.completedAt === 'string') { metadata.completedAt = Date.parse(metadata.completedAt) || 0; } return metadata; } catch (error) { Logger_1.appLogger.warn(`Failed to read metadata in ${dir}`, error); return null; } }); // Wait for all metadata to load. const allMetadata = (await Promise.all(metadataPromises)).filter((metadata) => metadata !== null); // Apply filters based on query parameters let filteredMetadata = allMetadata; if (name) { filteredMetadata = filteredMetadata.filter((flow) => flow.name === name); } if (runMode) { filteredMetadata = filteredMetadata.filter((flow) => flow.runMode === runMode); } if (state) { filteredMetadata = filteredMetadata.filter((flow) => flow.state === state); } // Apply startedAfter filter if (startedAfter !== undefined) { filteredMetadata = filteredMetadata.filter((flow) => (flow.startedAt || 0) >= startedAfter); } // Apply startedBefore filter if (startedBefore !== undefined) { filteredMetadata = filteredMetadata.filter((flow) => (flow.startedAt || 0) <= startedBefore); } // Sort by creation date (newest first). filteredMetadata.sort((a, b) => (b.startedAt || 0) - (a.startedAt || 0)); // Parse page token to get the offset. const offset = pageToken ? parseInt(pageToken, 10) : 0; // Apply pagination. const paginatedMetadata = filteredMetadata.slice(offset, offset + validLimit); // Check if there are more results. const hasMore = offset + validLimit < filteredMetadata.length; // Calculate next page token. const nextPageToken = hasMore ? (offset + validLimit).toString() : undefined; return { items: paginatedMetadata, nextPageToken, }; } async setVideo(flowId, bytes) { const videoPath = path_1.default.join(this.getFlowDirectory(flowId), FlowsPersistenceFilesystem.VIDEO_FILENAME); await promises_1.default.writeFile(videoPath, bytes); } async getVideoSegment(flowId, startOffset, length) { try { await this.getMetadataByFlowId(flowId); const videoPath = path_1.default.join(this.getFlowDirectory(flowId), FlowsPersistenceFilesystem.VIDEO_FILENAME); const stats = await promises_1.default.stat(videoPath); const totalLength = stats.size; const adjustedLength = Math.min(length, totalLength - startOffset); const buffer = Buffer.alloc(adjustedLength); const fileHandle = await promises_1.default.open(videoPath, 'r'); try { await fileHandle.read(buffer, 0, adjustedLength, startOffset); return { bytes: buffer, totalLength: totalLength, startOffset: startOffset, }; } finally { await fileHandle.close(); } } catch { return null; } } async getFlowFile(flowId, fileId) { try { await this.getMetadataByFlowId(flowId); const filePath = path_1.default.join(this.getFlowDirectory(flowId), fileId); return await promises_1.default.readFile(filePath); } catch { return null; } } async setFlowFile(flowId, fileId, fileBytes) { const filePath = path_1.default.join(this.getFlowDirectory(flowId), fileId); await promises_1.default.writeFile(filePath, fileBytes); } async setBrowserState(flowId, browserState) { const serializedBrowserState = Buffer.from(JSON.stringify(browserState), 'utf-8'); await this.setFlowFile(flowId, FlowsPersistenceFilesystem.BROWSER_STATE_FILENAME, serializedBrowserState); } async getBrowserState(flowId) { const browserStateRaw = await this.getFlowFile(flowId, FlowsPersistenceFilesystem.BROWSER_STATE_FILENAME); if (browserStateRaw) { const browserState = JsonUtils_1.JsonUtils.jsonStringToJsonObject(browserStateRaw.toString('utf-8')); if (browserState) { return browserState; } else { throw new Error(`Cannot load malformed browser state from flow ${flowId}`); } } else { return null; } } async deleteFlow(flowId) { const flowDirectory = this.getFlowDirectory(flowId); await promises_1.default.rm(flowDirectory, { recursive: true, force: true }); } getFlowDirectory(flowId) { if (!flowId) { throw FlowNotFoundException_1.FlowNotFoundException.forId(flowId); } return path_1.default.join(this.flowsDirectory, flowId); } async getAllFlowDirectories() { try { const entries = await promises_1.default.readdir(this.flowsDirectory, { withFileTypes: true, }); return entries .filter((entry) => entry.isDirectory()) .map((entry) => path_1.default.join(this.flowsDirectory, entry.name)); } catch (error) { Logger_1.appLogger.error('Failed to find flow root directory', error); throw error; } } } exports.FlowsPersistenceFilesystem = FlowsPersistenceFilesystem; FlowsPersistenceFilesystem.METADATA_FILENAME = 'metadata.json'; FlowsPersistenceFilesystem.BROWSER_STATE_FILENAME = 'browserstate.json'; FlowsPersistenceFilesystem.SCREENSHOT_FILENAME_SUFFIX = '.screenshot.png'; FlowsPersistenceFilesystem.TOOL_CALL_FILENAME_SUFFIX = '.tool-call.json'; FlowsPersistenceFilesystem.VIDEO_FILENAME = 'video.webm'; //# sourceMappingURL=FlowsPersistenceFilesystem.js.map