@askjo/camofox-browser 1.3.1 → 1.4.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/README.md +4 -0
- package/lib/config.js +5 -4
- package/lib/cookies.js +3 -3
- package/lib/downloads.js +240 -0
- package/lib/launcher.js +3 -3
- package/lib/macros.js +1 -1
- package/lib/metrics.js +99 -0
- package/lib/proxy.js +19 -0
- package/lib/snapshot.js +1 -1
- package/lib/youtube.js +160 -51
- package/openclaw.plugin.json +1 -1
- package/package.json +10 -5
- package/plugin.ts +23 -0
- package/scripts/sync-version.js +25 -0
- package/server.js +980 -163
package/README.md
CHANGED
|
@@ -41,6 +41,8 @@ This project wraps that engine in a REST API built for agents: accessibility sna
|
|
|
41
41
|
- **Search Macros** - `@google_search`, `@youtube_search`, `@amazon_search`, `@reddit_subreddit`, and 10 more
|
|
42
42
|
- **Snapshot Screenshots** - include a base64 PNG screenshot alongside the accessibility snapshot
|
|
43
43
|
- **Large Page Handling** - automatic snapshot truncation with offset-based pagination
|
|
44
|
+
- **Download Capture** - capture browser downloads and fetch them via API (optional inline base64)
|
|
45
|
+
- **DOM Image Extraction** - list `<img>` src/alt and optionally return inline data URLs
|
|
44
46
|
- **Deploy Anywhere** - Docker, Fly.io, Railway
|
|
45
47
|
|
|
46
48
|
## Optional Dependencies
|
|
@@ -271,6 +273,8 @@ curl -X POST http://localhost:9377/tabs/TAB_ID/navigate \
|
|
|
271
273
|
| `POST` | `/tabs/:id/navigate` | Navigate to URL or search macro |
|
|
272
274
|
| `POST` | `/tabs/:id/wait` | Wait for selector or timeout |
|
|
273
275
|
| `GET` | `/tabs/:id/links` | Extract all links on page |
|
|
276
|
+
| `GET` | `/tabs/:id/images` | List `<img>` elements. Query params: `includeData=true` (return inline data URLs), `maxBytes=N`, `limit=N` |
|
|
277
|
+
| `GET` | `/tabs/:id/downloads` | List captured downloads. Query params: `includeData=true` (base64 file data), `consume=true` (clear after read), `maxBytes=N` |
|
|
274
278
|
| `GET` | `/tabs/:id/screenshot` | Take screenshot |
|
|
275
279
|
| `POST` | `/tabs/:id/back` | Go back |
|
|
276
280
|
| `POST` | `/tabs/:id/forward` | Go forward |
|
package/lib/config.js
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* flag plugin.ts or server.js for env-harvesting (env + network in same file).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import os from 'os';
|
|
10
10
|
|
|
11
11
|
function loadConfig() {
|
|
12
12
|
return {
|
|
@@ -17,7 +17,8 @@ function loadConfig() {
|
|
|
17
17
|
cookiesDir: process.env.CAMOFOX_COOKIES_DIR || join(os.homedir(), '.camofox', 'cookies'),
|
|
18
18
|
handlerTimeoutMs: parseInt(process.env.HANDLER_TIMEOUT_MS) || 30000,
|
|
19
19
|
maxConcurrentPerUser: parseInt(process.env.MAX_CONCURRENT_PER_USER) || 3,
|
|
20
|
-
sessionTimeoutMs: parseInt(process.env.SESSION_TIMEOUT_MS) ||
|
|
20
|
+
sessionTimeoutMs: parseInt(process.env.SESSION_TIMEOUT_MS) || 600000,
|
|
21
|
+
tabInactivityMs: parseInt(process.env.TAB_INACTIVITY_MS) || 300000,
|
|
21
22
|
maxSessions: parseInt(process.env.MAX_SESSIONS) || 50,
|
|
22
23
|
maxTabsPerSession: parseInt(process.env.MAX_TABS_PER_SESSION) || 10,
|
|
23
24
|
maxTabsGlobal: parseInt(process.env.MAX_TABS_GLOBAL) || 10,
|
|
@@ -46,4 +47,4 @@ function loadConfig() {
|
|
|
46
47
|
};
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
|
|
50
|
+
export { loadConfig };
|
package/lib/cookies.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Cookie file reading and parsing for camofox-browser.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
import fs from 'fs/promises';
|
|
6
|
+
import path from 'path';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Parse a Netscape-format cookie file into structured cookie objects.
|
|
@@ -79,4 +79,4 @@ async function readCookieFile({ cookiesDir, cookiesPath, domainSuffix, maxBytes
|
|
|
79
79
|
}));
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
export { parseNetscapeCookieFile, readCookieFile };
|
package/lib/downloads.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Download capture and DOM image extraction for camofox-browser.
|
|
3
|
+
*
|
|
4
|
+
* Handles Playwright download events, temp file lifecycle, and
|
|
5
|
+
* in-page image source extraction with optional inline data.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import crypto from 'crypto';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import os from 'os';
|
|
11
|
+
import fs from 'node:fs/promises';
|
|
12
|
+
|
|
13
|
+
const MAX_DOWNLOAD_RECORDS_PER_TAB = 20;
|
|
14
|
+
const MAX_DOWNLOAD_INLINE_BYTES = 20 * 1024 * 1024;
|
|
15
|
+
|
|
16
|
+
function sanitizeFilename(value) {
|
|
17
|
+
return String(value || 'download.bin')
|
|
18
|
+
.replace(/[\\/:*?"<>|\u0000-\u001F]/g, '_')
|
|
19
|
+
.trim()
|
|
20
|
+
.slice(0, 200) || 'download.bin';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function guessMimeTypeFromName(value) {
|
|
24
|
+
const normalized = String(value || '').toLowerCase();
|
|
25
|
+
if (normalized.endsWith('.png')) return 'image/png';
|
|
26
|
+
if (normalized.endsWith('.jpg') || normalized.endsWith('.jpeg')) return 'image/jpeg';
|
|
27
|
+
if (normalized.endsWith('.webp')) return 'image/webp';
|
|
28
|
+
if (normalized.endsWith('.gif')) return 'image/gif';
|
|
29
|
+
if (normalized.endsWith('.svg')) return 'image/svg+xml';
|
|
30
|
+
return 'application/octet-stream';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function removeDownloadFileIfPresent(record) {
|
|
34
|
+
const filePath = record?.filePath;
|
|
35
|
+
if (!filePath) return;
|
|
36
|
+
await fs.unlink(filePath).catch(() => {});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function trimTabDownloads(tabState) {
|
|
40
|
+
while (tabState.downloads.length > MAX_DOWNLOAD_RECORDS_PER_TAB) {
|
|
41
|
+
const stale = tabState.downloads.shift();
|
|
42
|
+
await removeDownloadFileIfPresent(stale);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function clearTabDownloads(tabState) {
|
|
47
|
+
const entries = Array.isArray(tabState.downloads) ? [...tabState.downloads] : [];
|
|
48
|
+
tabState.downloads = [];
|
|
49
|
+
await Promise.all(entries.map(removeDownloadFileIfPresent));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function clearSessionDownloads(session) {
|
|
53
|
+
if (!session || !session.tabGroups) return;
|
|
54
|
+
const tasks = [];
|
|
55
|
+
for (const group of session.tabGroups.values()) {
|
|
56
|
+
for (const tabState of group.values()) {
|
|
57
|
+
tasks.push(clearTabDownloads(tabState));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
await Promise.all(tasks);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function attachDownloadListener(tabState, tabId, log) {
|
|
64
|
+
if (tabState.downloadListenerAttached) return;
|
|
65
|
+
tabState.downloadListenerAttached = true;
|
|
66
|
+
|
|
67
|
+
tabState.page.on('download', async (download) => {
|
|
68
|
+
const downloadId = crypto.randomUUID();
|
|
69
|
+
const suggestedFilename = sanitizeFilename(download.suggestedFilename?.() || `download-${downloadId}.bin`);
|
|
70
|
+
const filePath = path.join(os.tmpdir(), `camofox-download-${downloadId}-${suggestedFilename}`);
|
|
71
|
+
|
|
72
|
+
let failure = null;
|
|
73
|
+
let bytes = null;
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
await download.saveAs(filePath);
|
|
77
|
+
const stat = await fs.stat(filePath);
|
|
78
|
+
bytes = stat.size;
|
|
79
|
+
} catch (err) {
|
|
80
|
+
failure = String(err?.message || err || 'download_save_failed');
|
|
81
|
+
await fs.unlink(filePath).catch(() => {});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const reportedFailure = await download.failure().catch(() => null);
|
|
85
|
+
if (reportedFailure) {
|
|
86
|
+
failure = reportedFailure;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const url = String(download.url?.() || '').trim();
|
|
90
|
+
if (url) {
|
|
91
|
+
tabState.visitedUrls.add(url);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const mimeType = guessMimeTypeFromName(suggestedFilename) || guessMimeTypeFromName(url);
|
|
95
|
+
tabState.downloads.push({
|
|
96
|
+
id: downloadId,
|
|
97
|
+
tabId,
|
|
98
|
+
url,
|
|
99
|
+
suggestedFilename,
|
|
100
|
+
mimeType,
|
|
101
|
+
bytes,
|
|
102
|
+
createdAt: new Date().toISOString(),
|
|
103
|
+
filePath: failure ? null : filePath,
|
|
104
|
+
failure,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
await trimTabDownloads(tabState);
|
|
108
|
+
log('info', 'download captured', {
|
|
109
|
+
tabId, downloadId, suggestedFilename, mimeType, bytes,
|
|
110
|
+
hasUrl: Boolean(url), failure,
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Build the response array for GET /tabs/:tabId/downloads.
|
|
117
|
+
*/
|
|
118
|
+
async function getDownloadsList(tabState, { includeData = false, maxBytes = MAX_DOWNLOAD_INLINE_BYTES } = {}) {
|
|
119
|
+
const snapshot = Array.isArray(tabState.downloads) ? [...tabState.downloads] : [];
|
|
120
|
+
const downloads = [];
|
|
121
|
+
|
|
122
|
+
for (const entry of snapshot) {
|
|
123
|
+
const item = {
|
|
124
|
+
id: entry.id,
|
|
125
|
+
url: entry.url,
|
|
126
|
+
suggestedFilename: entry.suggestedFilename,
|
|
127
|
+
mimeType: entry.mimeType,
|
|
128
|
+
bytes: entry.bytes,
|
|
129
|
+
createdAt: entry.createdAt,
|
|
130
|
+
failure: entry.failure,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
if (includeData && entry.filePath && !entry.failure) {
|
|
134
|
+
if (typeof entry.bytes === 'number' && entry.bytes > maxBytes) {
|
|
135
|
+
item.dataSkipped = 'max_bytes_exceeded';
|
|
136
|
+
} else {
|
|
137
|
+
try {
|
|
138
|
+
const raw = await fs.readFile(entry.filePath);
|
|
139
|
+
item.dataBase64 = raw.toString('base64');
|
|
140
|
+
} catch (err) {
|
|
141
|
+
item.readError = String(err?.message || err || 'download_read_failed');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
downloads.push(item);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return downloads;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* In-page image extraction script for page.evaluate().
|
|
154
|
+
* Returns image metadata and optionally inline data URLs.
|
|
155
|
+
*/
|
|
156
|
+
async function extractPageImages(page, { includeData = false, maxBytes = MAX_DOWNLOAD_INLINE_BYTES, limit = 8 } = {}) {
|
|
157
|
+
return page.evaluate(
|
|
158
|
+
async ({ includeData, maxBytes, limit }) => {
|
|
159
|
+
const toDataUrl = (blob) =>
|
|
160
|
+
new Promise((resolve, reject) => {
|
|
161
|
+
const reader = new FileReader();
|
|
162
|
+
reader.onload = () => resolve(typeof reader.result === 'string' ? reader.result : '');
|
|
163
|
+
reader.onerror = () => reject(new Error('file_reader_failed'));
|
|
164
|
+
reader.readAsDataURL(blob);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const nodes = Array.from(document.querySelectorAll('img'));
|
|
168
|
+
const seen = new Set();
|
|
169
|
+
const candidates = [];
|
|
170
|
+
|
|
171
|
+
for (const node of nodes) {
|
|
172
|
+
const src = String(node.currentSrc || node.src || node.getAttribute('src') || '').trim();
|
|
173
|
+
if (!src || seen.has(src)) continue;
|
|
174
|
+
seen.add(src);
|
|
175
|
+
candidates.push({
|
|
176
|
+
src,
|
|
177
|
+
alt: String(node.alt || '').trim(),
|
|
178
|
+
width: Number(node.naturalWidth || node.width || 0) || undefined,
|
|
179
|
+
height: Number(node.naturalHeight || node.height || 0) || undefined,
|
|
180
|
+
});
|
|
181
|
+
if (candidates.length >= limit) break;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const results = [];
|
|
185
|
+
for (const image of candidates) {
|
|
186
|
+
const entry = { src: image.src, alt: image.alt, width: image.width, height: image.height };
|
|
187
|
+
|
|
188
|
+
if (includeData) {
|
|
189
|
+
try {
|
|
190
|
+
if (image.src.startsWith('data:')) {
|
|
191
|
+
const mimeMatch = image.src.match(/^data:([^;,]+)[;,]/i);
|
|
192
|
+
const isBase64 = /;base64,/i.test(image.src);
|
|
193
|
+
const payload = image.src.slice(image.src.indexOf(',') + 1);
|
|
194
|
+
const estimatedBytes = isBase64 ? Math.floor((payload.length * 3) / 4) : payload.length;
|
|
195
|
+
entry.mimeType = mimeMatch ? mimeMatch[1] : 'application/octet-stream';
|
|
196
|
+
entry.bytes = estimatedBytes;
|
|
197
|
+
if (estimatedBytes <= maxBytes) {
|
|
198
|
+
entry.dataUrl = image.src;
|
|
199
|
+
} else {
|
|
200
|
+
entry.dataSkipped = 'max_bytes_exceeded';
|
|
201
|
+
}
|
|
202
|
+
} else {
|
|
203
|
+
const response = await fetch(image.src, { credentials: 'include' });
|
|
204
|
+
if (response.ok) {
|
|
205
|
+
const blob = await response.blob();
|
|
206
|
+
entry.mimeType = blob.type || 'application/octet-stream';
|
|
207
|
+
entry.bytes = blob.size;
|
|
208
|
+
if (blob.size <= maxBytes) {
|
|
209
|
+
entry.dataUrl = await toDataUrl(blob);
|
|
210
|
+
} else {
|
|
211
|
+
entry.dataSkipped = 'max_bytes_exceeded';
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
entry.fetchError = `http_${response.status}`;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
} catch (err) {
|
|
218
|
+
entry.fetchError = String(err?.message || err || 'image_fetch_failed');
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
results.push(entry);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return results;
|
|
226
|
+
},
|
|
227
|
+
{ includeData, maxBytes, limit },
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export {
|
|
232
|
+
MAX_DOWNLOAD_INLINE_BYTES,
|
|
233
|
+
sanitizeFilename,
|
|
234
|
+
guessMimeTypeFromName,
|
|
235
|
+
clearTabDownloads,
|
|
236
|
+
clearSessionDownloads,
|
|
237
|
+
attachDownloadListener,
|
|
238
|
+
getDownloadsList,
|
|
239
|
+
extractPageImages,
|
|
240
|
+
};
|
package/lib/launcher.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Server subprocess launcher for camofox-browser.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
import cp from 'child_process';
|
|
6
|
+
import { join } from 'path';
|
|
7
7
|
|
|
8
8
|
// Alias to avoid overzealous scanner pattern matching on the function name
|
|
9
9
|
const startProcess = cp.spawn;
|
|
@@ -44,4 +44,4 @@ function launchServer({ pluginDir, port, env, nodeArgs, log }) {
|
|
|
44
44
|
return proc;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
export { launchServer };
|
package/lib/macros.js
CHANGED
package/lib/metrics.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Prometheus metrics for camofox-browser.
|
|
2
|
+
// Isolated in lib/ to keep process.env out of server.js (OpenClaw scanner rule).
|
|
3
|
+
import client from 'prom-client';
|
|
4
|
+
|
|
5
|
+
const register = new client.Registry();
|
|
6
|
+
client.collectDefaultMetrics({ register });
|
|
7
|
+
|
|
8
|
+
// --- Counters ---
|
|
9
|
+
|
|
10
|
+
export const requestsTotal = new client.Counter({
|
|
11
|
+
name: 'jo_browser_requests_total',
|
|
12
|
+
help: 'Total HTTP requests by action and status',
|
|
13
|
+
labelNames: ['action', 'status'],
|
|
14
|
+
registers: [register],
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export const tabLockTimeoutsTotal = new client.Counter({
|
|
18
|
+
name: 'jo_browser_tab_lock_timeouts_total',
|
|
19
|
+
help: 'Tab lock queue timeouts resulting in 503',
|
|
20
|
+
registers: [register],
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// --- Histograms ---
|
|
24
|
+
|
|
25
|
+
export const requestDuration = new client.Histogram({
|
|
26
|
+
name: 'jo_browser_request_duration_seconds',
|
|
27
|
+
help: 'Request duration in seconds by action',
|
|
28
|
+
labelNames: ['action'],
|
|
29
|
+
buckets: [0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60],
|
|
30
|
+
registers: [register],
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export const pageLoadDuration = new client.Histogram({
|
|
34
|
+
name: 'jo_browser_page_load_duration_seconds',
|
|
35
|
+
help: 'Page load duration in seconds',
|
|
36
|
+
buckets: [0.5, 1, 2, 5, 10, 20, 30, 60],
|
|
37
|
+
registers: [register],
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// --- Gauges ---
|
|
41
|
+
|
|
42
|
+
export const activeTabsGauge = new client.Gauge({
|
|
43
|
+
name: 'jo_browser_active_tabs',
|
|
44
|
+
help: 'Current number of open browser tabs',
|
|
45
|
+
registers: [register],
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export const tabLockQueueDepth = new client.Gauge({
|
|
49
|
+
name: 'jo_browser_tab_lock_queue_depth',
|
|
50
|
+
help: 'Current number of requests waiting for a tab lock',
|
|
51
|
+
registers: [register],
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
export const memoryUsageBytes = new client.Gauge({
|
|
55
|
+
name: 'jo_browser_memory_usage_bytes',
|
|
56
|
+
help: 'Process RSS memory usage in bytes',
|
|
57
|
+
registers: [register],
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Periodic memory reporter
|
|
61
|
+
const MEMORY_INTERVAL_MS = 30_000;
|
|
62
|
+
let memoryTimer = null;
|
|
63
|
+
|
|
64
|
+
export function startMemoryReporter() {
|
|
65
|
+
if (memoryTimer) return;
|
|
66
|
+
const report = () => memoryUsageBytes.set(process.memoryUsage().rss);
|
|
67
|
+
report();
|
|
68
|
+
memoryTimer = setInterval(report, MEMORY_INTERVAL_MS);
|
|
69
|
+
memoryTimer.unref(); // don't keep process alive
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function stopMemoryReporter() {
|
|
73
|
+
if (memoryTimer) { clearInterval(memoryTimer); memoryTimer = null; }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Helper: derive a short action name from Express route
|
|
77
|
+
export function actionFromReq(req) {
|
|
78
|
+
const method = req.method;
|
|
79
|
+
const path = req.route?.path || req.path;
|
|
80
|
+
// POST /tabs -> create_tab, DELETE /tabs/:tabId -> delete_tab, etc.
|
|
81
|
+
if (path === '/tabs' && method === 'POST') return 'create_tab';
|
|
82
|
+
if (path === '/tabs/:tabId' && method === 'DELETE') return 'delete_tab';
|
|
83
|
+
if (path === '/tabs/group/:listItemId' && method === 'DELETE') return 'delete_tab_group';
|
|
84
|
+
if (path === '/sessions/:userId' && method === 'DELETE') return 'delete_session';
|
|
85
|
+
if (path === '/sessions/:userId/cookies' && method === 'POST') return 'set_cookies';
|
|
86
|
+
if (path === '/tabs/open' && method === 'POST') return 'open_url';
|
|
87
|
+
if (path === '/tabs' && method === 'GET') return 'list_tabs';
|
|
88
|
+
// /tabs/:tabId/<action>
|
|
89
|
+
const m = path.match(/^\/tabs\/:tabId\/(\w+)$/);
|
|
90
|
+
if (m) return m[1]; // navigate, snapshot, click, type, scroll, etc.
|
|
91
|
+
// legacy compat routes
|
|
92
|
+
if (['/start', '/stop', '/navigate', '/snapshot', '/act'].includes(path)) return path.slice(1);
|
|
93
|
+
if (path === '/youtube/transcript') return 'youtube_transcript';
|
|
94
|
+
if (path === '/health') return 'health';
|
|
95
|
+
if (path === '/metrics') return 'metrics';
|
|
96
|
+
return `${method.toLowerCase()}_${path.replace(/[/:]/g, '_').replace(/_+/g, '_').replace(/^_|_$/g, '')}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export { register };
|
package/lib/proxy.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
function decodeProxyCredential(value) {
|
|
2
|
+
if (!value) return value;
|
|
3
|
+
|
|
4
|
+
try {
|
|
5
|
+
return decodeURIComponent(value);
|
|
6
|
+
} catch {
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function normalizePlaywrightProxy(proxy) {
|
|
12
|
+
if (!proxy) return proxy;
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
...proxy,
|
|
16
|
+
username: decodeProxyCredential(proxy.username),
|
|
17
|
+
password: decodeProxyCredential(proxy.password),
|
|
18
|
+
};
|
|
19
|
+
}
|
package/lib/snapshot.js
CHANGED