donobu
Version:
Create browser automations with an LLM agent and replay them as Playwright scripts.
270 lines • 12.1 kB
JavaScript
;
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