donobu
Version:
Create browser automations with an LLM agent and replay them as Playwright scripts.
370 lines • 13.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.FlowsApi = void 0;
const v4_1 = require("zod/v4");
const CodeGenerationOptions_1 = require("../models/CodeGenerationOptions");
const CreateDonobuFlow_1 = require("../models/CreateDonobuFlow");
const FlowMetadata_1 = require("../models/FlowMetadata");
/**
* API controller for managing Donobu flows throughout their lifecycle.
*
* The FlowsApi provides endpoints for creating, querying, controlling, and managing
* Donobu flows. It handles flow metadata operations, code generation, project export,
* and runtime control operations like pause/resume/cancel.
*
* @remarks
* This class serves as the HTTP API layer for the {@link DonobuFlowsManager}, translating
* REST requests into appropriate manager method calls. All methods are designed to be
* used as Express.js route handlers.
*
* Flow States:
* - UNSTARTED: Flow created but not yet initialized
* - INITIALIZING: Setting up browser context and initial state
* - RUNNING_ACTION: Executing a tool call
* - QUERYING_LLM_FOR_NEXT_ACTION: AI determining next action (AUTONOMOUS mode)
* - WAITING_ON_USER_FOR_NEXT_ACTION: Waiting for user input (INSTRUCT mode)
* - PAUSED: Flow execution temporarily suspended
* - RESUMING: Transitioning from paused to active state
* - FAILED: Flow terminated unsuccessfully
* - SUCCESS: Flow completed successfully
*/
class FlowsApi {
constructor(donobuFlowsManager) {
this.donobuFlowsManager = donobuFlowsManager;
}
/**
* Generates Playwright code that can replay a completed flow.
*
* Creates a Node.js Microsoft Playwright script that reproduces the actions
* taken during the specified flow. The generated code uses the Donobu Playwright
* extension and can be executed independently.
*
* @throws {@link FlowNotFoundException} When the specified flow doesn't exist
*
* @example
* ```http
* GET /api/flows/abc123/code
*
* Response:
* {
* "script": "import { test } from 'donobu';\n\ntest('My Flow', async ({ page }) => {\n await page.goto('https://example.com');\n // ... more actions\n});"
* }
* ```
*/
async getFlowAsCode(req, res) {
const flowId = String(req.params.flowId);
const options = CodeGenerationOptions_1.CodeGenerationOptionsSchema.optional().parse(req.query);
const draftedPlaywrightScript = await this.donobuFlowsManager.getFlowAsPlaywrightScript(flowId, options ?? FlowsApi.DEFAULT_CODE_OPTIONS);
res.send({ script: draftedPlaywrightScript });
}
/**
* Returns the log snapshot for a flow. For running flows, returns the live
* in-memory buffer. For completed flows, returns the persisted snapshot.
*/
async getFlowLogs(req, res) {
const flowId = String(req.params.flowId);
const snapshot = await this.donobuFlowsManager.getFlowLogs(flowId);
res.json(snapshot);
}
/**
* Generates a complete Playwright project structure for multiple flows.
*
* Creates a full Playwright project with proper dependency management, configuration,
* and test files. Automatically resolves and includes flow dependencies to ensure
* a complete, runnable project.
*
* @throws {@link FlowNotFoundException} When any specified flow doesn't exist
*
* @example
* ```http
* POST /api/flows/project
* Content-Type: application/json
*
* {
* "flowIds": [ "flowId1", "flowId2", "flowId3" ],
* "options": {
* "areElementIdsVolatile": true,
* "disableSelectorFailover": true
* }
* }
*
* Response:
* {
* "files": [
* {
* "path": "tests/login-flow.spec.ts",
* "content": "import { test } from 'donobu';\n..."
* },
* {
* "path": "playwright.config.ts",
* "content": "import { defineConfig } from 'donobu';\n..."
* }
* ]
* }
* ```
*/
async getFlowsAsProject(req, res) {
const parsedBody = v4_1.z
.object({
flowIds: v4_1.z.array(v4_1.z.string()),
options: CodeGenerationOptions_1.CodeGenerationOptionsSchema.optional(),
})
.parse(req.body);
const project = await this.donobuFlowsManager.getFlowsAsPlaywrightProject(parsedBody.flowIds, parsedBody.options ?? FlowsApi.DEFAULT_CODE_OPTIONS);
res.json({
files: project.files,
});
}
/**
* Converts a flow into a rerunnable format.
*
* Returns a {@link CreateDonobuFlow} object that can be used to replay the specified
* flow deterministically. The returned parameters include all original tool calls
* configured for deterministic execution.
*
* @throws {@link FlowNotFoundException} When the specified flow doesn't exist
*
* @example
* ```http
* GET /api/flows/abc123/rerun
*
* Response: CreateDonobuFlow object ready for POST to /api/flows
* ```
*/
async getFlowAsRerun(req, res) {
const flowId = String(req.params.flowId);
const createDonobuFlow = await this.donobuFlowsManager.getFlowAsRerun(flowId, {
areElementIdsVolatile: false,
disableSelectorFailover: false,
});
res.json(createDonobuFlow);
}
/**
* Retrieves paginated flows with optional filtering.
*
* Returns a list of flow metadata objects matching the specified criteria.
* Supports pagination, filtering by name/state/runMode, and time-based filtering.
* Results are ordered by startedAt timestamp in descending order (newest first).
*
* @throws {@link InvalidParamValueException} When query parameters are invalid
*
* @example
* ```http
* GET /api/flows?limit=10&state=SUCCESS&startedAfter=1640995200000
*
* Response:
* {
* "flows": [
* {
* "id": "abc123",
* "name": "Login Test",
* "state": "SUCCESS",
* "startedAt": 1641000000000,
* // ... other metadata
* }
* ],
* "nextPageToken": "eyJ..."
* }
* ```
*
* Query Parameters:
* - `pageToken`: Pagination token for subsequent pages
* - `limit`: Maximum results per page (1-100, default varies)
* - `name`: Filter by exact flow name
* - `partialName`: Filter by partial flow name
* - `runMode`: Filter by execution mode (AUTONOMOUS|INSTRUCT|DETERMINISTIC)
* - `state`: Filter by current state
* - `startedAfter`: Unix timestamp - flows started after this time
* - `startedBefore`: Unix timestamp - flows started before this time
* - `sortBy`: Column to sort results by (created_at, name, run_mode, state) (default: created_at)
* - `sortOrder`: Sort direction (asc, desc) (default: desc)
*/
async getFlows(req, res) {
const query = FlowMetadata_1.FlowsQuerySchema.parse(req.query);
const flows = await this.donobuFlowsManager.getFlows(query);
res.json({ flows: flows.items, nextPageToken: flows.nextPageToken });
}
/**
* Creates and starts a new Donobu flow.
*
* Validates the provided parameters, creates a new flow instance, and begins
* execution according to the specified run mode. The flow runs asynchronously
* after this method returns.
*
* @throws {@link InvalidParamValueException} When flow parameters are invalid
* @throws {@link ToolRequiresGptException} When tools require GPT but none configured
*
* @example
* ```http
* POST /api/flows
* Content-Type: application/json
*
* {
* "targetWebsite": "https://example.com",
* "overallObjective": "Sign up for an account",
* "initialRunMode": "AUTONOMOUS",
* "maxToolCalls": 30
* }
*
* Response: FlowMetadata object with state "UNSTARTED" or "INITIALIZING"
* ```
*/
async createFlow(req, res) {
const flowParams = CreateDonobuFlow_1.CreateDonobuFlowSchema.parse(req.body);
const flow = (await this.donobuFlowsManager.createFlow(flowParams))
.donobuFlow;
res.json(flow.metadata);
}
/**
* Updates the name of an existing flow.
*
* Changes the display name of the specified flow. The name is used for
* identification and organization purposes and can be null to clear the name.
*
* @throws {@link FlowNotFoundException} When the specified flow doesn't exist.
* @throws {@link InvalidParamValueException} When the name is invalid.
*
* @example
* ```http
* POST /api/flows/abc123/rename
* Content-Type: application/json
*
* {
* "name": "Updated Flow Name"
* }
* ```
*/
async renameFlow(req, res) {
const flowId = String(req.params.flowId);
const flowParams = v4_1.z.object({ name: v4_1.z.string() }).parse(req.body);
const metadata = await this.donobuFlowsManager.renameFlow(flowId, flowParams.name);
res.json(metadata);
}
/**
* Permanently deletes a flow and all associated data.
*
* Removes the flow from storage including metadata, tool calls, screenshots,
* and video recordings. This operation cannot be undone.
*
* @throws {@link FlowNotFoundException} When the specified flow doesn't exist
* @throws {@link CannotDeleteRunningFlowException} When attempting to delete an active flow
*
* @example
* ```http
* DELETE /api/flows/abc123
*
* Response: HTTP 200 OK
* ```
*/
async deleteFlow(req, res) {
const flowId = String(req.params.flowId);
await this.donobuFlowsManager.deleteFlowById(flowId);
res.sendStatus(200);
}
/**
* Retrieves complete metadata for a specific flow.
*
* Returns the full {@link FlowMetadata} object containing current state,
* configuration, execution history, and results.
*
* @throws {@link FlowNotFoundException} When the specified flow doesn't exist
*
* @example
* ```http
* GET /api/flows/abc123
*
* Response: Complete FlowMetadata object
* ```
*/
async getFlowMetadata(req, res) {
const flowId = String(req.params.flowId);
const metadata = await this.donobuFlowsManager.getFlowById(flowId);
res.json(metadata);
}
/**
* Pauses execution of an active flow.
*
* Signals the flow to pause after completing its current action. The flow
* will transition to PAUSED state and can be resumed later. Only works
* for flows that are not already in a terminal state.
*
* @throws {@link ActiveFlowNotFoundException} When the flow is not active locally
*
* @example
* ```http
* POST /api/flows/abc123/pause
*
* Response: FlowMetadata with nextState set to "PAUSED"
* ```
*
* @remarks
* This endpoint is only available in LOCAL deployment environments for security.
* The flow will complete its current action before pausing.
*/
async pauseFlow(req, res) {
const flowId = String(req.params.flowId);
const flow = this.donobuFlowsManager.getActiveFlow(flowId);
if (!(0, FlowMetadata_1.isComplete)(flow.metadata.state)) {
flow.metadata.nextState = 'PAUSED';
}
res.json(flow.metadata);
}
/**
* Resumes execution of a paused flow.
*
* Signals a paused flow to continue execution. The flow will transition
* from PAUSED to RESUMING state and then continue normal operation.
*
* @throws {@link ActiveFlowNotFoundException} When the flow is not active locally
*
* @example
* ```http
* POST /api/flows/abc123/resume
*
* Response: FlowMetadata with nextState set to "RESUMING"
* ```
*
* @remarks
* This endpoint is only available in LOCAL deployment environments for security.
* Only works on flows currently in PAUSED state.
*/
async resumeFlow(req, res) {
const flowId = String(req.params.flowId);
const flow = this.donobuFlowsManager.getActiveFlow(flowId);
if (flow.metadata.state === 'PAUSED') {
flow.metadata.nextState = 'RESUMING';
}
res.json(flow.metadata);
}
/**
* Cancels and terminates an active flow.
*
* Forcibly stops flow execution and sets the state to FAILED. For active
* flows, this closes the browser context and terminates the execution thread.
*
* @throws {@link FlowNotFoundException} When the specified flow doesn't exist
*
* @example
* ```http
* POST /api/flows/abc123/cancel
*
* Response: FlowMetadata with state "FAILED"
* ```
*
* @remarks
* This endpoint is only available in LOCAL deployment environments for security.
* Cannot be undone - the flow will need to be recreated to retry.
*/
async cancelFlow(req, res) {
const flowId = String(req.params.flowId);
const metadata = await this.donobuFlowsManager.cancelFlow(flowId);
res.json(metadata);
}
}
exports.FlowsApi = FlowsApi;
FlowsApi.DEFAULT_CODE_OPTIONS = {
areElementIdsVolatile: false,
disableSelectorFailover: false,
playwrightScriptVariant: 'ai',
};
//# sourceMappingURL=FlowsApi.js.map