UNPKG

donobu

Version:

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

254 lines (253 loc) 10.9 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.getDonobuSqliteDatabase = getDonobuSqliteDatabase; exports.closeDonobuSqliteDatabase = closeDonobuSqliteDatabase; const path_1 = __importDefault(require("path")); const fs_1 = __importDefault(require("fs")); const better_sqlite3_1 = __importDefault(require("better-sqlite3")); const MiscUtils_1 = require("../utils/MiscUtils"); const Logger_1 = require("../utils/Logger"); let instance = null; /** * ################################################################## * # # * # WARNING! WARNING! WARNING! WARNING! WARNING! WARNING! # * # # * ################################################################## * * NEVER change existing migrations. This list is APPEND-ONLY! If you do not * abide by this, YOU WILL BREAK OUR USERS! */ const migrations = [ { // Setup the Donobu agent configs table. version: 1, up: (db) => { db.exec(` CREATE TABLE IF NOT EXISTS agent_configs ( agent TEXT PRIMARY KEY, config_name TEXT NULL ); INSERT OR IGNORE INTO agent_configs (agent, config_name) VALUES ('flow-runner', NULL);`); }, }, { version: 2, up: (db) => { // Setup the Donobu flows-related tables. db.exec(` CREATE TABLE IF NOT EXISTS flow_metadata ( id TEXT PRIMARY KEY, name TEXT NULL, metadata TEXT NOT NULL, created_at INTEGER NOT NULL, run_mode TEXT NOT NULL, state TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS tool_calls ( id TEXT PRIMARY KEY, flow_id TEXT NOT NULL, created_at INTEGER NOT NULL, tool_call TEXT NOT NULL, FOREIGN KEY (flow_id) REFERENCES flow_metadata(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS binary_files ( id TEXT PRIMARY KEY, flow_id TEXT NOT NULL, file_id TEXT NOT NULL, mime_type TEXT NOT NULL, content BLOB, FOREIGN KEY (flow_id) REFERENCES flow_metadata(id) ON DELETE CASCADE, UNIQUE(flow_id, file_id) ); -- Indices for better performance CREATE INDEX IF NOT EXISTS idx_flow_metadata_name ON flow_metadata(name); CREATE INDEX IF NOT EXISTS idx_flow_metadata_created_at ON flow_metadata(created_at); CREATE INDEX IF NOT EXISTS idx_flow_metadata_run_mode ON flow_metadata(run_mode); CREATE INDEX IF NOT EXISTS idx_flow_metadata_state ON flow_metadata(state); CREATE INDEX IF NOT EXISTS idx_flow_metadata_run_mode_state ON flow_metadata(run_mode, state); CREATE INDEX IF NOT EXISTS idx_tool_calls_flow_id_started ON tool_calls(flow_id, created_at); CREATE INDEX IF NOT EXISTS idx_tool_calls_created_at ON tool_calls(created_at); CREATE INDEX IF NOT EXISTS idx_binary_files_flow_file ON binary_files(flow_id, file_id); -- Enable foreign key support PRAGMA foreign_keys = ON;`); }, }, { // Setup the GPT configs table. version: 3, up: (db) => { db.exec(` CREATE TABLE IF NOT EXISTS gpt_configs ( name TEXT PRIMARY KEY, config TEXT NOT NULL );`); }, }, { // Update the flow JSON metadata schema. version: 4, up: (db) => { // For each row in the flow_metadata table... // 1. Load the metadata field. // 2. Parse it as JSON data. // 3. Update the JSON data such that... // a. There is a new field called "browser" that is an object. // b. The "initialBrowserState" field is moved to the "browser" object as "initialState". // c. The "persistBrowserState" field is moved to the "browser" object as "persistState". // d. There is a field in all "browser" objects called "using" that is an object. // e. If there is a non-null "remoteBrowserInstanceUrl" field, then... // i. The "remoteBrowserInstanceUrl" field is moved to the "using" object as "url". // ii. The "type" field in the "using" object is set to "remoteInstance". // f. If the "remoteBrowserInstanceUrl" field is null, then... // i. The "deviceName" field is moved to the "using" object. If "deviceName" is null, // then it is defaulted to "Desktop Chromium". // ii. The "headless" field is moved to the "using" object. // iii. The "type" field in the "using" object is set to "device". // g. The original top-level "initialBrowserState" field is removed. // h. The original top-level "persistBrowserState" field is removed. // i. The original top-level "remoteBrowserInstanceUrl" field is removed. // j. The original top-level "deviceName" field is removed. // k. The original top-level "headless" field is removed. // 4. Save the JSON data back to the metadata field. // Get all rows from the flow_metadata table. const flows = db .prepare('SELECT id, metadata FROM flow_metadata') .all(); // Prepare the update statement. const updateStmt = db.prepare('UPDATE flow_metadata SET metadata = ? WHERE id = ?'); // Process each flow's metadata. for (const flow of flows) { try { // Parse the metadata JSON. const metadata = JSON.parse(flow.metadata); // Create the new browser object structure. metadata.browser = { initialState: metadata.initialBrowserState || null, persistState: metadata.persistBrowserState || false, using: {}, }; // Determine the browser type and populate the 'using' object. if (metadata.remoteBrowserInstanceUrl) { // Remote browser instance configuration. metadata.browser.using = { type: 'remoteInstance', url: metadata.remoteBrowserInstanceUrl, }; } else { // Local device configuration. metadata.browser.using = { type: 'device', deviceName: metadata.deviceName || 'Desktop Chromium', headless: metadata.headless !== undefined ? metadata.headless : false, }; } // Remove the old fields. delete metadata.initialBrowserState; delete metadata.persistBrowserState; delete metadata.remoteBrowserInstanceUrl; delete metadata.deviceName; delete metadata.headless; // Update the database with the modified metadata. updateStmt.run(JSON.stringify(metadata), flow.id); } catch (error) { Logger_1.appLogger.error(`Error updating metadata for flow ${flow.id}:`, error); // Continue processing other flows even if one fails. } } }, }, ]; /** * Create the SQL schema migrations table that can be used to manage table * schemas across Donobu releases. */ function initMigrationsTable(db) { db.exec(` CREATE TABLE IF NOT EXISTS schema_migrations ( version INTEGER PRIMARY KEY, applied_at TEXT NOT NULL ); `); } function applyMigrations(db) { // First, validate that no duplicate version numbers exist in the migrations // array. const versionCounts = new Map(); const duplicateVersions = []; // Count occurrences of each version. for (const migration of migrations) { const count = (versionCounts.get(migration.version) || 0) + 1; versionCounts.set(migration.version, count); // If we've seen this version more than once, add it to duplicates. if (count > 1 && !duplicateVersions.includes(migration.version)) { duplicateVersions.push(migration.version); } } // If we found duplicates, throw an error. if (duplicateVersions.length > 0) { throw new Error(`Duplicate migration version(s) detected: ${duplicateVersions.join(', ')}. ` + 'Each migration must have a unique version number.'); } // Get current version. const getCurrentVersionStmt = db.prepare('SELECT MAX(version) as current_version FROM schema_migrations'); const result = getCurrentVersionStmt.get(); const currentVersion = result.current_version || 0; // Apply pending migrations in transaction. const pendingMigrations = migrations .filter((m) => m.version > currentVersion) .sort((a, b) => a.version - b.version); if (pendingMigrations.length > 0) { const transaction = db.transaction(() => { for (const migration of pendingMigrations) { Logger_1.appLogger.debug(`Applying migration ${migration.version}...`); migration.up(db); // Record migration. const recordMigrationStmt = db.prepare('INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)'); recordMigrationStmt.run(migration.version, new Date().toISOString()); } }); transaction(); } } /** * Returns the singleton SQLite database instance. * Creates the database on first call and initializes it with proper settings. */ function getDonobuSqliteDatabase() { if (instance) { return instance; } const dbDirectory = MiscUtils_1.MiscUtils.baseWorkingDirectory(); if (!fs_1.default.existsSync(dbDirectory)) { fs_1.default.mkdirSync(dbDirectory, { recursive: true }); } const dbFilepath = path_1.default.join(MiscUtils_1.MiscUtils.baseWorkingDirectory(), 'database.sqlite'); // Initialize database. const donobuSqliteDb = new better_sqlite3_1.default(dbFilepath); // Set up database configuration. donobuSqliteDb.pragma('journal_mode = WAL'); donobuSqliteDb.pragma('foreign_keys = ON'); // Set up tables. initMigrationsTable(donobuSqliteDb); applyMigrations(donobuSqliteDb); // Update global database handle. instance = donobuSqliteDb; return instance; } /** * Closes the database connection. Should be called during application shutdown. */ function closeDonobuSqliteDatabase() { if (instance) { instance.close(); instance = null; } } //# sourceMappingURL=DonobuSqliteDb.js.map