@monoes/monobrowse 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/package.json +75 -0
- package/dist/src/browser/adapters/index.d.ts.map +1 -1
- package/dist/src/browser/adapters/index.js +4 -0
- package/dist/src/browser/adapters/index.js.map +1 -1
- package/dist/src/browser/browser.js +2 -2
- package/dist/src/browser/browser.js.map +1 -1
- package/dist/src/browser/cdp.d.ts +6 -0
- package/dist/src/browser/cdp.d.ts.map +1 -1
- package/dist/src/browser/cdp.js +56 -1
- package/dist/src/browser/cdp.js.map +1 -1
- package/dist/src/browser/dashboard/api-types.d.ts +1 -1
- package/dist/src/browser/dashboard/server.d.ts +1 -1
- package/dist/src/browser/dashboard/server.d.ts.map +1 -1
- package/dist/src/browser/dashboard/server.js +204 -15
- package/dist/src/browser/dashboard/server.js.map +1 -1
- package/dist/src/browser/dashboard/ui.html +1811 -0
- package/dist/src/browser/playbook/browser-handlers.d.ts +3 -0
- package/dist/src/browser/playbook/browser-handlers.d.ts.map +1 -0
- package/dist/src/browser/playbook/browser-handlers.js +377 -0
- package/dist/src/browser/playbook/browser-handlers.js.map +1 -0
- package/dist/src/browser/playbook/builtin-handlers.d.ts +3 -0
- package/dist/src/browser/playbook/builtin-handlers.d.ts.map +1 -0
- package/dist/src/browser/playbook/builtin-handlers.js +364 -0
- package/dist/src/browser/playbook/builtin-handlers.js.map +1 -0
- package/dist/src/browser/playbook/index.d.ts +3 -0
- package/dist/src/browser/playbook/index.d.ts.map +1 -0
- package/dist/src/browser/playbook/index.js +3 -0
- package/dist/src/browser/playbook/index.js.map +1 -0
- package/dist/src/browser/playbook/store.d.ts +8 -0
- package/dist/src/browser/playbook/store.d.ts.map +1 -0
- package/dist/src/browser/playbook/store.js +87 -0
- package/dist/src/browser/playbook/store.js.map +1 -0
- package/dist/src/browser/screenshot.d.ts +2 -0
- package/dist/src/browser/screenshot.d.ts.map +1 -1
- package/dist/src/browser/screenshot.js +69 -2
- package/dist/src/browser/screenshot.js.map +1 -1
- package/dist/src/browser/types.d.ts.map +1 -1
- package/dist/src/browser/types.js +11 -0
- package/dist/src/browser/types.js.map +1 -1
- package/dist/src/cli/action.d.ts +3 -0
- package/dist/src/cli/action.d.ts.map +1 -0
- package/dist/src/cli/action.js +139 -0
- package/dist/src/cli/action.js.map +1 -0
- package/dist/src/cli/commands.d.ts +8 -0
- package/dist/src/cli/commands.d.ts.map +1 -0
- package/dist/src/cli/commands.js +3021 -0
- package/dist/src/cli/commands.js.map +1 -0
- package/dist/src/cli/output.d.ts +11 -0
- package/dist/src/cli/output.d.ts.map +1 -0
- package/dist/src/cli/output.js +33 -0
- package/dist/src/cli/output.js.map +1 -0
- package/dist/src/cli/platform.d.ts +7 -0
- package/dist/src/cli/platform.d.ts.map +1 -0
- package/dist/src/cli/platform.js +193 -0
- package/dist/src/cli/platform.js.map +1 -0
- package/dist/src/cli/playbook.d.ts +3 -0
- package/dist/src/cli/playbook.d.ts.map +1 -0
- package/dist/src/cli/playbook.js +265 -0
- package/dist/src/cli/playbook.js.map +1 -0
- package/dist/src/cli/types.d.ts +42 -0
- package/dist/src/cli/types.d.ts.map +1 -0
- package/dist/src/cli/types.js +2 -0
- package/dist/src/cli/types.js.map +1 -0
- package/dist/src/cli/workflow.d.ts +2 -0
- package/dist/src/cli/workflow.d.ts.map +1 -0
- package/dist/src/cli/workflow.js +3 -0
- package/dist/src/cli/workflow.js.map +1 -0
- package/dist/src/cli.d.ts +7 -0
- package/dist/src/cli.d.ts.map +1 -0
- package/dist/src/cli.js +187 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/index.d.ts +5 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +16 -1
- package/dist/src/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +24 -5
- package/src/browser/adapters/index.ts +4 -0
- package/src/browser/browser.ts +2 -2
- package/src/browser/cdp.ts +68 -1
- package/src/browser/dashboard/api-types.ts +1 -1
- package/src/browser/dashboard/server.ts +210 -17
- package/src/browser/dashboard/ui.html +1412 -45
- package/src/browser/playbook/browser-handlers.ts +449 -0
- package/src/browser/{workflow → playbook}/builtin-handlers.ts +35 -14
- package/src/browser/playbook/index.ts +2 -0
- package/src/browser/{workflow → playbook}/store.ts +14 -14
- package/src/browser/screenshot.ts +78 -3
- package/src/browser/types.ts +11 -0
- package/src/cli/action.ts +145 -0
- package/src/cli/commands.ts +2895 -0
- package/src/cli/output.ts +37 -0
- package/src/cli/platform.ts +211 -0
- package/src/cli/playbook.ts +280 -0
- package/src/cli/types.ts +47 -0
- package/src/cli.ts +208 -0
- package/src/index.ts +20 -1
- package/src/browser/workflow/engine.ts +0 -202
- package/src/browser/workflow/expression.ts +0 -107
- package/src/browser/workflow/index.ts +0 -5
- package/src/browser/workflow/types.ts +0 -62
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
// createBrowserHandlers — CDP-backed NodeHandler implementations for @monoes/monoplaybook.
|
|
2
|
+
//
|
|
3
|
+
// browser.open — connect to Chrome CDP and open a URL
|
|
4
|
+
// browser.navigate — navigate the current tab to a URL
|
|
5
|
+
// browser.click — click an element (selector | role | text | label | placeholder)
|
|
6
|
+
// browser.type — type text into an element (keystroke-by-keystroke)
|
|
7
|
+
// browser.fill — set an input value directly (faster than type)
|
|
8
|
+
// browser.select — pick an <option> by value or visible text
|
|
9
|
+
// browser.check — check or uncheck a checkbox / radio
|
|
10
|
+
// browser.hover — hover over an element
|
|
11
|
+
// browser.press — press a key or combo (e.g. "Enter", "Ctrl+A")
|
|
12
|
+
// browser.scroll — scroll up/down/left/right by pixel amount
|
|
13
|
+
// browser.wait — wait for selector | url | load | networkidle | duration
|
|
14
|
+
// browser.extract — pull text, attribute, html, all_text, url, or title into item.data
|
|
15
|
+
// browser.snapshot — capture an accessibility tree snapshot
|
|
16
|
+
// browser.screenshot — take a screenshot (saves to file or returns base64)
|
|
17
|
+
// browser.evaluate — run arbitrary JS; result goes into item.data
|
|
18
|
+
// browser.close — drop the cached CDP connection (does not kill Chrome)
|
|
19
|
+
|
|
20
|
+
import type { NodeHandler, Item } from '@monoes/monoplaybook';
|
|
21
|
+
import { CdpClient } from '../cdp.js';
|
|
22
|
+
import {
|
|
23
|
+
connectToTarget,
|
|
24
|
+
openUrl,
|
|
25
|
+
waitForLoad,
|
|
26
|
+
getCurrentUrl,
|
|
27
|
+
getCurrentTitle,
|
|
28
|
+
} from '../browser.js';
|
|
29
|
+
import {
|
|
30
|
+
clickElement,
|
|
31
|
+
fillElement,
|
|
32
|
+
typeText,
|
|
33
|
+
pressKey,
|
|
34
|
+
pressKeyCombo,
|
|
35
|
+
scrollElement,
|
|
36
|
+
hoverElement,
|
|
37
|
+
selectOption,
|
|
38
|
+
checkElement,
|
|
39
|
+
evaluateJs,
|
|
40
|
+
} from '../actions.js';
|
|
41
|
+
import {
|
|
42
|
+
findBySelector,
|
|
43
|
+
findByRole,
|
|
44
|
+
findByText,
|
|
45
|
+
findByLabel,
|
|
46
|
+
findByPlaceholder,
|
|
47
|
+
} from '../find.js';
|
|
48
|
+
import { waitFor } from '../wait.js';
|
|
49
|
+
import { captureSnapshot } from '../snapshot.js';
|
|
50
|
+
import { captureScreenshot } from '../screenshot.js';
|
|
51
|
+
import type { ElementRef } from '../types.js';
|
|
52
|
+
import { mkdir } from 'node:fs/promises';
|
|
53
|
+
import { dirname, resolve } from 'node:path';
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Shared connection state — cached per CDP port across nodes in one playbook run
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
interface BrowserConnection {
|
|
60
|
+
client: CdpClient;
|
|
61
|
+
sessionId: string;
|
|
62
|
+
refs: Map<string, ElementRef>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const connectionCache = new Map<number, BrowserConnection>();
|
|
66
|
+
|
|
67
|
+
async function getConnection(port: number): Promise<BrowserConnection> {
|
|
68
|
+
if (connectionCache.has(port)) {
|
|
69
|
+
const cached = connectionCache.get(port)!;
|
|
70
|
+
try {
|
|
71
|
+
// Lightweight liveness check — if the CDP connection has dropped, this throws
|
|
72
|
+
await cached.client.send('Runtime.evaluate', { expression: '1', returnByValue: true }, cached.sessionId);
|
|
73
|
+
return cached;
|
|
74
|
+
} catch {
|
|
75
|
+
// Stale connection — evict and reconnect below
|
|
76
|
+
connectionCache.delete(port);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const { client, sessionId } = await connectToTarget(port);
|
|
80
|
+
const conn: BrowserConnection = { client, sessionId, refs: new Map() };
|
|
81
|
+
connectionCache.set(port, conn);
|
|
82
|
+
return conn;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function releaseConnection(port: number): void {
|
|
86
|
+
connectionCache.delete(port);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function safeResolvePath(rawPath: string): string {
|
|
90
|
+
const cwd = process.cwd();
|
|
91
|
+
const abs = resolve(cwd, rawPath);
|
|
92
|
+
if (abs !== cwd && !abs.startsWith(cwd + '/')) {
|
|
93
|
+
throw new Error(`Path traversal blocked: "${rawPath}" resolves outside working directory`);
|
|
94
|
+
}
|
|
95
|
+
return abs;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// Element resolution — locator types: selector | role | text | label | placeholder
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
async function findElement(
|
|
103
|
+
conn: BrowserConnection,
|
|
104
|
+
config: Record<string, unknown>,
|
|
105
|
+
): Promise<ElementRef> {
|
|
106
|
+
const locator = (config['locator'] as string | undefined) ?? 'selector';
|
|
107
|
+
const value = config['value'] as string;
|
|
108
|
+
if (!value) throw new Error(`browser handler requires config.value (the element locator)`);
|
|
109
|
+
|
|
110
|
+
const { client, sessionId, refs } = conn;
|
|
111
|
+
const opts = {
|
|
112
|
+
exact: config['exact'] as boolean | undefined,
|
|
113
|
+
nth: config['nth'] as number | undefined,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
let ref: ElementRef | null = null;
|
|
117
|
+
switch (locator) {
|
|
118
|
+
case 'selector': ref = await findBySelector(client, sessionId, refs, value); break;
|
|
119
|
+
case 'role': ref = await findByRole(client, sessionId, refs, value, opts); break;
|
|
120
|
+
case 'text': ref = await findByText(client, sessionId, refs, value, opts); break;
|
|
121
|
+
case 'label': ref = await findByLabel(client, sessionId, refs, value, opts); break;
|
|
122
|
+
case 'placeholder': ref = await findByPlaceholder(client, sessionId, refs, value, opts); break;
|
|
123
|
+
default: throw new Error(`Unknown locator "${locator}". Use: selector|role|text|label|placeholder`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!ref) throw new Error(`Element not found: ${locator}="${value}"`);
|
|
127
|
+
return ref;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Modifier bitmask used by CDP (matches DevTools protocol)
|
|
131
|
+
function modifierBits(modifiers: string[]): number {
|
|
132
|
+
let bits = 0;
|
|
133
|
+
for (const m of modifiers) {
|
|
134
|
+
switch (m.toLowerCase()) {
|
|
135
|
+
case 'alt': bits |= 1; break;
|
|
136
|
+
case 'ctrl':
|
|
137
|
+
case 'control': bits |= 2; break;
|
|
138
|
+
case 'meta':
|
|
139
|
+
case 'cmd': bits |= 4; break;
|
|
140
|
+
case 'shift': bits |= 8; break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return bits;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// Handlers
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
export function createBrowserHandlers(): Map<string, NodeHandler> {
|
|
151
|
+
const h = new Map<string, NodeHandler>();
|
|
152
|
+
|
|
153
|
+
// ── browser.open ──────────────────────────────────────────────────────────
|
|
154
|
+
h.set('browser.open', async (items, config): Promise<Item[]> => {
|
|
155
|
+
const port = (config['port'] as number | undefined) ?? 9222;
|
|
156
|
+
const url = config['url'] as string;
|
|
157
|
+
if (!url) throw new Error('browser.open requires config.url');
|
|
158
|
+
const timeout = (config['timeoutMs'] as number | undefined) ?? 30000;
|
|
159
|
+
const condition = (config['waitUntil'] as 'load' | 'networkidle' | 'domcontentloaded' | undefined) ?? 'load';
|
|
160
|
+
|
|
161
|
+
const conn = await getConnection(port);
|
|
162
|
+
await openUrl(conn.client, conn.sessionId, url);
|
|
163
|
+
await waitForLoad(conn.client, conn.sessionId, condition, timeout);
|
|
164
|
+
|
|
165
|
+
const currentUrl = await getCurrentUrl(conn.client, conn.sessionId);
|
|
166
|
+
const title = await getCurrentTitle(conn.client, conn.sessionId);
|
|
167
|
+
return items.map(item => ({ ...item, data: { ...item.data, url: currentUrl, title, port } }));
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// ── browser.navigate ──────────────────────────────────────────────────────
|
|
171
|
+
h.set('browser.navigate', async (items, config): Promise<Item[]> => {
|
|
172
|
+
const port = (config['port'] as number | undefined) ?? 9222;
|
|
173
|
+
const url = config['url'] as string;
|
|
174
|
+
if (!url) throw new Error('browser.navigate requires config.url');
|
|
175
|
+
const timeout = (config['timeoutMs'] as number | undefined) ?? 30000;
|
|
176
|
+
const condition = (config['waitUntil'] as 'load' | 'networkidle' | 'domcontentloaded' | undefined) ?? 'load';
|
|
177
|
+
|
|
178
|
+
const conn = await getConnection(port);
|
|
179
|
+
await openUrl(conn.client, conn.sessionId, url);
|
|
180
|
+
await waitForLoad(conn.client, conn.sessionId, condition, timeout);
|
|
181
|
+
|
|
182
|
+
const currentUrl = await getCurrentUrl(conn.client, conn.sessionId);
|
|
183
|
+
const title = await getCurrentTitle(conn.client, conn.sessionId);
|
|
184
|
+
return items.map(item => ({ ...item, data: { ...item.data, url: currentUrl, title } }));
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// ── browser.click ─────────────────────────────────────────────────────────
|
|
188
|
+
h.set('browser.click', async (items, config): Promise<Item[]> => {
|
|
189
|
+
const port = (config['port'] as number | undefined) ?? 9222;
|
|
190
|
+
const conn = await getConnection(port);
|
|
191
|
+
const ref = await findElement(conn, config);
|
|
192
|
+
await clickElement(conn.client, conn.sessionId, ref);
|
|
193
|
+
return items;
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// ── browser.type ──────────────────────────────────────────────────────────
|
|
197
|
+
h.set('browser.type', async (items, config): Promise<Item[]> => {
|
|
198
|
+
const port = (config['port'] as number | undefined) ?? 9222;
|
|
199
|
+
const text = config['text'] as string;
|
|
200
|
+
if (text === undefined) throw new Error('browser.type requires config.text');
|
|
201
|
+
|
|
202
|
+
const conn = await getConnection(port);
|
|
203
|
+
const ref = await findElement(conn, config);
|
|
204
|
+
if (config['clear'] !== false) {
|
|
205
|
+
await fillElement(conn.client, conn.sessionId, ref, '');
|
|
206
|
+
}
|
|
207
|
+
await typeText(conn.client, conn.sessionId, text);
|
|
208
|
+
return items;
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// ── browser.fill ──────────────────────────────────────────────────────────
|
|
212
|
+
h.set('browser.fill', async (items, config): Promise<Item[]> => {
|
|
213
|
+
const port = (config['port'] as number | undefined) ?? 9222;
|
|
214
|
+
const text = (config['text'] as string | undefined) ?? '';
|
|
215
|
+
|
|
216
|
+
const conn = await getConnection(port);
|
|
217
|
+
const ref = await findElement(conn, config);
|
|
218
|
+
await fillElement(conn.client, conn.sessionId, ref, text);
|
|
219
|
+
return items;
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// ── browser.select ────────────────────────────────────────────────────────
|
|
223
|
+
h.set('browser.select', async (items, config): Promise<Item[]> => {
|
|
224
|
+
const port = (config['port'] as number | undefined) ?? 9222;
|
|
225
|
+
const option = config['option'] as string;
|
|
226
|
+
if (!option) throw new Error('browser.select requires config.option');
|
|
227
|
+
|
|
228
|
+
const conn = await getConnection(port);
|
|
229
|
+
const ref = await findElement(conn, config);
|
|
230
|
+
await selectOption(conn.client, conn.sessionId, ref, option);
|
|
231
|
+
return items;
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// ── browser.check ─────────────────────────────────────────────────────────
|
|
235
|
+
h.set('browser.check', async (items, config): Promise<Item[]> => {
|
|
236
|
+
const port = (config['port'] as number | undefined) ?? 9222;
|
|
237
|
+
const checked = (config['checked'] as boolean | undefined) ?? true;
|
|
238
|
+
|
|
239
|
+
const conn = await getConnection(port);
|
|
240
|
+
const ref = await findElement(conn, config);
|
|
241
|
+
await checkElement(conn.client, conn.sessionId, ref, checked);
|
|
242
|
+
return items;
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// ── browser.hover ─────────────────────────────────────────────────────────
|
|
246
|
+
h.set('browser.hover', async (items, config): Promise<Item[]> => {
|
|
247
|
+
const port = (config['port'] as number | undefined) ?? 9222;
|
|
248
|
+
const conn = await getConnection(port);
|
|
249
|
+
const ref = await findElement(conn, config);
|
|
250
|
+
await hoverElement(conn.client, conn.sessionId, ref);
|
|
251
|
+
return items;
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// ── browser.press ─────────────────────────────────────────────────────────
|
|
255
|
+
// Config: port, key — e.g. "Enter", "Tab", "Escape", "Ctrl+A", "Shift+Tab"
|
|
256
|
+
h.set('browser.press', async (items, config): Promise<Item[]> => {
|
|
257
|
+
const port = (config['port'] as number | undefined) ?? 9222;
|
|
258
|
+
const key = config['key'] as string;
|
|
259
|
+
if (!key) throw new Error('browser.press requires config.key');
|
|
260
|
+
|
|
261
|
+
const conn = await getConnection(port);
|
|
262
|
+
if (key.includes('+')) {
|
|
263
|
+
const parts = key.split('+').map(k => k.trim());
|
|
264
|
+
const mainKey = parts[parts.length - 1];
|
|
265
|
+
const bits = modifierBits(parts.slice(0, -1));
|
|
266
|
+
// Delegate to pressKeyCombo which uses resolveKey() for correct code/key mapping
|
|
267
|
+
// (avoids KeyENTER-style bugs from naive Key${mainKey.toUpperCase()} construction)
|
|
268
|
+
await pressKeyCombo(conn.client, conn.sessionId, mainKey, bits);
|
|
269
|
+
} else {
|
|
270
|
+
await pressKey(conn.client, conn.sessionId, key);
|
|
271
|
+
}
|
|
272
|
+
return items;
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// ── browser.scroll ────────────────────────────────────────────────────────
|
|
276
|
+
// Config: port, direction (up|down|left|right, default "down"), amount (px, default 300),
|
|
277
|
+
// locator + value (optional — scroll a specific element)
|
|
278
|
+
h.set('browser.scroll', async (items, config): Promise<Item[]> => {
|
|
279
|
+
const port = (config['port'] as number | undefined) ?? 9222;
|
|
280
|
+
const direction = (config['direction'] as 'up' | 'down' | 'left' | 'right' | undefined) ?? 'down';
|
|
281
|
+
const amount = (config['amount'] as number | undefined) ?? 300;
|
|
282
|
+
|
|
283
|
+
const conn = await getConnection(port);
|
|
284
|
+
const hasLocator = !!(config['value']);
|
|
285
|
+
|
|
286
|
+
if (hasLocator) {
|
|
287
|
+
const ref = await findElement(conn, config);
|
|
288
|
+
await scrollElement(conn.client, conn.sessionId, direction, amount, ref);
|
|
289
|
+
} else {
|
|
290
|
+
await scrollElement(conn.client, conn.sessionId, direction, amount);
|
|
291
|
+
}
|
|
292
|
+
return items;
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// ── browser.wait ──────────────────────────────────────────────────────────
|
|
296
|
+
// Config: port, for (selector|url|load|networkidle|duration), value, timeoutMs
|
|
297
|
+
h.set('browser.wait', async (items, config): Promise<Item[]> => {
|
|
298
|
+
const port = (config['port'] as number | undefined) ?? 9222;
|
|
299
|
+
const conn = await getConnection(port);
|
|
300
|
+
const waitKind = (config['for'] as string | undefined) ?? 'duration';
|
|
301
|
+
const timeout = (config['timeoutMs'] as number | undefined) ?? 30000;
|
|
302
|
+
const value = config['value'];
|
|
303
|
+
|
|
304
|
+
switch (waitKind) {
|
|
305
|
+
case 'duration':
|
|
306
|
+
await new Promise(res => setTimeout(res, (value as number | undefined) ?? 1000));
|
|
307
|
+
break;
|
|
308
|
+
case 'selector':
|
|
309
|
+
await waitFor(conn.client, conn.sessionId, { selector: value as string, timeout });
|
|
310
|
+
break;
|
|
311
|
+
case 'url':
|
|
312
|
+
await waitFor(conn.client, conn.sessionId, { url: value as string, timeout });
|
|
313
|
+
break;
|
|
314
|
+
case 'load':
|
|
315
|
+
await waitForLoad(conn.client, conn.sessionId, 'load', timeout);
|
|
316
|
+
break;
|
|
317
|
+
case 'networkidle':
|
|
318
|
+
await waitForLoad(conn.client, conn.sessionId, 'networkidle', timeout);
|
|
319
|
+
break;
|
|
320
|
+
default:
|
|
321
|
+
throw new Error(`browser.wait: unknown 'for' value: "${waitKind}". Use selector|url|load|networkidle|duration`);
|
|
322
|
+
}
|
|
323
|
+
return items;
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// ── browser.extract ───────────────────────────────────────────────────────
|
|
327
|
+
// Config: port, as (text|attribute|html|all_text|url|title), locator, value,
|
|
328
|
+
// attribute (when as=attribute), outputKey (default "extracted")
|
|
329
|
+
h.set('browser.extract', async (items, config): Promise<Item[]> => {
|
|
330
|
+
const port = (config['port'] as number | undefined) ?? 9222;
|
|
331
|
+
const as = (config['as'] as string | undefined) ?? 'text';
|
|
332
|
+
const outputKey = (config['outputKey'] as string | undefined) ?? 'extracted';
|
|
333
|
+
const conn = await getConnection(port);
|
|
334
|
+
|
|
335
|
+
if (as === 'url') {
|
|
336
|
+
const url = await getCurrentUrl(conn.client, conn.sessionId);
|
|
337
|
+
return items.map(item => ({ ...item, data: { ...item.data, [outputKey]: url } }));
|
|
338
|
+
}
|
|
339
|
+
if (as === 'title') {
|
|
340
|
+
const title = await getCurrentTitle(conn.client, conn.sessionId);
|
|
341
|
+
return items.map(item => ({ ...item, data: { ...item.data, [outputKey]: title } }));
|
|
342
|
+
}
|
|
343
|
+
if (as === 'all_text') {
|
|
344
|
+
const text = await evaluateJs(conn.client, conn.sessionId, 'document.body.innerText');
|
|
345
|
+
return items.map(item => ({ ...item, data: { ...item.data, [outputKey]: text } }));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const ref = await findElement(conn, config);
|
|
349
|
+
|
|
350
|
+
if (as === 'text') {
|
|
351
|
+
const result = await conn.client.send<{ result: { value: unknown } }>(
|
|
352
|
+
'Runtime.callFunctionOn',
|
|
353
|
+
{ objectId: ref.objectId, functionDeclaration: 'function(){return this.innerText ?? this.textContent ?? ""}', returnByValue: true },
|
|
354
|
+
conn.sessionId,
|
|
355
|
+
);
|
|
356
|
+
return items.map(item => ({ ...item, data: { ...item.data, [outputKey]: result.result.value } }));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (as === 'attribute') {
|
|
360
|
+
const attr = config['attribute'] as string;
|
|
361
|
+
if (!attr) throw new Error('browser.extract with as=attribute requires config.attribute');
|
|
362
|
+
const result = await conn.client.send<{ result: { value: unknown } }>(
|
|
363
|
+
'Runtime.callFunctionOn',
|
|
364
|
+
{
|
|
365
|
+
objectId: ref.objectId,
|
|
366
|
+
functionDeclaration: `function(){return this.getAttribute(${JSON.stringify(attr)})}`,
|
|
367
|
+
returnByValue: true,
|
|
368
|
+
},
|
|
369
|
+
conn.sessionId,
|
|
370
|
+
);
|
|
371
|
+
return items.map(item => ({ ...item, data: { ...item.data, [outputKey]: result.result.value } }));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (as === 'html') {
|
|
375
|
+
const result = await conn.client.send<{ result: { value: unknown } }>(
|
|
376
|
+
'Runtime.callFunctionOn',
|
|
377
|
+
{ objectId: ref.objectId, functionDeclaration: 'function(){return this.outerHTML}', returnByValue: true },
|
|
378
|
+
conn.sessionId,
|
|
379
|
+
);
|
|
380
|
+
return items.map(item => ({ ...item, data: { ...item.data, [outputKey]: result.result.value } }));
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
throw new Error(`browser.extract: unknown 'as' value: "${as}". Use text|attribute|html|all_text|url|title`);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// ── browser.snapshot ──────────────────────────────────────────────────────
|
|
387
|
+
// Captures an accessibility tree. Output stored in item.data[outputKey].
|
|
388
|
+
// Config: port, selector (scope to element), interactiveOnly, compact, outputKey
|
|
389
|
+
h.set('browser.snapshot', async (items, config): Promise<Item[]> => {
|
|
390
|
+
const port = (config['port'] as number | undefined) ?? 9222;
|
|
391
|
+
const outputKey = (config['outputKey'] as string | undefined) ?? 'snapshot';
|
|
392
|
+
const conn = await getConnection(port);
|
|
393
|
+
|
|
394
|
+
const result = await captureSnapshot(conn.client, conn.sessionId, {
|
|
395
|
+
selector: config['selector'] as string | undefined,
|
|
396
|
+
interactiveOnly: (config['interactiveOnly'] as boolean | undefined) ?? false,
|
|
397
|
+
compact: (config['compact'] as boolean | undefined) ?? true,
|
|
398
|
+
});
|
|
399
|
+
return items.map(item => ({ ...item, data: { ...item.data, [outputKey]: result } }));
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
// ── browser.screenshot ────────────────────────────────────────────────────
|
|
403
|
+
// Config: port, path (optional file path), fullPage, format (jpeg|png|webp),
|
|
404
|
+
// quality, outputKey (default "screenshot")
|
|
405
|
+
// Output: if path is set → saves file and puts path in data; else → puts dataUrl in data + binaryBase64
|
|
406
|
+
h.set('browser.screenshot', async (items, config): Promise<Item[]> => {
|
|
407
|
+
const port = (config['port'] as number | undefined) ?? 9222;
|
|
408
|
+
const outputKey = (config['outputKey'] as string | undefined) ?? 'screenshot';
|
|
409
|
+
const conn = await getConnection(port);
|
|
410
|
+
|
|
411
|
+
const userPath = config['path'] as string | undefined;
|
|
412
|
+
const absPath = userPath ? safeResolvePath(userPath) : undefined;
|
|
413
|
+
if (absPath) await mkdir(dirname(absPath), { recursive: true });
|
|
414
|
+
|
|
415
|
+
const { path: savedPath, dataUrl } = await captureScreenshot(conn.client, conn.sessionId, {
|
|
416
|
+
path: absPath,
|
|
417
|
+
fullPage: (config['fullPage'] as boolean | undefined) ?? false,
|
|
418
|
+
format: (config['format'] as 'jpeg' | 'png' | 'webp' | undefined) ?? 'png',
|
|
419
|
+
quality: config['quality'] as number | undefined,
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
return items.map(item => ({
|
|
423
|
+
...item,
|
|
424
|
+
data: { ...item.data, [outputKey]: userPath ? savedPath : dataUrl },
|
|
425
|
+
}));
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// ── browser.evaluate ──────────────────────────────────────────────────────
|
|
429
|
+
// Config: port, script (JS expression), outputKey (default "result")
|
|
430
|
+
h.set('browser.evaluate', async (items, config): Promise<Item[]> => {
|
|
431
|
+
const port = (config['port'] as number | undefined) ?? 9222;
|
|
432
|
+
const script = config['script'] as string;
|
|
433
|
+
if (!script) throw new Error('browser.evaluate requires config.script');
|
|
434
|
+
const outputKey = (config['outputKey'] as string | undefined) ?? 'result';
|
|
435
|
+
|
|
436
|
+
const conn = await getConnection(port);
|
|
437
|
+
const result = await evaluateJs(conn.client, conn.sessionId, script);
|
|
438
|
+
return items.map(item => ({ ...item, data: { ...item.data, [outputKey]: result } }));
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// ── browser.close ─────────────────────────────────────────────────────────
|
|
442
|
+
h.set('browser.close', async (items, config): Promise<Item[]> => {
|
|
443
|
+
const port = (config['port'] as number | undefined) ?? 9222;
|
|
444
|
+
releaseConnection(port);
|
|
445
|
+
return items;
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
return h;
|
|
449
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
// Built-in node handlers registered for every `browse
|
|
1
|
+
// Built-in node handlers registered for every `browse playbook run` invocation.
|
|
2
2
|
//
|
|
3
3
|
// action.http — fetch a URL (GET/POST/etc.) and put the response in item.data
|
|
4
4
|
// action.save_file — write item data or binaryBase64 to a file on disk
|
|
5
|
-
// action.log — console.log each item (useful for debugging
|
|
5
|
+
// action.log — console.log each item (useful for debugging playbooks)
|
|
6
6
|
// action.gemini_image — generate image via Gemini web app (browser automation + session store)
|
|
7
7
|
// or Imagen REST API (GEMINI_API_KEY), or mock mode
|
|
8
8
|
import { writeFile, readFile, mkdir, chmod } from 'node:fs/promises';
|
|
@@ -19,8 +19,7 @@ function safeResolvePath(rawPath: string): string {
|
|
|
19
19
|
}
|
|
20
20
|
return safePath;
|
|
21
21
|
}
|
|
22
|
-
import type { NodeHandler } from '
|
|
23
|
-
import type { Item } from './types.js';
|
|
22
|
+
import type { NodeHandler, Item } from '@monoes/monoplaybook';
|
|
24
23
|
|
|
25
24
|
// ---------------------------------------------------------------------------
|
|
26
25
|
// Session persistence (shared format with browse-platform.ts)
|
|
@@ -150,7 +149,7 @@ async function generateViaGeminiBrowser(
|
|
|
150
149
|
console.log('│ A browser window has been opened to Gemini. │');
|
|
151
150
|
console.log('│ Please sign in with your Google account. │');
|
|
152
151
|
console.log('│ │');
|
|
153
|
-
console.log('│ The
|
|
152
|
+
console.log('│ The playbook will resume automatically after login. │');
|
|
154
153
|
console.log('│ Waiting up to 5 minutes… │');
|
|
155
154
|
console.log('└─────────────────────────────────────────────────────────┘');
|
|
156
155
|
console.log('');
|
|
@@ -238,24 +237,46 @@ export function createBuiltinHandlers(): Map<string, NodeHandler> {
|
|
|
238
237
|
const handlers = new Map<string, NodeHandler>();
|
|
239
238
|
|
|
240
239
|
// action.http
|
|
241
|
-
// config: { url, method?, headers?, body?, responseField? }
|
|
242
|
-
// Puts { statusCode, body, json? } into item.data[responseField ?? 'response']
|
|
240
|
+
// config: { url, method?, headers?, body?, responseField?, timeoutMs?, throwOnError? }
|
|
241
|
+
// Puts { statusCode, body, json? } into item.data[responseField ?? 'response'].
|
|
242
|
+
// throwOnError (default true): throws on 4xx/5xx responses.
|
|
243
|
+
// timeoutMs (default 30000): abort the request after this many ms.
|
|
243
244
|
handlers.set('action.http', async (items, config) => {
|
|
244
245
|
const url = String(config['url'] ?? '');
|
|
245
246
|
const method = String(config['method'] ?? 'GET').toUpperCase();
|
|
246
247
|
const headers = (config['headers'] as Record<string, string>) ?? {};
|
|
247
248
|
const body = config['body'] !== undefined ? JSON.stringify(config['body']) : undefined;
|
|
248
249
|
const responseField = String(config['responseField'] ?? 'response');
|
|
249
|
-
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
250
|
+
const timeoutMs = Number(config['timeoutMs'] ?? 30_000);
|
|
251
|
+
const throwOnError = config['throwOnError'] !== false; // default true
|
|
252
|
+
|
|
253
|
+
const controller = new AbortController();
|
|
254
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
255
|
+
|
|
256
|
+
let res: Response;
|
|
257
|
+
try {
|
|
258
|
+
res = await fetch(url, {
|
|
259
|
+
method,
|
|
260
|
+
headers: { 'Content-Type': 'application/json', ...headers },
|
|
261
|
+
body: method !== 'GET' && method !== 'HEAD' ? body : undefined,
|
|
262
|
+
signal: controller.signal,
|
|
263
|
+
});
|
|
264
|
+
} catch (err) {
|
|
265
|
+
clearTimeout(timer);
|
|
266
|
+
const msg = err instanceof Error && err.name === 'AbortError'
|
|
267
|
+
? `action.http: request timed out after ${timeoutMs}ms — ${url}`
|
|
268
|
+
: `action.http: network error — ${err instanceof Error ? err.message : String(err)}`;
|
|
269
|
+
throw new Error(msg);
|
|
270
|
+
}
|
|
271
|
+
clearTimeout(timer);
|
|
255
272
|
|
|
256
273
|
const text = await res.text();
|
|
257
274
|
let json: unknown;
|
|
258
|
-
try { json = JSON.parse(text); } catch { /* not JSON */ }
|
|
275
|
+
try { json = JSON.parse(text); } catch { /* not JSON — body is plain text or binary */ }
|
|
276
|
+
|
|
277
|
+
if (throwOnError && !res.ok) {
|
|
278
|
+
throw new Error(`action.http: HTTP ${res.status} ${res.statusText} — ${url}\n${text.slice(0, 500)}`);
|
|
279
|
+
}
|
|
259
280
|
|
|
260
281
|
return items.map(item => ({
|
|
261
282
|
...item,
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import type {
|
|
2
|
+
import type { PlaybookDef, RunRecord } from '@monoes/monoplaybook';
|
|
3
3
|
import type { ActionDef } from '../action-builder/types.js';
|
|
4
4
|
|
|
5
5
|
// TODO: Upgrade to better-sqlite3 for persistence across process restarts.
|
|
6
6
|
// Schema:
|
|
7
7
|
// CREATE TABLE browse_runs (
|
|
8
|
-
// id TEXT PRIMARY KEY,
|
|
8
|
+
// id TEXT PRIMARY KEY, playbook_id TEXT, playbook_name TEXT,
|
|
9
9
|
// status TEXT, started_at INTEGER, completed_at INTEGER,
|
|
10
10
|
// items_processed INTEGER, items_total INTEGER, error TEXT
|
|
11
11
|
// );
|
|
@@ -19,25 +19,25 @@ export function clearRunStore(): void {
|
|
|
19
19
|
runStore.clear();
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
export async function
|
|
22
|
+
export async function readPlaybook(filePath: string): Promise<PlaybookDef> {
|
|
23
23
|
let raw: string;
|
|
24
24
|
try {
|
|
25
25
|
raw = await readFile(filePath, 'utf-8');
|
|
26
26
|
} catch {
|
|
27
|
-
throw new Error(`
|
|
27
|
+
throw new Error(`Playbook file not found: ${filePath}`);
|
|
28
28
|
}
|
|
29
29
|
let def: unknown;
|
|
30
30
|
try {
|
|
31
31
|
def = JSON.parse(raw);
|
|
32
32
|
} catch {
|
|
33
|
-
throw new Error(`Invalid JSON in
|
|
33
|
+
throw new Error(`Invalid JSON in playbook file: ${filePath}`);
|
|
34
34
|
}
|
|
35
|
-
if (!
|
|
35
|
+
if (!isPlaybookDef(def)) {
|
|
36
36
|
const w = def as Record<string, unknown>;
|
|
37
|
-
if (typeof w?.['id'] !== 'string') throw new Error('
|
|
38
|
-
if (typeof w?.['name'] !== 'string') throw new Error('
|
|
39
|
-
if (!Array.isArray(w?.['nodes'])) throw new Error('
|
|
40
|
-
throw new Error('
|
|
37
|
+
if (typeof w?.['id'] !== 'string') throw new Error('Playbook missing required field: id');
|
|
38
|
+
if (typeof w?.['name'] !== 'string') throw new Error('Playbook missing required field: name');
|
|
39
|
+
if (!Array.isArray(w?.['nodes'])) throw new Error('Playbook missing required field: nodes');
|
|
40
|
+
throw new Error('Playbook missing required field: connections');
|
|
41
41
|
}
|
|
42
42
|
return def;
|
|
43
43
|
}
|
|
@@ -49,7 +49,7 @@ function validateAction(def: unknown): void {
|
|
|
49
49
|
if (!Array.isArray(a['steps'])) throw new Error('Action missing required field: steps');
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
function
|
|
52
|
+
function isPlaybookDef(def: unknown): def is PlaybookDef {
|
|
53
53
|
if (typeof def !== 'object' || def === null) return false;
|
|
54
54
|
const w = def as Record<string, unknown>;
|
|
55
55
|
return (
|
|
@@ -77,11 +77,11 @@ export async function readAction(filePath: string): Promise<ActionDef> {
|
|
|
77
77
|
return def as ActionDef;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
export async function
|
|
80
|
+
export async function writePlaybookRun(record: RunRecord): Promise<void> {
|
|
81
81
|
runStore.set(record.id, { ...record });
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
export async function
|
|
84
|
+
export async function listPlaybookRuns(playbookId?: string): Promise<RunRecord[]> {
|
|
85
85
|
const all = [...runStore.values()];
|
|
86
|
-
return
|
|
86
|
+
return playbookId ? all.filter(r => r.playbookId === playbookId) : all;
|
|
87
87
|
}
|