@monoes/monobrowse 1.0.1 → 1.0.2
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 +11 -15
- package/dist/src/browser/action-builder/analyzer.d.ts +0 -1
- package/dist/src/browser/action-builder/analyzer.d.ts.map +1 -1
- package/dist/src/browser/action-builder/analyzer.js +43 -21
- package/dist/src/browser/action-builder/analyzer.js.map +1 -1
- package/dist/src/browser/dashboard/server.d.ts +12 -1
- package/dist/src/browser/dashboard/server.d.ts.map +1 -1
- package/dist/src/browser/dashboard/server.js +4 -198
- package/dist/src/browser/dashboard/server.js.map +1 -1
- package/dist/src/cli/action.d.ts.map +1 -1
- package/dist/src/cli/action.js +5 -2
- package/dist/src/cli/action.js.map +1 -1
- package/dist/src/cli/commands.d.ts.map +1 -1
- package/dist/src/cli/commands.js +0 -3
- package/dist/src/cli/commands.js.map +1 -1
- package/dist/src/index.d.ts +0 -5
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +0 -16
- package/dist/src/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -15
- package/src/browser/action-builder/analyzer.ts +41 -23
- package/src/browser/dashboard/server.ts +16 -198
- package/src/cli/action.ts +6 -2
- package/src/cli/commands.ts +0 -3
- package/src/index.ts +0 -20
- package/src/browser/playbook/browser-handlers.ts +0 -449
- package/src/browser/playbook/builtin-handlers.ts +0 -400
- package/src/browser/playbook/index.ts +0 -2
- package/src/browser/playbook/store.ts +0 -87
- package/src/cli/playbook.ts +0 -280
|
@@ -1,400 +0,0 @@
|
|
|
1
|
-
// Built-in node handlers registered for every `browse playbook run` invocation.
|
|
2
|
-
//
|
|
3
|
-
// action.http — fetch a URL (GET/POST/etc.) and put the response in item.data
|
|
4
|
-
// action.save_file — write item data or binaryBase64 to a file on disk
|
|
5
|
-
// action.log — console.log each item (useful for debugging playbooks)
|
|
6
|
-
// action.gemini_image — generate image via Gemini web app (browser automation + session store)
|
|
7
|
-
// or Imagen REST API (GEMINI_API_KEY), or mock mode
|
|
8
|
-
import { writeFile, readFile, mkdir, chmod } from 'node:fs/promises';
|
|
9
|
-
import { existsSync } from 'node:fs';
|
|
10
|
-
import { dirname, join, resolve } from 'node:path';
|
|
11
|
-
import { homedir } from 'node:os';
|
|
12
|
-
|
|
13
|
-
/** Resolve a user-supplied path and assert it stays within the working directory. */
|
|
14
|
-
function safeResolvePath(rawPath: string): string {
|
|
15
|
-
const cwd = process.cwd();
|
|
16
|
-
const safePath = resolve(cwd, rawPath);
|
|
17
|
-
if (safePath !== cwd && !safePath.startsWith(cwd + '/')) {
|
|
18
|
-
throw new Error(`Path traversal blocked: "${rawPath}" resolves outside working directory`);
|
|
19
|
-
}
|
|
20
|
-
return safePath;
|
|
21
|
-
}
|
|
22
|
-
import type { NodeHandler, Item } from '@monoes/monoplaybook';
|
|
23
|
-
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
// Session persistence (shared format with browse-platform.ts)
|
|
26
|
-
// ---------------------------------------------------------------------------
|
|
27
|
-
|
|
28
|
-
const SESSIONS_FILE = join(homedir(), '.monomind', 'sessions.json');
|
|
29
|
-
|
|
30
|
-
interface StoredSession {
|
|
31
|
-
id: string;
|
|
32
|
-
platform: string;
|
|
33
|
-
username: string;
|
|
34
|
-
cookies: string; // JSON-serialized CDP cookie array
|
|
35
|
-
userAgent: string;
|
|
36
|
-
createdAt: number;
|
|
37
|
-
lastUsedAt: number;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async function loadStoredSession(platform: string): Promise<StoredSession | null> {
|
|
41
|
-
if (!existsSync(SESSIONS_FILE)) return null;
|
|
42
|
-
try {
|
|
43
|
-
const sessions: StoredSession[] = JSON.parse(await readFile(SESSIONS_FILE, 'utf-8'));
|
|
44
|
-
return sessions.find(s => s.platform === platform) ?? null;
|
|
45
|
-
} catch { return null; }
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async function saveStoredSession(
|
|
49
|
-
platform: string,
|
|
50
|
-
cookies: object[],
|
|
51
|
-
userAgent: string,
|
|
52
|
-
username: string,
|
|
53
|
-
): Promise<void> {
|
|
54
|
-
let sessions: StoredSession[] = [];
|
|
55
|
-
if (existsSync(SESSIONS_FILE)) {
|
|
56
|
-
try { sessions = JSON.parse(await readFile(SESSIONS_FILE, 'utf-8')); } catch { /* start fresh */ }
|
|
57
|
-
}
|
|
58
|
-
const id = `${platform}:${username}`;
|
|
59
|
-
const now = Date.now();
|
|
60
|
-
const existing = sessions.findIndex(s => s.platform === platform);
|
|
61
|
-
const entry: StoredSession = {
|
|
62
|
-
id, platform, username, userAgent,
|
|
63
|
-
cookies: JSON.stringify(cookies),
|
|
64
|
-
createdAt: existing >= 0 ? sessions[existing].createdAt : now,
|
|
65
|
-
lastUsedAt: now,
|
|
66
|
-
};
|
|
67
|
-
if (existing >= 0) sessions[existing] = entry; else sessions.push(entry);
|
|
68
|
-
await mkdir(join(homedir(), '.monomind'), { recursive: true });
|
|
69
|
-
await writeFile(SESSIONS_FILE, JSON.stringify(sessions, null, 2));
|
|
70
|
-
await chmod(SESSIONS_FILE, 0o600);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// ---------------------------------------------------------------------------
|
|
74
|
-
// Gemini browser automation
|
|
75
|
-
// ---------------------------------------------------------------------------
|
|
76
|
-
|
|
77
|
-
const GEMINI_IMAGE_CHECK = `
|
|
78
|
-
(() => {
|
|
79
|
-
const imgs = Array.from(document.querySelectorAll('img'));
|
|
80
|
-
return imgs.some(img =>
|
|
81
|
-
img.complete && img.naturalWidth > 200 && img.naturalHeight > 200 &&
|
|
82
|
-
img.src && !img.src.includes('icon') && !img.src.includes('avatar') &&
|
|
83
|
-
!img.src.includes('logo') && !img.src.includes('profile')
|
|
84
|
-
);
|
|
85
|
-
})()
|
|
86
|
-
`;
|
|
87
|
-
|
|
88
|
-
const GEMINI_IMAGE_SRC = `
|
|
89
|
-
(() => {
|
|
90
|
-
const imgs = Array.from(document.querySelectorAll('img'));
|
|
91
|
-
const c = imgs.filter(img =>
|
|
92
|
-
img.complete && img.naturalWidth > 200 && img.naturalHeight > 200 &&
|
|
93
|
-
img.src && !img.src.includes('icon') && !img.src.includes('avatar') &&
|
|
94
|
-
!img.src.includes('logo') && !img.src.includes('profile')
|
|
95
|
-
);
|
|
96
|
-
return c.length ? c[c.length - 1].src : '';
|
|
97
|
-
})()
|
|
98
|
-
`;
|
|
99
|
-
|
|
100
|
-
// Generate an image via the Gemini web app, with session management.
|
|
101
|
-
// Flow: connect → restore saved session → if not logged in, wait for user login → generate.
|
|
102
|
-
// Returns the saved file path, or null if Chrome is not available.
|
|
103
|
-
async function generateViaGeminiBrowser(
|
|
104
|
-
prompt: string,
|
|
105
|
-
outputPath: string,
|
|
106
|
-
cdpPort: number,
|
|
107
|
-
): Promise<string | null> {
|
|
108
|
-
let browser: typeof import('../index.js');
|
|
109
|
-
try { browser = await import('../index.js'); } catch { return null; }
|
|
110
|
-
|
|
111
|
-
let conn: Awaited<ReturnType<typeof browser.connectToTarget>>;
|
|
112
|
-
try { conn = await browser.connectToTarget(cdpPort); } catch { return null; }
|
|
113
|
-
|
|
114
|
-
const { client, sessionId } = conn;
|
|
115
|
-
const refs = new Map();
|
|
116
|
-
|
|
117
|
-
try {
|
|
118
|
-
// Restore stored Gemini session cookies before navigating
|
|
119
|
-
const stored = await loadStoredSession('gemini');
|
|
120
|
-
if (stored?.cookies) {
|
|
121
|
-
try {
|
|
122
|
-
const cookies = JSON.parse(stored.cookies) as object[];
|
|
123
|
-
if (cookies.length > 0) {
|
|
124
|
-
await browser.setCookies(client, sessionId, cookies as Parameters<typeof browser.setCookies>[2]);
|
|
125
|
-
}
|
|
126
|
-
} catch { /* ignore malformed cookies */ }
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Navigate to Gemini
|
|
130
|
-
await browser.openUrl(client, sessionId, 'https://gemini.google.com/app');
|
|
131
|
-
|
|
132
|
-
// Dismiss cookie consent if present
|
|
133
|
-
const refs2 = new Map();
|
|
134
|
-
const consentBtn = await browser.findByRole(client, sessionId, refs2, 'button', { name: 'Accept all' }).catch(() => null);
|
|
135
|
-
if (consentBtn) {
|
|
136
|
-
await browser.clickElement(client, sessionId, consentBtn);
|
|
137
|
-
await new Promise(r => setTimeout(r, 1500));
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Check if logged in — .ql-editor only appears when authenticated
|
|
141
|
-
let inputRef = await browser.findBySelector(client, sessionId, refs, '.ql-editor').catch(() => null);
|
|
142
|
-
|
|
143
|
-
if (!inputRef) {
|
|
144
|
-
// Not logged in — ask the user to authenticate
|
|
145
|
-
console.log('');
|
|
146
|
-
console.log('┌─────────────────────────────────────────────────────────┐');
|
|
147
|
-
console.log('│ Gemini login required │');
|
|
148
|
-
console.log('│ │');
|
|
149
|
-
console.log('│ A browser window has been opened to Gemini. │');
|
|
150
|
-
console.log('│ Please sign in with your Google account. │');
|
|
151
|
-
console.log('│ │');
|
|
152
|
-
console.log('│ The playbook will resume automatically after login. │');
|
|
153
|
-
console.log('│ Waiting up to 5 minutes… │');
|
|
154
|
-
console.log('└─────────────────────────────────────────────────────────┘');
|
|
155
|
-
console.log('');
|
|
156
|
-
|
|
157
|
-
// Navigate to Google sign-in
|
|
158
|
-
await browser.openUrl(client, sessionId, 'https://accounts.google.com/signin/v2/identifier');
|
|
159
|
-
|
|
160
|
-
// Wait up to 5 min for the user to complete login and land back on Gemini
|
|
161
|
-
const loginDeadline = Date.now() + 5 * 60 * 1000;
|
|
162
|
-
let loggedIn = false;
|
|
163
|
-
while (Date.now() < loginDeadline) {
|
|
164
|
-
await new Promise(r => setTimeout(r, 2000));
|
|
165
|
-
const url = await browser.getCurrentUrl(client, sessionId).catch(() => '');
|
|
166
|
-
if (url.includes('gemini.google.com')) {
|
|
167
|
-
inputRef = await browser.findBySelector(client, sessionId, refs, '.ql-editor').catch(() => null);
|
|
168
|
-
if (inputRef) { loggedIn = true; break; }
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (!loggedIn) {
|
|
173
|
-
console.log('[action.gemini_image] Login timeout. Run: monomind browse platform connect gemini');
|
|
174
|
-
return null;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
console.log('[action.gemini_image] Login detected! Saving session...');
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Save/refresh cookies after confirming login
|
|
181
|
-
const liveCookies = await browser.getCookies(client, sessionId).catch(() => []);
|
|
182
|
-
const userAgent = await browser.evaluateJs(client, sessionId, 'navigator.userAgent').catch(() => 'unknown') as string;
|
|
183
|
-
const username = await browser.evaluateJs(
|
|
184
|
-
client, sessionId,
|
|
185
|
-
"document.querySelector('.gb_A.gb_Sa')?.textContent?.trim() ?? 'gemini-user'"
|
|
186
|
-
).catch(() => 'gemini-user') as string;
|
|
187
|
-
await saveStoredSession('gemini', liveCookies, String(userAgent), String(username));
|
|
188
|
-
|
|
189
|
-
// Ensure we're on the app page with the input visible
|
|
190
|
-
if (!inputRef) {
|
|
191
|
-
await browser.openUrl(client, sessionId, 'https://gemini.google.com/app');
|
|
192
|
-
inputRef = await browser.findBySelector(client, sessionId, refs, '.ql-editor').catch(() => null);
|
|
193
|
-
}
|
|
194
|
-
if (!inputRef) return null;
|
|
195
|
-
|
|
196
|
-
// Fill and submit the prompt
|
|
197
|
-
await browser.fillElement(client, sessionId, inputRef, prompt);
|
|
198
|
-
await new Promise(r => setTimeout(r, 300));
|
|
199
|
-
await browser.pressKey(client, sessionId, 'Return');
|
|
200
|
-
console.log('[action.gemini_image] Prompt submitted to Gemini, waiting for image...');
|
|
201
|
-
|
|
202
|
-
// Poll up to 90s for generated image
|
|
203
|
-
const deadline = Date.now() + 90_000;
|
|
204
|
-
while (Date.now() < deadline) {
|
|
205
|
-
const found = await browser.evaluateJs(client, sessionId, GEMINI_IMAGE_CHECK).catch(() => false);
|
|
206
|
-
if (found) break;
|
|
207
|
-
await new Promise(r => setTimeout(r, 500));
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
await mkdir(dirname(outputPath), { recursive: true });
|
|
211
|
-
|
|
212
|
-
// Extract image URL and download
|
|
213
|
-
const imgSrc = (await browser.evaluateJs(client, sessionId, GEMINI_IMAGE_SRC).catch(() => '')) as string;
|
|
214
|
-
if (imgSrc && (imgSrc.startsWith('https://') || imgSrc.startsWith('http://'))) {
|
|
215
|
-
try {
|
|
216
|
-
const resp = await fetch(imgSrc);
|
|
217
|
-
if (resp.ok) {
|
|
218
|
-
await writeFile(outputPath, Buffer.from(await resp.arrayBuffer()));
|
|
219
|
-
console.log(`[action.gemini_image] Image saved → ${outputPath}`);
|
|
220
|
-
return outputPath;
|
|
221
|
-
}
|
|
222
|
-
} catch { /* fall through to screenshot */ }
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Fallback: screenshot the response
|
|
226
|
-
const screenshotPath = outputPath.replace(/\.(png|jpe?g|webp)$/i, '') + '-screenshot.png';
|
|
227
|
-
const ss = await browser.captureScreenshot(client, sessionId, { path: screenshotPath, fullPage: true }).catch(() => null);
|
|
228
|
-
if (ss) { console.log(`[action.gemini_image] Screenshot saved → ${ss.path}`); return ss.path; }
|
|
229
|
-
|
|
230
|
-
return null;
|
|
231
|
-
} finally {
|
|
232
|
-
client.close();
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
export function createBuiltinHandlers(): Map<string, NodeHandler> {
|
|
237
|
-
const handlers = new Map<string, NodeHandler>();
|
|
238
|
-
|
|
239
|
-
// action.http
|
|
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.
|
|
244
|
-
handlers.set('action.http', async (items, config) => {
|
|
245
|
-
const url = String(config['url'] ?? '');
|
|
246
|
-
const method = String(config['method'] ?? 'GET').toUpperCase();
|
|
247
|
-
const headers = (config['headers'] as Record<string, string>) ?? {};
|
|
248
|
-
const body = config['body'] !== undefined ? JSON.stringify(config['body']) : undefined;
|
|
249
|
-
const responseField = String(config['responseField'] ?? 'response');
|
|
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);
|
|
272
|
-
|
|
273
|
-
const text = await res.text();
|
|
274
|
-
let json: unknown;
|
|
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
|
-
}
|
|
280
|
-
|
|
281
|
-
return items.map(item => ({
|
|
282
|
-
...item,
|
|
283
|
-
data: {
|
|
284
|
-
...item.data,
|
|
285
|
-
[responseField]: { statusCode: res.status, body: text, json },
|
|
286
|
-
},
|
|
287
|
-
}));
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
// action.save_file
|
|
291
|
-
// config: { path, content?, field?, encoding? }
|
|
292
|
-
// Writes item.data[field] (or binaryBase64 decoded) to disk
|
|
293
|
-
handlers.set('action.save_file', async (items, config) => {
|
|
294
|
-
const results: Item[] = [];
|
|
295
|
-
for (const item of items) {
|
|
296
|
-
const outPath = safeResolvePath(String(config['path'] ?? './output.txt'));
|
|
297
|
-
const field = config['field'] as string | undefined;
|
|
298
|
-
const encoding = String(config['encoding'] ?? 'utf8');
|
|
299
|
-
|
|
300
|
-
await mkdir(dirname(outPath), { recursive: true });
|
|
301
|
-
|
|
302
|
-
if (item.binaryBase64) {
|
|
303
|
-
await writeFile(outPath, Buffer.from(item.binaryBase64, 'base64'));
|
|
304
|
-
} else {
|
|
305
|
-
const content = field
|
|
306
|
-
? JSON.stringify(item.data[field] ?? '', null, 2)
|
|
307
|
-
: (config['content'] as string ?? JSON.stringify(item.data, null, 2));
|
|
308
|
-
await writeFile(outPath, content, encoding as BufferEncoding);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
results.push({ ...item, data: { ...item.data, savedPath: outPath } });
|
|
312
|
-
}
|
|
313
|
-
return results;
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
// action.log
|
|
317
|
-
// config: { label? }
|
|
318
|
-
handlers.set('action.log', async (items, config) => {
|
|
319
|
-
const label = String(config['label'] ?? 'action.log');
|
|
320
|
-
for (const item of items) {
|
|
321
|
-
console.log(`[${label}]`, JSON.stringify(item.data, null, 2));
|
|
322
|
-
}
|
|
323
|
-
return items;
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
// action.gemini_image
|
|
327
|
-
// config: { prompt, cdpPort?, outputPath?, apiKey?, model?, aspectRatio? }
|
|
328
|
-
// Priority: (1) Gemini web browser via CDP port 9222, (2) Imagen REST API, (3) mock
|
|
329
|
-
handlers.set('action.gemini_image', async (items, config) => {
|
|
330
|
-
const cdpPort = Number(config['cdpPort'] ?? process.env['GEMINI_CDP_PORT'] ?? 9222);
|
|
331
|
-
const apiKey = String(config['apiKey'] ?? process.env['GEMINI_API_KEY'] ?? process.env['GOOGLE_API_KEY'] ?? '');
|
|
332
|
-
const model = String(config['model'] ?? 'imagen-3.0-generate-001');
|
|
333
|
-
const aspectRatio = String(config['aspectRatio'] ?? '1:1');
|
|
334
|
-
const outputPath = config['outputPath'] as string | undefined;
|
|
335
|
-
|
|
336
|
-
const results: Item[] = [];
|
|
337
|
-
|
|
338
|
-
for (const item of items) {
|
|
339
|
-
const prompt = String(config['prompt'] ?? item.data['prompt'] ?? '');
|
|
340
|
-
const filePath = safeResolvePath(outputPath ?? `./output/gemini-image-${Date.now()}.png`);
|
|
341
|
-
|
|
342
|
-
// Priority 1: Browser automation via authenticated Chrome session
|
|
343
|
-
const browserPath = await generateViaGeminiBrowser(prompt, filePath, cdpPort);
|
|
344
|
-
if (browserPath) {
|
|
345
|
-
results.push({
|
|
346
|
-
...item,
|
|
347
|
-
data: { ...item.data, prompt, generatedImagePath: browserPath, source: 'gemini-browser' },
|
|
348
|
-
});
|
|
349
|
-
continue;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// Priority 2: Gemini Imagen REST API
|
|
353
|
-
if (apiKey) {
|
|
354
|
-
console.log(`[action.gemini_image] Browser unavailable — trying Imagen REST API`);
|
|
355
|
-
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:predict?key=${apiKey}`;
|
|
356
|
-
const res = await fetch(url, {
|
|
357
|
-
method: 'POST',
|
|
358
|
-
headers: { 'Content-Type': 'application/json' },
|
|
359
|
-
body: JSON.stringify({
|
|
360
|
-
instances: [{ prompt }],
|
|
361
|
-
parameters: { sampleCount: 1, aspectRatio },
|
|
362
|
-
}),
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
if (!res.ok) throw new Error(`Gemini Imagen API error ${res.status}: ${await res.text()}`);
|
|
366
|
-
|
|
367
|
-
const data = await res.json() as { predictions?: { bytesBase64Encoded?: string; mimeType?: string }[] };
|
|
368
|
-
const prediction = data.predictions?.[0];
|
|
369
|
-
if (!prediction?.bytesBase64Encoded) throw new Error('No image data in Gemini response');
|
|
370
|
-
|
|
371
|
-
await mkdir(dirname(filePath), { recursive: true });
|
|
372
|
-
await writeFile(filePath, Buffer.from(prediction.bytesBase64Encoded, 'base64'));
|
|
373
|
-
|
|
374
|
-
results.push({
|
|
375
|
-
...item,
|
|
376
|
-
data: { ...item.data, prompt, generatedImagePath: filePath, mimeType: prediction.mimeType ?? 'image/png', source: 'gemini-api' },
|
|
377
|
-
binaryBase64: prediction.bytesBase64Encoded,
|
|
378
|
-
});
|
|
379
|
-
continue;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// Priority 3: Mock mode — no browser on port 9222 and no API key
|
|
383
|
-
console.log(`[action.gemini_image] No browser on port ${cdpPort} and no API key — mock mode.`);
|
|
384
|
-
console.log(` Prompt: "${prompt}"`);
|
|
385
|
-
results.push({
|
|
386
|
-
...item,
|
|
387
|
-
data: {
|
|
388
|
-
...item.data,
|
|
389
|
-
prompt,
|
|
390
|
-
mockMode: true,
|
|
391
|
-
note: `Set GEMINI_CDP_PORT env var (default: 9222) for browser mode, or GEMINI_API_KEY for REST API`,
|
|
392
|
-
},
|
|
393
|
-
});
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
return results;
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
return handlers;
|
|
400
|
-
}
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import type { PlaybookDef, RunRecord } from '@monoes/monoplaybook';
|
|
3
|
-
import type { ActionDef } from '../action-builder/types.js';
|
|
4
|
-
|
|
5
|
-
// TODO: Upgrade to better-sqlite3 for persistence across process restarts.
|
|
6
|
-
// Schema:
|
|
7
|
-
// CREATE TABLE browse_runs (
|
|
8
|
-
// id TEXT PRIMARY KEY, playbook_id TEXT, playbook_name TEXT,
|
|
9
|
-
// status TEXT, started_at INTEGER, completed_at INTEGER,
|
|
10
|
-
// items_processed INTEGER, items_total INTEGER, error TEXT
|
|
11
|
-
// );
|
|
12
|
-
// CREATE TABLE browse_sessions (
|
|
13
|
-
// id TEXT PRIMARY KEY, platform TEXT, username TEXT,
|
|
14
|
-
// cookies TEXT, user_agent TEXT, created_at INTEGER, last_used_at INTEGER
|
|
15
|
-
// );
|
|
16
|
-
const runStore = new Map<string, RunRecord>();
|
|
17
|
-
|
|
18
|
-
export function clearRunStore(): void {
|
|
19
|
-
runStore.clear();
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export async function readPlaybook(filePath: string): Promise<PlaybookDef> {
|
|
23
|
-
let raw: string;
|
|
24
|
-
try {
|
|
25
|
-
raw = await readFile(filePath, 'utf-8');
|
|
26
|
-
} catch {
|
|
27
|
-
throw new Error(`Playbook file not found: ${filePath}`);
|
|
28
|
-
}
|
|
29
|
-
let def: unknown;
|
|
30
|
-
try {
|
|
31
|
-
def = JSON.parse(raw);
|
|
32
|
-
} catch {
|
|
33
|
-
throw new Error(`Invalid JSON in playbook file: ${filePath}`);
|
|
34
|
-
}
|
|
35
|
-
if (!isPlaybookDef(def)) {
|
|
36
|
-
const w = def as Record<string, unknown>;
|
|
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
|
-
}
|
|
42
|
-
return def;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function validateAction(def: unknown): void {
|
|
46
|
-
if (typeof def !== 'object' || def === null) throw new Error('Action must be a JSON object');
|
|
47
|
-
const a = def as Record<string, unknown>;
|
|
48
|
-
if (typeof a['id'] !== 'string') throw new Error('Action missing required field: id');
|
|
49
|
-
if (!Array.isArray(a['steps'])) throw new Error('Action missing required field: steps');
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function isPlaybookDef(def: unknown): def is PlaybookDef {
|
|
53
|
-
if (typeof def !== 'object' || def === null) return false;
|
|
54
|
-
const w = def as Record<string, unknown>;
|
|
55
|
-
return (
|
|
56
|
-
typeof w['id'] === 'string' &&
|
|
57
|
-
typeof w['name'] === 'string' &&
|
|
58
|
-
Array.isArray(w['nodes']) &&
|
|
59
|
-
Array.isArray(w['connections'])
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export async function readAction(filePath: string): Promise<ActionDef> {
|
|
64
|
-
let raw: string;
|
|
65
|
-
try {
|
|
66
|
-
raw = await readFile(filePath, 'utf-8');
|
|
67
|
-
} catch {
|
|
68
|
-
throw new Error(`Action file not found: ${filePath}`);
|
|
69
|
-
}
|
|
70
|
-
let def: unknown;
|
|
71
|
-
try {
|
|
72
|
-
def = JSON.parse(raw);
|
|
73
|
-
} catch {
|
|
74
|
-
throw new Error(`Invalid JSON in action file: ${filePath}`);
|
|
75
|
-
}
|
|
76
|
-
validateAction(def);
|
|
77
|
-
return def as ActionDef;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export async function writePlaybookRun(record: RunRecord): Promise<void> {
|
|
81
|
-
runStore.set(record.id, { ...record });
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export async function listPlaybookRuns(playbookId?: string): Promise<RunRecord[]> {
|
|
85
|
-
const all = [...runStore.values()];
|
|
86
|
-
return playbookId ? all.filter(r => r.playbookId === playbookId) : all;
|
|
87
|
-
}
|