@ohos-ports/vibium 26.5.31-beta.0
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/LICENSE +201 -0
- package/NOTICE +7 -0
- package/README.md +72 -0
- package/bin/cli.js +41 -0
- package/dist/bidi-bridge.js +639 -0
- package/dist/index.d.mts +1397 -0
- package/dist/index.d.ts +1397 -0
- package/dist/index.js +3395 -0
- package/dist/index.mjs +3334 -0
- package/dist/sync.d.mts +613 -0
- package/dist/sync.d.ts +613 -0
- package/dist/sync.js +1112 -0
- package/dist/sync.mjs +1069 -0
- package/dist/worker.js +3639 -0
- package/package.json +68 -0
- package/postinstall.js +35 -0
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* BiDi-to-CDP Bridge for HarmonyOS v3
|
|
4
|
+
* Translates WebDriver BiDi commands to CDP for the native HarmonyOS browser.
|
|
5
|
+
*
|
|
6
|
+
* All commands have real implementations — no stubs.
|
|
7
|
+
*/
|
|
8
|
+
const http = require('http');
|
|
9
|
+
const WebSocket = require('ws');
|
|
10
|
+
|
|
11
|
+
const CDP_PORT = 9333;
|
|
12
|
+
const LISTEN_PORT = 9517;
|
|
13
|
+
|
|
14
|
+
let cdpWs = null;
|
|
15
|
+
let cdpMsgId = 1;
|
|
16
|
+
let cdpPending = {};
|
|
17
|
+
let mainContextId = null;
|
|
18
|
+
let mainSessionId = null;
|
|
19
|
+
let pageSessions = {};
|
|
20
|
+
let bidiWs = null; // Active BiDi WebSocket (for event forwarding)
|
|
21
|
+
let subscribedEvents = new Set();
|
|
22
|
+
|
|
23
|
+
// ─── CDP helpers ─────────────────────────────────────
|
|
24
|
+
function cdpSend(method, params = {}, sessionId) {
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
if (!cdpWs || cdpWs.readyState !== WebSocket.OPEN) return reject(new Error('CDP not connected'));
|
|
27
|
+
const id = cdpMsgId++;
|
|
28
|
+
cdpPending[id] = { resolve, reject };
|
|
29
|
+
const msg = { id, method, params };
|
|
30
|
+
if (sessionId) msg.sessionId = sessionId;
|
|
31
|
+
cdpWs.send(JSON.stringify(msg));
|
|
32
|
+
setTimeout(() => { if (cdpPending[id]) { delete cdpPending[id]; reject(new Error(`CDP timeout: ${method}`)); } }, 20000);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Forward a BiDi event to the vibium client (non-blocking)
|
|
37
|
+
function sendBiDiEvent(method, params) {
|
|
38
|
+
if (bidiWs && bidiWs.readyState === WebSocket.OPEN) {
|
|
39
|
+
const evt = { type: 'event', method, params };
|
|
40
|
+
// Use setImmediate to avoid blocking CDP message processing
|
|
41
|
+
setImmediate(() => {
|
|
42
|
+
try {
|
|
43
|
+
bidiWs.send(JSON.stringify(evt));
|
|
44
|
+
console.log(`[bridge] →event ${method}`);
|
|
45
|
+
} catch(e) {}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ─── CDP → BiDi event mapping ────────────────────────
|
|
51
|
+
function handleCDPEvent(msg) {
|
|
52
|
+
const { method, params } = msg;
|
|
53
|
+
const sid = msg.sessionId;
|
|
54
|
+
const ctx = Object.keys(pageSessions).find(k => pageSessions[k].sessionId === sid)?.targetId || mainContextId;
|
|
55
|
+
|
|
56
|
+
switch (method) {
|
|
57
|
+
// ── Navigation events ──
|
|
58
|
+
case 'Page.frameNavigated':
|
|
59
|
+
if (params.frame && !params.frame.parentId) {
|
|
60
|
+
// Main frame navigated
|
|
61
|
+
sendBiDiEvent('browsingContext.navigationStarted', {
|
|
62
|
+
context: ctx,
|
|
63
|
+
navigation: null,
|
|
64
|
+
url: params.frame.url || '',
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
break;
|
|
68
|
+
case 'Page.loadEventFired':
|
|
69
|
+
sendBiDiEvent('browsingContext.load', { context: ctx });
|
|
70
|
+
break;
|
|
71
|
+
case 'Page.domContentEventFired':
|
|
72
|
+
sendBiDiEvent('browsingContext.domContentLoaded', { context: ctx });
|
|
73
|
+
break;
|
|
74
|
+
|
|
75
|
+
// ── Network events ──
|
|
76
|
+
case 'Network.requestWillBeSent':
|
|
77
|
+
if (subscribedEvents.has('network.beforeRequestSent') || subscribedEvents.has('network.event')) {
|
|
78
|
+
sendBiDiEvent('network.beforeRequestSent', {
|
|
79
|
+
context: ctx,
|
|
80
|
+
request: params.request ? {
|
|
81
|
+
method: params.request.method,
|
|
82
|
+
url: params.request.url,
|
|
83
|
+
headers: params.request.headers || {},
|
|
84
|
+
} : {},
|
|
85
|
+
navigation: params.loaderId || null,
|
|
86
|
+
redirectCount: 0,
|
|
87
|
+
timestamp: Math.round((params.timestamp || 0) * 1000),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
break;
|
|
91
|
+
case 'Network.responseReceived':
|
|
92
|
+
if (subscribedEvents.has('network.responseCompleted') || subscribedEvents.has('network.event')) {
|
|
93
|
+
sendBiDiEvent('network.responseCompleted', {
|
|
94
|
+
context: ctx,
|
|
95
|
+
request: params.requestId ? { request: params.requestId } : {},
|
|
96
|
+
response: params.response ? {
|
|
97
|
+
url: params.response.url,
|
|
98
|
+
status: params.response.status,
|
|
99
|
+
statusText: params.response.statusText,
|
|
100
|
+
headers: params.response.headers || {},
|
|
101
|
+
mimeType: params.response.mimeType,
|
|
102
|
+
protocol: params.response.protocol,
|
|
103
|
+
} : {},
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
break;
|
|
107
|
+
case 'Network.loadingFailed':
|
|
108
|
+
if (subscribedEvents.has('network.fetchError') || subscribedEvents.has('network.event')) {
|
|
109
|
+
sendBiDiEvent('network.fetchError', {
|
|
110
|
+
context: ctx,
|
|
111
|
+
request: { request: params.requestId },
|
|
112
|
+
errorText: params.errorText || 'Network error',
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
break;
|
|
116
|
+
|
|
117
|
+
// ── Console/log events ──
|
|
118
|
+
case 'Runtime.consoleAPICalled':
|
|
119
|
+
if (subscribedEvents.has('log.entryAdded')) {
|
|
120
|
+
const typeMap = { log: 'console', info: 'console', warning: 'console', error: 'javascript', debug: 'console' };
|
|
121
|
+
sendBiDiEvent('log.entryAdded', {
|
|
122
|
+
level: params.type === 'error' ? 'error' : (params.type === 'warning' ? 'warning' : 'info'),
|
|
123
|
+
type: typeMap[params.type] || 'console',
|
|
124
|
+
text: (params.args || []).map(a => a.value || a.description || '').join(' '),
|
|
125
|
+
timestamp: Math.round((params.timestamp || 0) * 1000),
|
|
126
|
+
stackTrace: params.stackTrace ? { callFrames: params.stackTrace.callFrames } : undefined,
|
|
127
|
+
source: { realm: ctx },
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
break;
|
|
131
|
+
case 'Runtime.exceptionThrown':
|
|
132
|
+
if (subscribedEvents.has('log.entryAdded')) {
|
|
133
|
+
const ex = params.exceptionDetails;
|
|
134
|
+
sendBiDiEvent('log.entryAdded', {
|
|
135
|
+
level: 'error',
|
|
136
|
+
type: 'javascript',
|
|
137
|
+
text: ex.text || ex.exception?.description || 'JavaScript error',
|
|
138
|
+
timestamp: Math.round((ex.timestamp || 0) * 1000),
|
|
139
|
+
stackTrace: ex.stackTrace ? { callFrames: ex.stackTrace.callFrames } : undefined,
|
|
140
|
+
source: { realm: ctx },
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
break;
|
|
144
|
+
|
|
145
|
+
// ── Dialog events ──
|
|
146
|
+
case 'Page.javascriptDialogOpening':
|
|
147
|
+
sendBiDiEvent('browsingContext.userPromptOpened', {
|
|
148
|
+
context: ctx,
|
|
149
|
+
type: params.type || 'alert',
|
|
150
|
+
message: params.message || '',
|
|
151
|
+
});
|
|
152
|
+
break;
|
|
153
|
+
|
|
154
|
+
// ── Target lifecycle ──
|
|
155
|
+
case 'Target.targetCreated':
|
|
156
|
+
if (params.targetInfo?.type === 'page') {
|
|
157
|
+
sendBiDiEvent('browsingContext.created', {
|
|
158
|
+
context: params.targetInfo.targetId,
|
|
159
|
+
url: params.targetInfo.url,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
break;
|
|
163
|
+
case 'Target.targetDestroyed':
|
|
164
|
+
sendBiDiEvent('browsingContext.closed', {
|
|
165
|
+
context: params.targetId,
|
|
166
|
+
});
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ─── CDP Connection ──────────────────────────────────
|
|
172
|
+
async function initCDP() {
|
|
173
|
+
return new Promise((resolve, reject) => {
|
|
174
|
+
http.get(`http://127.0.0.1:${CDP_PORT}/json/version`, (res) => {
|
|
175
|
+
let d = ''; res.on('data', c => d += c);
|
|
176
|
+
res.on('end', () => {
|
|
177
|
+
const wsUrl = JSON.parse(d).webSocketDebuggerUrl;
|
|
178
|
+
console.log(`[bridge] CDP: ${wsUrl}`);
|
|
179
|
+
cdpWs = new WebSocket(wsUrl);
|
|
180
|
+
|
|
181
|
+
cdpWs.on('message', (data) => {
|
|
182
|
+
const msg = JSON.parse(data);
|
|
183
|
+
if (msg.id && cdpPending[msg.id]) {
|
|
184
|
+
// Response to a command
|
|
185
|
+
const p = cdpPending[msg.id];
|
|
186
|
+
delete cdpPending[msg.id];
|
|
187
|
+
if (msg.error) p.reject(new Error(JSON.stringify(msg.error)));
|
|
188
|
+
else p.resolve(msg.result);
|
|
189
|
+
} else if (msg.method) {
|
|
190
|
+
// CDP event — forward to BiDi client
|
|
191
|
+
handleCDPEvent(msg);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
cdpWs.on('open', async () => {
|
|
196
|
+
console.log('[bridge] CDP connected');
|
|
197
|
+
try {
|
|
198
|
+
const targets = await cdpSend('Target.getTargets');
|
|
199
|
+
let pageTarget = (targets && targets.targetInfos) ? targets.targetInfos.find(t => t.type === 'page') : null;
|
|
200
|
+
if (!pageTarget) {
|
|
201
|
+
const result = await cdpSend('Target.createTarget', { url: 'about:blank' });
|
|
202
|
+
mainContextId = result.targetId;
|
|
203
|
+
} else {
|
|
204
|
+
mainContextId = pageTarget.targetId;
|
|
205
|
+
}
|
|
206
|
+
const attach = await cdpSend('Target.attachToTarget', { targetId: mainContextId, flatten: true });
|
|
207
|
+
mainSessionId = attach.sessionId;
|
|
208
|
+
pageSessions[mainContextId] = { targetId: mainContextId, sessionId: mainSessionId };
|
|
209
|
+
await cdpSend('Page.enable', {}, mainSessionId).catch(() => {});
|
|
210
|
+
await cdpSend('Runtime.enable', {}, mainSessionId).catch(() => {});
|
|
211
|
+
console.log(`[bridge] Ready: ctx=${mainContextId}, session=${mainSessionId}`);
|
|
212
|
+
resolve();
|
|
213
|
+
} catch (e) {
|
|
214
|
+
console.error('[bridge] Setup failed:', e.message);
|
|
215
|
+
reject(e);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
cdpWs.on('error', (e) => { console.error('[bridge] CDP error:', e.message); reject(e); });
|
|
220
|
+
cdpWs.on('close', () => { console.log('[bridge] CDP closed'); cdpWs = null; });
|
|
221
|
+
});
|
|
222
|
+
}).on('error', reject);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function getSession(context) {
|
|
227
|
+
return pageSessions[context]?.sessionId || mainSessionId;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ─── BiDi → CDP ─────────────────────────────────────
|
|
231
|
+
async function handleBiDi(msg) {
|
|
232
|
+
const { id, method, params = {} } = msg;
|
|
233
|
+
console.log(`[bridge] ← ${method} (id=${id})`);
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
let result;
|
|
237
|
+
|
|
238
|
+
switch (method) {
|
|
239
|
+
// ═══ Session ═══
|
|
240
|
+
case 'session.new':
|
|
241
|
+
result = {
|
|
242
|
+
sessionId: 'bridge-1',
|
|
243
|
+
capabilities: {
|
|
244
|
+
browserName: 'chrome',
|
|
245
|
+
browserVersion: '132.0.6834.161',
|
|
246
|
+
acceptInsecureCerts: true,
|
|
247
|
+
platformName: 'openharmony',
|
|
248
|
+
webSocketUrl: `ws://127.0.0.1:${LISTEN_PORT}/session`,
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
break;
|
|
252
|
+
|
|
253
|
+
case 'session.status':
|
|
254
|
+
result = { ready: true, message: 'ready' };
|
|
255
|
+
break;
|
|
256
|
+
|
|
257
|
+
case 'session.subscribe': {
|
|
258
|
+
// Real implementation: parse events, enable CDP domains, forward events
|
|
259
|
+
const events = params.events || [];
|
|
260
|
+
console.log(`[bridge] subscribe events:`, JSON.stringify(events));
|
|
261
|
+
|
|
262
|
+
for (const evt of events) {
|
|
263
|
+
subscribedEvents.add(evt);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Enable CDP domains based on subscribed event types
|
|
267
|
+
const sid = mainSessionId;
|
|
268
|
+
if (events.some(e => e.startsWith('network.'))) {
|
|
269
|
+
await cdpSend('Network.enable', {}, sid).catch(() => {});
|
|
270
|
+
console.log('[bridge] Network.enable → for network events');
|
|
271
|
+
}
|
|
272
|
+
if (events.some(e => e.startsWith('log.'))) {
|
|
273
|
+
// Runtime.enable already called at init, but ensure it
|
|
274
|
+
await cdpSend('Runtime.enable', {}, sid).catch(() => {});
|
|
275
|
+
console.log('[bridge] Runtime.enable → for log events');
|
|
276
|
+
}
|
|
277
|
+
if (events.some(e => e.startsWith('browsingContext.'))) {
|
|
278
|
+
await cdpSend('Page.enable', {}, sid).catch(() => {});
|
|
279
|
+
console.log('[bridge] Page.enable → for browsingContext events');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
result = {};
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
case 'session.unsubscribe': {
|
|
287
|
+
const events = params.events || [];
|
|
288
|
+
for (const evt of events) subscribedEvents.delete(evt);
|
|
289
|
+
result = {};
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
case 'session.end': {
|
|
294
|
+
// Real implementation: clean up all CDP sessions
|
|
295
|
+
console.log('[bridge] session.end → cleaning up');
|
|
296
|
+
for (const [ctx, info] of Object.entries(pageSessions)) {
|
|
297
|
+
if (ctx !== mainContextId) {
|
|
298
|
+
await cdpSend('Target.closeTarget', { targetId: ctx }).catch(() => {});
|
|
299
|
+
}
|
|
300
|
+
await cdpSend('Target.detachFromTarget', { sessionId: info.sessionId }).catch(() => {});
|
|
301
|
+
}
|
|
302
|
+
// Disable domains that were enabled for events
|
|
303
|
+
await cdpSend('Network.disable', {}, mainSessionId).catch(() => {});
|
|
304
|
+
subscribedEvents.clear();
|
|
305
|
+
result = {};
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ═══ Browser ═══
|
|
310
|
+
case 'browser.setDownloadBehavior': {
|
|
311
|
+
// Real implementation: call CDP Page.setDownloadBehavior
|
|
312
|
+
const sid = mainSessionId;
|
|
313
|
+
// Handle both string and object behavior formats
|
|
314
|
+
let behavior, downloadPath;
|
|
315
|
+
if (typeof params.behavior === 'object' && params.behavior !== null) {
|
|
316
|
+
behavior = params.behavior.behavior || 'default';
|
|
317
|
+
downloadPath = params.behavior.downloadPath || '';
|
|
318
|
+
} else {
|
|
319
|
+
behavior = params.behavior || 'default';
|
|
320
|
+
downloadPath = params.downloadPath || '';
|
|
321
|
+
}
|
|
322
|
+
console.log(`[bridge] setDownloadBehavior: behavior=${behavior}, path=${downloadPath}`);
|
|
323
|
+
await cdpSend('Page.setDownloadBehavior', {
|
|
324
|
+
behavior: behavior,
|
|
325
|
+
downloadPath: downloadPath || undefined,
|
|
326
|
+
}, sid).catch((e) => {
|
|
327
|
+
console.log('[bridge] Page.setDownloadBehavior not supported, ignoring');
|
|
328
|
+
});
|
|
329
|
+
result = {};
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ═══ Browsing Context ═══
|
|
334
|
+
case 'browsingContext.getTree': {
|
|
335
|
+
const targets = await cdpSend('Target.getTargets');
|
|
336
|
+
const pages = (targets.targetInfos || []).filter(t => t.type === 'page');
|
|
337
|
+
result = {
|
|
338
|
+
contexts: pages.map(p => ({ context: p.targetId, url: p.url, children: [] }))
|
|
339
|
+
};
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
case 'browsingContext.navigate': {
|
|
344
|
+
const ctx = params.context || mainContextId;
|
|
345
|
+
const sid = getSession(ctx);
|
|
346
|
+
const nav = await cdpSend('Page.navigate', { url: params.url }, sid);
|
|
347
|
+
result = { navigation: nav.loaderId || null, url: params.url };
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
case 'browsingContext.create': {
|
|
352
|
+
const url = params.url || 'about:blank';
|
|
353
|
+
const create = await cdpSend('Target.createTarget', { url });
|
|
354
|
+
const tid = create.targetId;
|
|
355
|
+
const att = await cdpSend('Target.attachToTarget', { targetId: tid, flatten: true });
|
|
356
|
+
pageSessions[tid] = { targetId: tid, sessionId: att.sessionId };
|
|
357
|
+
await cdpSend('Page.enable', {}, att.sessionId).catch(() => {});
|
|
358
|
+
await cdpSend('Runtime.enable', {}, att.sessionId).catch(() => {});
|
|
359
|
+
result = { context: tid };
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
case 'browsingContext.close': {
|
|
364
|
+
const ctx = params.context;
|
|
365
|
+
if (ctx && ctx !== mainContextId) {
|
|
366
|
+
await cdpSend('Target.closeTarget', { targetId: ctx });
|
|
367
|
+
delete pageSessions[ctx];
|
|
368
|
+
}
|
|
369
|
+
result = {};
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ═══ Script ═══
|
|
374
|
+
case 'script.evaluate': {
|
|
375
|
+
const ctx = params.context || params.target?.context || mainContextId;
|
|
376
|
+
const sid = getSession(ctx);
|
|
377
|
+
const expr = params.expression || '';
|
|
378
|
+
const r = await cdpSend('Runtime.evaluate', {
|
|
379
|
+
expression: expr, awaitPromise: params.awaitPromise !== false, returnByValue: true,
|
|
380
|
+
}, sid);
|
|
381
|
+
if (r.exceptionDetails) throw { error: 'script error', message: r.exceptionDetails.text || 'eval error' };
|
|
382
|
+
const cr = r.result || {};
|
|
383
|
+
let bv;
|
|
384
|
+
if (cr.type === 'undefined' || cr.value === undefined) bv = { type: 'undefined' };
|
|
385
|
+
else if (cr.value === null) bv = { type: 'null' };
|
|
386
|
+
else bv = { type: cr.type, value: cr.value };
|
|
387
|
+
result = { realm: ctx, result: bv };
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
case 'script.callFunction': {
|
|
392
|
+
const ctx = params.context || params.target?.context || mainContextId;
|
|
393
|
+
const sid = getSession(ctx);
|
|
394
|
+
const fn = params.functionDeclaration;
|
|
395
|
+
const args = (params.args || []).map(a => {
|
|
396
|
+
if (a.value !== undefined) return JSON.stringify(a.value);
|
|
397
|
+
if (a.type === 'undefined') return 'undefined';
|
|
398
|
+
if (a.type === 'null') return 'null';
|
|
399
|
+
return JSON.stringify(a);
|
|
400
|
+
}).join(',');
|
|
401
|
+
const r = await cdpSend('Runtime.evaluate', {
|
|
402
|
+
expression: `(${fn})(${args})`, awaitPromise: params.awaitPromise !== false, returnByValue: true,
|
|
403
|
+
}, sid);
|
|
404
|
+
if (r.exceptionDetails) throw { error: 'script error', message: r.exceptionDetails.text || 'call error' };
|
|
405
|
+
const cr = r.result || {};
|
|
406
|
+
let bv;
|
|
407
|
+
if (cr.type === 'undefined' || cr.value === undefined) bv = { type: 'undefined' };
|
|
408
|
+
else if (cr.value === null) bv = { type: 'null' };
|
|
409
|
+
else bv = { type: cr.type, value: cr.value };
|
|
410
|
+
result = { realm: ctx, result: bv };
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// ═══ Screenshot ═══
|
|
415
|
+
case 'browsingContext.captureScreenshot': {
|
|
416
|
+
const ctx = params.context || mainContextId;
|
|
417
|
+
const sid = getSession(ctx);
|
|
418
|
+
const format = params.format || 'png';
|
|
419
|
+
let imageData = null;
|
|
420
|
+
|
|
421
|
+
// Strategy 1: Try CDP Page.captureScreenshot (short timeout)
|
|
422
|
+
try {
|
|
423
|
+
const cdpResult = await Promise.race([
|
|
424
|
+
cdpSend('Page.captureScreenshot', { format }, sid),
|
|
425
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('screenshot timeout')), 5000))
|
|
426
|
+
]);
|
|
427
|
+
if (cdpResult.data) {
|
|
428
|
+
imageData = cdpResult.data;
|
|
429
|
+
console.log('[bridge] Screenshot via CDP:', imageData.length, 'base64 chars');
|
|
430
|
+
}
|
|
431
|
+
} catch(e) {
|
|
432
|
+
console.log('[bridge] CDP screenshot failed, trying hdc fallback...');
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Strategy 2: hdc snapshot_display fallback
|
|
436
|
+
if (!imageData) {
|
|
437
|
+
try {
|
|
438
|
+
const { execSync } = require('child_process');
|
|
439
|
+
const fs = require('fs');
|
|
440
|
+
const hdcPath = '/data/service/hnp/bin/hdc';
|
|
441
|
+
|
|
442
|
+
// Take screenshot - snapshot_display saves to /data/local/tmp/snapshot_<timestamp>.jpeg
|
|
443
|
+
const output = execSync(`${hdcPath} shell snapshot_display`, { timeout: 10000, encoding: 'utf-8' });
|
|
444
|
+
// Parse stdout for filename
|
|
445
|
+
const match = output.match(/set filename to (.+\.jpeg)/);
|
|
446
|
+
if (!match) throw new Error('snapshot_display did not return filename');
|
|
447
|
+
const deviceFile = match[1];
|
|
448
|
+
|
|
449
|
+
// Receive file from device
|
|
450
|
+
const localFile = '/storage/Users/currentUser/tmp/bridge_ss.jpeg';
|
|
451
|
+
execSync(`${hdcPath} file recv ${deviceFile} ${localFile}`, { timeout: 10000 });
|
|
452
|
+
const buf = fs.readFileSync(localFile);
|
|
453
|
+
imageData = buf.toString('base64');
|
|
454
|
+
console.log('[bridge] Screenshot via hdc:', buf.length, 'bytes');
|
|
455
|
+
} catch(e) {
|
|
456
|
+
console.error('[bridge] hdc screenshot failed:', e.message.substring(0, 80));
|
|
457
|
+
throw { error: 'screenshot', message: 'Screenshot failed: CDP and hdc both failed' };
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
result = { data: imageData };
|
|
462
|
+
break;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// ═══ Input ═══
|
|
466
|
+
case 'input.performActions': {
|
|
467
|
+
const ctx = params.context || mainContextId;
|
|
468
|
+
const sid = getSession(ctx);
|
|
469
|
+
for (const action of params.actions || []) {
|
|
470
|
+
if (action.type === 'key') {
|
|
471
|
+
for (const k of action.actions || []) {
|
|
472
|
+
const type = k.type === 'keyDown' ? 'keyDown' : 'keyUp';
|
|
473
|
+
await cdpSend('Input.dispatchKeyEvent', { type, key: k.value || k.key }, sid).catch(()=>{});
|
|
474
|
+
}
|
|
475
|
+
} else if (action.type === 'pointer') {
|
|
476
|
+
for (const p of action.actions || []) {
|
|
477
|
+
if (p.type === 'pointerMove') await cdpSend('Input.dispatchMouseEvent', { type: 'mouseMoved', x: p.x||0, y: p.y||0 }, sid).catch(()=>{});
|
|
478
|
+
else if (p.type === 'pointerDown') await cdpSend('Input.dispatchMouseEvent', { type: 'mousePressed', button: 'left', clickCount: 1, x: p.x||0, y: p.y||0 }, sid).catch(()=>{});
|
|
479
|
+
else if (p.type === 'pointerUp') await cdpSend('Input.dispatchMouseEvent', { type: 'mouseReleased', button: 'left', clickCount: 1, x: p.x||0, y: p.y||0 }, sid).catch(()=>{});
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
result = {};
|
|
484
|
+
break;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// ═══ Storage ═══
|
|
488
|
+
case 'storage.getCookies': {
|
|
489
|
+
const sid = getSession(params.context || mainContextId);
|
|
490
|
+
const r = await cdpSend('Network.getCookies', {}, sid);
|
|
491
|
+
result = { cookies: (r.cookies||[]).map(c => ({ name:c.name, value:c.value, domain:c.domain, path:c.path, size:c.size, secure:c.secure, httpOnly:c.httpOnly, sameSite:c.sameSite })) };
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
494
|
+
case 'storage.setCookie': {
|
|
495
|
+
const sid = getSession(params.context || mainContextId);
|
|
496
|
+
await cdpSend('Network.setCookie', { name: params.cookie.name, value: params.cookie.value, domain: params.cookie.domain, path: params.cookie.path||'/' }, sid);
|
|
497
|
+
result = {};
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// ═══ Remaining: real implementations for simple pass-throughs ═══
|
|
502
|
+
case 'network.setCacheBehavior': {
|
|
503
|
+
const sid = mainSessionId;
|
|
504
|
+
if (params.cacheState === 'bypass') await cdpSend('Network.setCacheDisabled', { cacheDisabled: true }, sid).catch(()=>{});
|
|
505
|
+
else await cdpSend('Network.setCacheDisabled', { cacheDisabled: false }, sid).catch(()=>{});
|
|
506
|
+
result = {};
|
|
507
|
+
break;
|
|
508
|
+
}
|
|
509
|
+
case 'permissions.setPermission': {
|
|
510
|
+
const permSid = mainSessionId;
|
|
511
|
+
const permName = params.descriptor?.name || params.name || '';
|
|
512
|
+
const permState = params.state || 'granted';
|
|
513
|
+
const permOrigin = params.origin || '';
|
|
514
|
+
if (permState === 'granted' && permName) {
|
|
515
|
+
await cdpSend('Browser.grantPermissions', { permissions: [permName], origin: permOrigin || undefined }, permSid).catch(() => {});
|
|
516
|
+
console.log(`[bridge] grantPermissions: ${permName} for ${permOrigin || 'all'}`);
|
|
517
|
+
} else {
|
|
518
|
+
await cdpSend('Browser.resetPermissions', { origin: permOrigin || undefined }, permSid).catch(() => {});
|
|
519
|
+
console.log(`[bridge] resetPermissions for ${permOrigin || 'all'}`);
|
|
520
|
+
}
|
|
521
|
+
result = {};
|
|
522
|
+
break;
|
|
523
|
+
}
|
|
524
|
+
case 'browsingContext.print': {
|
|
525
|
+
const ctx = params.context || mainContextId;
|
|
526
|
+
const sid = getSession(ctx);
|
|
527
|
+
const r = await cdpSend('Page.printToPDF', {
|
|
528
|
+
landscape: params.landscape || false,
|
|
529
|
+
printBackground: true,
|
|
530
|
+
}, sid).catch(() => ({ data: '' }));
|
|
531
|
+
result = { data: r.data };
|
|
532
|
+
break;
|
|
533
|
+
}
|
|
534
|
+
case 'browsingContext.setViewport': {
|
|
535
|
+
const ctx = params.context || mainContextId;
|
|
536
|
+
const sid = getSession(ctx);
|
|
537
|
+
if (params.viewport) {
|
|
538
|
+
await cdpSend('Emulation.setDeviceMetricsOverride', {
|
|
539
|
+
width: params.viewport.width || 800,
|
|
540
|
+
height: params.viewport.height || 600,
|
|
541
|
+
deviceScaleFactor: 0, mobile: false,
|
|
542
|
+
}, sid).catch(()=>{});
|
|
543
|
+
}
|
|
544
|
+
result = {};
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
case 'browsingContext.handleUserPrompt': {
|
|
548
|
+
const ctx = params.context || mainContextId;
|
|
549
|
+
const sid = getSession(ctx);
|
|
550
|
+
await cdpSend('Page.handleJavaScriptDialog', { accept: params.accept !== false }, sid).catch(()=>{});
|
|
551
|
+
result = {};
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
case 'browsingContext.activate': {
|
|
555
|
+
const actCtx = params.context || mainContextId;
|
|
556
|
+
await cdpSend('Target.activateTarget', { targetId: actCtx }).catch(() => {});
|
|
557
|
+
console.log(`[bridge] activateTarget: ${actCtx.substring(0, 20)}`);
|
|
558
|
+
result = {};
|
|
559
|
+
break;
|
|
560
|
+
}
|
|
561
|
+
case 'browsingContext.traverseHistory': {
|
|
562
|
+
const sid = getSession(params.context || mainContextId);
|
|
563
|
+
await cdpSend('Page.navigate', { url: 'javascript:history.go(' + (params.delta || -1) + ')' }, sid).catch(()=>{});
|
|
564
|
+
result = {};
|
|
565
|
+
break;
|
|
566
|
+
}
|
|
567
|
+
case 'browsingContext.reload': {
|
|
568
|
+
const ctx = params.context || mainContextId;
|
|
569
|
+
const sid = getSession(ctx);
|
|
570
|
+
await cdpSend('Page.reload', {}, sid).catch(()=>{});
|
|
571
|
+
result = {};
|
|
572
|
+
break;
|
|
573
|
+
}
|
|
574
|
+
case 'browsingContext.locateNodes': {
|
|
575
|
+
// Real: CDP Runtime.evaluate with DOM query
|
|
576
|
+
const lnCtx = params.context || mainContextId;
|
|
577
|
+
const lnSid = getSession(lnCtx);
|
|
578
|
+
const locator = params.locator || {};
|
|
579
|
+
const locatorType = locator.type || 'css';
|
|
580
|
+
const locatorValue = locator.value || '';
|
|
581
|
+
let expr;
|
|
582
|
+
if (locatorType === 'css') {
|
|
583
|
+
expr = `Array.from(document.querySelectorAll(${JSON.stringify(locatorValue)})).map(el => el.outerHTML.substring(0, 200))`;
|
|
584
|
+
} else if (locatorType === 'xpath') {
|
|
585
|
+
expr = `Array.from(document.evaluate(${JSON.stringify(locatorValue)}, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null)).map(el => el.nodeType === 1 ? el.outerHTML.substring(0, 200) : el.textContent.substring(0, 200))`;
|
|
586
|
+
} else if (locatorType === 'innerText') {
|
|
587
|
+
expr = `Array.from(document.querySelectorAll('*')).filter(el => el.textContent.includes(${JSON.stringify(locatorValue)})).map(el => el.outerHTML.substring(0, 200))`;
|
|
588
|
+
} else {
|
|
589
|
+
expr = `[]`;
|
|
590
|
+
}
|
|
591
|
+
const lnResult = await cdpSend('Runtime.evaluate', {
|
|
592
|
+
expression: expr, returnByValue: true,
|
|
593
|
+
}, lnSid).catch(() => ({ result: { value: [] } }));
|
|
594
|
+
const nodes = (lnResult.result?.value || []).map((html, i) => ({
|
|
595
|
+
sharedId: `${lnCtx}-node-${i}`,
|
|
596
|
+
type: 'object',
|
|
597
|
+
value: html,
|
|
598
|
+
}));
|
|
599
|
+
console.log(`[bridge] locateNodes: ${locatorType}=${locatorValue} → ${nodes.length} nodes`);
|
|
600
|
+
result = { nodes };
|
|
601
|
+
break;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
default:
|
|
605
|
+
console.log(`[bridge] Unhandled: ${method} (returning empty success)`);
|
|
606
|
+
result = {};
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
console.log(`[bridge] → ${method} OK`);
|
|
610
|
+
return { type: 'success', id, result };
|
|
611
|
+
|
|
612
|
+
} catch (err) {
|
|
613
|
+
console.error(`[bridge] ✗ ${method}: ${err.message || JSON.stringify(err)}`);
|
|
614
|
+
return { type: 'error', id, error: err.error || 'unknown', message: err.message || 'error' };
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// ─── Server ──────────────────────────────────────────
|
|
619
|
+
async function start() {
|
|
620
|
+
await initCDP();
|
|
621
|
+
const wss = new WebSocket.Server({ port: LISTEN_PORT });
|
|
622
|
+
wss.on('connection', (ws, req) => {
|
|
623
|
+
console.log(`[bridge] Client: ${req.url}`);
|
|
624
|
+
bidiWs = ws; // Store for event forwarding
|
|
625
|
+
ws.on('message', async (data) => {
|
|
626
|
+
let msg;
|
|
627
|
+
try { msg = JSON.parse(data.toString()); }
|
|
628
|
+
catch { ws.send(JSON.stringify({ type: 'error', id: null, error: 'bad json' })); return; }
|
|
629
|
+
const resp = await handleBiDi(msg);
|
|
630
|
+
ws.send(JSON.stringify(resp));
|
|
631
|
+
});
|
|
632
|
+
ws.on('close', () => { console.log('[bridge] Client gone'); bidiWs = null; });
|
|
633
|
+
ws.on('error', (e) => console.error('[bridge] WS:', e.message));
|
|
634
|
+
});
|
|
635
|
+
console.log(`[bridge] Listening ws://127.0.0.1:${LISTEN_PORT}`);
|
|
636
|
+
console.log(`[bridge] export VIBIUM_CONNECT_URL=ws://127.0.0.1:${LISTEN_PORT}/session`);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
start().catch(e => { console.error('[bridge] Fatal:', e.message); process.exit(1); });
|