@dmsdc-ai/aigentry-deliberation 0.0.30 → 0.0.31
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/browser-control-port.js +273 -113
- package/index.js +304 -125
- package/install.js +1 -0
- package/observer.js +76 -11
- package/package.json +1 -1
- package/selectors/chatgpt.json +3 -1
- package/selectors/copilot.json +3 -2
- package/selectors/deepseek.json +2 -1
- package/selectors/grok.json +4 -2
- package/selectors/mistral.json +7 -6
- package/selectors/qwen.json +4 -3
package/browser-control-port.js
CHANGED
|
@@ -12,6 +12,7 @@ import { readFileSync } from "node:fs";
|
|
|
12
12
|
import { join, dirname } from "node:path";
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
14
|
import { DegradationStateMachine, makeResult, ERROR_CODES } from "./degradation-state-machine.js";
|
|
15
|
+
import { writeClipboardText } from "./clipboard.js";
|
|
15
16
|
|
|
16
17
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
18
|
|
|
@@ -103,6 +104,33 @@ class DevToolsMcpAdapter extends BrowserControlPort {
|
|
|
103
104
|
this._cmdId = 0;
|
|
104
105
|
/** @type {Map<string, Set<string>>} dedupe: sessionId → Set<turnId> */
|
|
105
106
|
this.sentTurns = new Map();
|
|
107
|
+
/** @type {Map<string, any>} wsUrl -> WebSocket connection promise */
|
|
108
|
+
this._connections = new Map();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async _getConnection(wsUrl) {
|
|
112
|
+
if (this._connections.has(wsUrl)) {
|
|
113
|
+
const conn = await this._connections.get(wsUrl);
|
|
114
|
+
if (conn.readyState === 1 || conn.readyState === 0) return conn; // OPEN or CONNECTING
|
|
115
|
+
this._connections.delete(wsUrl);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const connPromise = this._connectWs(wsUrl);
|
|
119
|
+
this._connections.set(wsUrl, connPromise);
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const ws = await connPromise;
|
|
123
|
+
// Handle cleanup if closed externally
|
|
124
|
+
if (ws.on) {
|
|
125
|
+
ws.on("close", () => this._connections.delete(wsUrl));
|
|
126
|
+
} else if (ws.addEventListener) {
|
|
127
|
+
ws.addEventListener("close", () => this._connections.delete(wsUrl));
|
|
128
|
+
}
|
|
129
|
+
return ws;
|
|
130
|
+
} catch (err) {
|
|
131
|
+
this._connections.delete(wsUrl);
|
|
132
|
+
throw err;
|
|
133
|
+
}
|
|
106
134
|
}
|
|
107
135
|
|
|
108
136
|
async attach(sessionId, targetHint = {}) {
|
|
@@ -241,6 +269,19 @@ class DevToolsMcpAdapter extends BrowserControlPort {
|
|
|
241
269
|
binding._baselineDirectCount = baseline.data?.directCount || 0;
|
|
242
270
|
binding._lastSentText = text; // Store for response filtering
|
|
243
271
|
|
|
272
|
+
// Mark existing containers with data-dlb-baseline for robust new-response detection
|
|
273
|
+
// (immune to content-visibility CSS count fluctuations)
|
|
274
|
+
await this._cdpEvaluate(binding, `
|
|
275
|
+
(function() {
|
|
276
|
+
var contSels = ${respContSel}.split(',').map(function(s) { return s.trim(); });
|
|
277
|
+
try { document.querySelectorAll('[data-dlb-baseline]').forEach(function(el) { el.removeAttribute('data-dlb-baseline'); }); } catch(e) {}
|
|
278
|
+
for (var i = 0; i < contSels.length; i++) {
|
|
279
|
+
try { document.querySelectorAll(contSels[i]).forEach(function(el) { el.setAttribute('data-dlb-baseline', '1'); }); } catch(e) {}
|
|
280
|
+
}
|
|
281
|
+
return { ok: true };
|
|
282
|
+
})()
|
|
283
|
+
`);
|
|
284
|
+
|
|
244
285
|
// Step 1: Focus input and insert text
|
|
245
286
|
const inputSel = JSON.stringify(binding.selectors.inputSelector);
|
|
246
287
|
const sendBtnSel = JSON.stringify(binding.selectors.sendButton);
|
|
@@ -270,105 +311,194 @@ class DevToolsMcpAdapter extends BrowserControlPort {
|
|
|
270
311
|
}
|
|
271
312
|
|
|
272
313
|
const isCE = focusInput.data?.isContentEditable;
|
|
314
|
+
const isMac = process.platform === "darwin";
|
|
273
315
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
316
|
+
// ── Step 1.1: Insert text ──
|
|
317
|
+
// For IME-heavy languages (Korean, Japanese, etc.) or complex editors (tiptap, ProseMirror),
|
|
318
|
+
// Input.insertText is the canonical CDP way to inject text while bypassing IME.
|
|
319
|
+
let insertionSuccess = false;
|
|
320
|
+
|
|
321
|
+
try {
|
|
322
|
+
// Strategy 1: Clear and insert via CDP (Reliable for most web apps)
|
|
277
323
|
await this._cdpEvaluate(binding, `
|
|
278
324
|
(function() {
|
|
279
325
|
var sels = ${inputSel}.split(',').map(function(s) { return s.trim(); });
|
|
280
|
-
var
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
326
|
+
for (var i = 0; i < sels.length; i++) {
|
|
327
|
+
var input = document.querySelector(sels[i]);
|
|
328
|
+
if (input) {
|
|
329
|
+
input.click();
|
|
330
|
+
input.focus();
|
|
331
|
+
if (input.isContentEditable) {
|
|
332
|
+
var sel = window.getSelection();
|
|
333
|
+
var range = document.createRange();
|
|
334
|
+
range.selectNodeContents(input);
|
|
335
|
+
sel.removeAllRanges();
|
|
336
|
+
sel.addRange(range);
|
|
337
|
+
document.execCommand('delete', false);
|
|
338
|
+
} else {
|
|
339
|
+
input.value = '';
|
|
340
|
+
}
|
|
341
|
+
return { ok: true };
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return { ok: false };
|
|
292
345
|
})()
|
|
293
346
|
`);
|
|
294
|
-
|
|
347
|
+
|
|
348
|
+
await this._cdpCommand(binding, "Input.insertText", { text });
|
|
349
|
+
await new Promise(r => setTimeout(r, 100));
|
|
350
|
+
|
|
351
|
+
// Sync framework state
|
|
352
|
+
await this._cdpEvaluate(binding, `
|
|
353
|
+
(function() {
|
|
354
|
+
var sels = ${inputSel}.split(',').map(function(s) { return s.trim(); });
|
|
355
|
+
for (var i = 0; i < sels.length; i++) {
|
|
356
|
+
var input = document.querySelector(sels[i]);
|
|
357
|
+
if (input) {
|
|
358
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
359
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
})()
|
|
363
|
+
`);
|
|
364
|
+
insertionSuccess = true;
|
|
365
|
+
} catch (e) { /* ignore and try fallback */ }
|
|
295
366
|
|
|
296
|
-
|
|
367
|
+
// Strategy 2: Fallback to System Clipboard Paste (Cmd+V)
|
|
368
|
+
if (!insertionSuccess) {
|
|
297
369
|
try {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
370
|
+
writeClipboardText(text);
|
|
371
|
+
const mod = isMac ? 4 : 2; // Meta (4) on Mac, Control (2) on Win/Linux
|
|
372
|
+
const keyCode = isMac ? 9 : 86; // 'V' key code
|
|
373
|
+
|
|
374
|
+
// On Mac, we can send a list of commands. "paste" is layout-independent.
|
|
375
|
+
const cdpParams = {
|
|
376
|
+
type: "keyDown",
|
|
377
|
+
modifiers: mod,
|
|
378
|
+
key: isMac ? "ㅍ" : "v", // Map to current layout if possible
|
|
379
|
+
code: "KeyV",
|
|
380
|
+
windowsVirtualKeyCode: 86,
|
|
381
|
+
nativeVirtualKeyCode: keyCode
|
|
382
|
+
};
|
|
383
|
+
if (isMac) cdpParams.commands = ["paste"];
|
|
384
|
+
|
|
385
|
+
await this._cdpCommand(binding, "Input.dispatchKeyEvent", cdpParams);
|
|
386
|
+
|
|
387
|
+
// Also send KeyUp
|
|
388
|
+
await this._cdpCommand(binding, "Input.dispatchKeyEvent", {
|
|
389
|
+
type: "keyUp",
|
|
390
|
+
modifiers: mod,
|
|
391
|
+
key: isMac ? "ㅍ" : "v",
|
|
392
|
+
code: "KeyV",
|
|
393
|
+
windowsVirtualKeyCode: 86,
|
|
394
|
+
nativeVirtualKeyCode: keyCode
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
insertionSuccess = true;
|
|
398
|
+
await new Promise(r => setTimeout(r, 200));
|
|
399
|
+
} catch (clipErr) { /* ignore */ }
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (!insertionSuccess) {
|
|
403
|
+
if (isCE) {
|
|
404
|
+
// For contenteditable: use CDP Input.insertText (works with tiptap/ProseMirror/Quill)
|
|
405
|
+
// First clear existing content
|
|
301
406
|
await this._cdpEvaluate(binding, `
|
|
302
407
|
(function() {
|
|
303
408
|
var sels = ${inputSel}.split(',').map(function(s) { return s.trim(); });
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
409
|
+
var input = null;
|
|
410
|
+
for (var i = 0; i < sels.length; i++) { input = document.querySelector(sels[i]); if (input) break; }
|
|
411
|
+
if (!input) return { ok: true };
|
|
412
|
+
input.click();
|
|
413
|
+
input.focus();
|
|
414
|
+
var sel = window.getSelection();
|
|
415
|
+
var range = document.createRange();
|
|
416
|
+
range.selectNodeContents(input);
|
|
417
|
+
sel.removeAllRanges();
|
|
418
|
+
sel.addRange(range);
|
|
419
|
+
document.execCommand('delete', false);
|
|
308
420
|
return { ok: true };
|
|
309
421
|
})()
|
|
310
422
|
`);
|
|
311
|
-
|
|
312
|
-
} else {
|
|
313
|
-
// For regular <textarea>/<input>: clear existing content, then use CDP Input.insertText
|
|
314
|
-
await this._cdpEvaluate(binding, `
|
|
315
|
-
(function() {
|
|
316
|
-
var input = document.querySelector(${inputSel});
|
|
317
|
-
if (!input) return { ok: true };
|
|
318
|
-
input.focus();
|
|
319
|
-
input.select ? input.select() : null;
|
|
320
|
-
input.value = '';
|
|
321
|
-
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
322
|
-
return { ok: true };
|
|
323
|
-
})()
|
|
324
|
-
`);
|
|
325
|
-
await new Promise(r => setTimeout(r, 50));
|
|
423
|
+
await new Promise(r => setTimeout(r, 50));
|
|
326
424
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
425
|
+
// Use CDP Input.insertText — triggers all framework handlers (tiptap, ProseMirror, Quill)
|
|
426
|
+
try {
|
|
427
|
+
await this._cdpCommand(binding, "Input.insertText", { text });
|
|
428
|
+
} catch {
|
|
429
|
+
// Fallback: execCommand (works for tiptap when properly focused via click)
|
|
430
|
+
await this._cdpEvaluate(binding, `
|
|
431
|
+
(function() {
|
|
432
|
+
var sels = ${inputSel}.split(',').map(function(s) { return s.trim(); });
|
|
433
|
+
for (var i = 0; i < sels.length; i++) {
|
|
434
|
+
var input = document.querySelector(sels[i]);
|
|
435
|
+
if (input) { input.click(); input.focus(); document.execCommand('insertText', false, ${escapedText}); break; }
|
|
436
|
+
}
|
|
437
|
+
return { ok: true };
|
|
438
|
+
})()
|
|
439
|
+
`);
|
|
440
|
+
}
|
|
441
|
+
} else {
|
|
442
|
+
// For regular <textarea>/<input>: clear existing content, then use CDP Input.insertText
|
|
332
443
|
await this._cdpEvaluate(binding, `
|
|
333
444
|
(function() {
|
|
334
445
|
var input = document.querySelector(${inputSel});
|
|
335
446
|
if (!input) return { ok: true };
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
if (nativeSetter && nativeSetter.set) {
|
|
340
|
-
nativeSetter.set.call(input, ${escapedText});
|
|
341
|
-
} else {
|
|
342
|
-
input.value = ${escapedText};
|
|
343
|
-
}
|
|
447
|
+
input.focus();
|
|
448
|
+
input.select ? input.select() : null;
|
|
449
|
+
input.value = '';
|
|
344
450
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
345
|
-
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
346
451
|
return { ok: true };
|
|
347
452
|
})()
|
|
348
453
|
`);
|
|
349
|
-
|
|
454
|
+
await new Promise(r => setTimeout(r, 50));
|
|
350
455
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
var
|
|
361
|
-
|
|
362
|
-
|
|
456
|
+
// CDP Input.insertText triggers OS-level text input
|
|
457
|
+
try {
|
|
458
|
+
await this._cdpCommand(binding, "Input.insertText", { text });
|
|
459
|
+
} catch {
|
|
460
|
+
// Fallback: nativeSetter approach
|
|
461
|
+
await this._cdpEvaluate(binding, `
|
|
462
|
+
(function() {
|
|
463
|
+
var input = document.querySelector(${inputSel});
|
|
464
|
+
if (!input) return { ok: true };
|
|
465
|
+
var nativeSetter = Object.getOwnPropertyDescriptor(
|
|
466
|
+
Object.getPrototypeOf(input), 'value'
|
|
467
|
+
);
|
|
468
|
+
if (nativeSetter && nativeSetter.set) {
|
|
469
|
+
nativeSetter.set.call(input, ${escapedText});
|
|
470
|
+
} else {
|
|
471
|
+
input.value = ${escapedText};
|
|
363
472
|
}
|
|
364
473
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
365
474
|
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
366
475
|
return { ok: true };
|
|
476
|
+
})()
|
|
477
|
+
`);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Sync React/Vue state: CDP Input.insertText updates DOM but may not trigger React onChange.
|
|
481
|
+
// Re-set value via nativeSetter + dispatch input event to force framework state sync.
|
|
482
|
+
await this._cdpEvaluate(binding, `
|
|
483
|
+
(function() {
|
|
484
|
+
var sels = ${inputSel}.split(',').map(function(s) { return s.trim(); });
|
|
485
|
+
for (var i = 0; i < sels.length; i++) {
|
|
486
|
+
var input = document.querySelector(sels[i]);
|
|
487
|
+
if (input && input.value) {
|
|
488
|
+
var proto = Object.getPrototypeOf(input);
|
|
489
|
+
var desc = Object.getOwnPropertyDescriptor(proto, 'value');
|
|
490
|
+
if (desc && desc.set) {
|
|
491
|
+
desc.set.call(input, input.value);
|
|
492
|
+
}
|
|
493
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
494
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
495
|
+
return { ok: true };
|
|
496
|
+
}
|
|
367
497
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
498
|
+
return { ok: true };
|
|
499
|
+
})()
|
|
500
|
+
`);
|
|
501
|
+
}
|
|
372
502
|
}
|
|
373
503
|
|
|
374
504
|
// Small delay for framework state propagation
|
|
@@ -527,6 +657,8 @@ class DevToolsMcpAdapter extends BrowserControlPort {
|
|
|
527
657
|
|
|
528
658
|
const timeoutMs = timeoutSec * 1000;
|
|
529
659
|
const pollInterval = binding.timing?.pollIntervalMs || 500;
|
|
660
|
+
const stabilizationThreshold = binding.timing?.stabilizationThreshold || 6;
|
|
661
|
+
const stabilizationMinWaitMs = binding.timing?.stabilizationMinWaitMs || 0;
|
|
530
662
|
const startTime = Date.now();
|
|
531
663
|
|
|
532
664
|
const streamSel = JSON.stringify(binding.selectors.streamingIndicator);
|
|
@@ -550,15 +682,31 @@ class DevToolsMcpAdapter extends BrowserControlPort {
|
|
|
550
682
|
function isResponse(txt) {
|
|
551
683
|
if (!txt || !txt.trim()) return false;
|
|
552
684
|
var t = txt.trim();
|
|
685
|
+
var tLower = t.toLowerCase();
|
|
553
686
|
// Reject if text is exact match of sent message (user echo)
|
|
554
687
|
if (sentMsg && t === sentMsg) return false;
|
|
555
688
|
// Reject if text is >80% similar to sent message (likely echo with minor formatting diff)
|
|
556
689
|
if (sentMsg && t.length > 20 && (t.includes(sentMsg) || sentMsg === t.substring(0, sentMsg.length))) return false;
|
|
557
|
-
//
|
|
690
|
+
// Strip known suffixes before checking (Qwen appends skip button text)
|
|
691
|
+
var suffixes = ['건너뛰기', 'skip'];
|
|
692
|
+
for (var s = 0; s < suffixes.length; s++) {
|
|
693
|
+
if (tLower.length > suffixes[s].length && tLower.lastIndexOf(suffixes[s]) === tLower.length - suffixes[s].length) {
|
|
694
|
+
t = t.substring(0, t.length - suffixes[s].length).trim();
|
|
695
|
+
tLower = t.toLowerCase();
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
// Reject known loading/placeholder texts (case-insensitive)
|
|
558
699
|
var placeholders = ['...', '…', '생각 중...', '생각 중', '생각이 끝났습니다',
|
|
559
|
-
'조금만 더 기다려 주세요', '조금만 더 기다려 주세요.',
|
|
560
|
-
'
|
|
561
|
-
|
|
700
|
+
'조금만 더 기다려 주세요', '조금만 더 기다려 주세요.',
|
|
701
|
+
'응답을 준비 중입니다. 잠시만 기다려 주세요.', '응답을 준비 중입니다.', '잠시만 기다려 주세요.',
|
|
702
|
+
'copilot 메시지', 'copilot message',
|
|
703
|
+
'thinking', 'thinking...', 'loading', 'loading...', 'generating', 'generating...'];
|
|
704
|
+
if (placeholders.indexOf(tLower) >= 0) return false;
|
|
705
|
+
// Reject by prefix (loading/preparing messages)
|
|
706
|
+
var prefixes = ['응답을 준비', 'generating response'];
|
|
707
|
+
for (var p = 0; p < prefixes.length; p++) {
|
|
708
|
+
if (tLower.indexOf(prefixes[p].toLowerCase()) === 0) return false;
|
|
709
|
+
}
|
|
562
710
|
// Reject very short text (likely placeholder dots or single chars)
|
|
563
711
|
if (t.length < 2) return false;
|
|
564
712
|
return true;
|
|
@@ -571,6 +719,7 @@ class DevToolsMcpAdapter extends BrowserControlPort {
|
|
|
571
719
|
return txt.replace(/\s*(Copied|Copy|복사|복사됨|공유|Share)\s*$/gi, '')
|
|
572
720
|
.replace(/\s*(오전|오후)\s+\d{1,2}:\d{2}\s*$/g, '') // Korean timestamps
|
|
573
721
|
.replace(/\s*\d+\.\d+초\s*$/g, '') // Duration indicators
|
|
722
|
+
.replace(/\s*(건너뛰기|skip)\s*$/gi, '') // Qwen skip button text
|
|
574
723
|
.trim();
|
|
575
724
|
}
|
|
576
725
|
|
|
@@ -594,15 +743,19 @@ class DevToolsMcpAdapter extends BrowserControlPort {
|
|
|
594
743
|
} catch(e) {}
|
|
595
744
|
}
|
|
596
745
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
746
|
+
// Find NEW containers (without baseline marker — immune to content-visibility count fluctuations)
|
|
747
|
+
var newContainers = [];
|
|
748
|
+
for (var nc = 0; nc < allContainers.length; nc++) {
|
|
749
|
+
if (!allContainers[nc].hasAttribute('data-dlb-baseline')) newContainers.push(allContainers[nc]);
|
|
750
|
+
}
|
|
751
|
+
if (newContainers.length > 0) {
|
|
752
|
+
var last = newContainers[newContainers.length - 1];
|
|
753
|
+
// Scroll into view to force content-visibility rendering
|
|
600
754
|
try { last.scrollIntoView({ block: 'end' }); } catch(e) {}
|
|
601
755
|
var respSels = ${respSel}.split(',').map(function(s) { return s.trim(); });
|
|
602
756
|
for (var i = 0; i < respSels.length; i++) {
|
|
603
757
|
try {
|
|
604
758
|
var content = last.querySelector(respSels[i]);
|
|
605
|
-
// Try textContent first, then innerText as fallback for content-visibility
|
|
606
759
|
var txt = content ? (content.textContent || content.innerText || '') : '';
|
|
607
760
|
if (isResponse(txt)) {
|
|
608
761
|
candidateText = txt;
|
|
@@ -630,14 +783,17 @@ class DevToolsMcpAdapter extends BrowserControlPort {
|
|
|
630
783
|
for (var j = 0; j < found.length; j++) allDirect.push(found[j]);
|
|
631
784
|
} catch(e) {}
|
|
632
785
|
}
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
786
|
+
// Filter to elements NOT inside a baseline-marked container
|
|
787
|
+
var newDirect = [];
|
|
788
|
+
for (var d = 0; d < allDirect.length; d++) {
|
|
789
|
+
if (!allDirect[d].closest('[data-dlb-baseline]')) newDirect.push(allDirect[d]);
|
|
790
|
+
}
|
|
791
|
+
for (var k = newDirect.length - 1; k >= 0; k--) {
|
|
792
|
+
var txt = newDirect[k].textContent?.trim();
|
|
793
|
+
if (isResponse(txt)) {
|
|
794
|
+
candidateText = txt;
|
|
795
|
+
candidateStrategy = 'direct';
|
|
796
|
+
break;
|
|
641
797
|
}
|
|
642
798
|
}
|
|
643
799
|
}
|
|
@@ -651,29 +807,27 @@ class DevToolsMcpAdapter extends BrowserControlPort {
|
|
|
651
807
|
})()
|
|
652
808
|
`);
|
|
653
809
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
elapsedMs: Date.now() - startTime,
|
|
659
|
-
strategy: status.data.strategy,
|
|
660
|
-
});
|
|
661
|
-
}
|
|
810
|
+
// Unified text tracking: always verify text stability before returning
|
|
811
|
+
// (prevents premature capture when streaming indicators are unreliable)
|
|
812
|
+
const currentText = (status.data?.text || status.data?.candidateText || '').trim();
|
|
813
|
+
const isCurrentlyStreaming = !!(status.data?.streaming);
|
|
662
814
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
if (status.data.candidateText === lastSeenText) {
|
|
815
|
+
if (currentText) {
|
|
816
|
+
if (currentText === lastSeenText) {
|
|
666
817
|
stableCount++;
|
|
667
|
-
|
|
818
|
+
// Non-streaming: require 2 stable polls (1s) for fast return
|
|
819
|
+
// Streaming: require full stabilizationThreshold for thorough check
|
|
820
|
+
const effectiveThreshold = isCurrentlyStreaming ? stabilizationThreshold : Math.min(2, stabilizationThreshold);
|
|
821
|
+
if (stableCount >= effectiveThreshold && elapsed >= stabilizationMinWaitMs) {
|
|
668
822
|
return makeResult(true, {
|
|
669
823
|
turnId,
|
|
670
|
-
response:
|
|
824
|
+
response: currentText,
|
|
671
825
|
elapsedMs: Date.now() - startTime,
|
|
672
|
-
strategy: 'stabilized',
|
|
826
|
+
strategy: isCurrentlyStreaming ? 'stabilized' : (status.data?.strategy || 'non-streaming'),
|
|
673
827
|
});
|
|
674
828
|
}
|
|
675
829
|
} else {
|
|
676
|
-
lastSeenText =
|
|
830
|
+
lastSeenText = currentText;
|
|
677
831
|
stableCount = 0;
|
|
678
832
|
}
|
|
679
833
|
}
|
|
@@ -883,37 +1037,43 @@ class DevToolsMcpAdapter extends BrowserControlPort {
|
|
|
883
1037
|
throw Object.assign(new Error("No WebSocket URL for CDP"), { code: "MCP_CHANNEL_CLOSED" });
|
|
884
1038
|
}
|
|
885
1039
|
|
|
886
|
-
|
|
887
|
-
const ws = await this._connectWs(binding.wsUrl);
|
|
1040
|
+
const ws = await this._getConnection(binding.wsUrl);
|
|
888
1041
|
const id = ++this._cmdId;
|
|
889
1042
|
|
|
890
1043
|
return new Promise((resolve, reject) => {
|
|
1044
|
+
let cleanup;
|
|
1045
|
+
|
|
891
1046
|
const timeout = setTimeout(() => {
|
|
892
|
-
|
|
893
|
-
reject(Object.assign(new Error(
|
|
894
|
-
},
|
|
1047
|
+
if (cleanup) cleanup();
|
|
1048
|
+
reject(Object.assign(new Error(`CDP command timeout: ${method}`), { code: "TIMEOUT" }));
|
|
1049
|
+
}, 15000);
|
|
895
1050
|
|
|
896
|
-
|
|
1051
|
+
const handleMessage = (event) => {
|
|
897
1052
|
try {
|
|
898
1053
|
const data = JSON.parse(typeof event === "string" ? event : event.data);
|
|
899
1054
|
if (data.id === id) {
|
|
900
1055
|
clearTimeout(timeout);
|
|
901
|
-
|
|
1056
|
+
if (cleanup) cleanup();
|
|
1057
|
+
|
|
902
1058
|
if (data.error) {
|
|
903
1059
|
reject(Object.assign(new Error(data.error.message), { code: "SEND_FAILED" }));
|
|
904
1060
|
} else {
|
|
905
1061
|
resolve(data.result);
|
|
906
1062
|
}
|
|
907
1063
|
}
|
|
908
|
-
} catch { /*
|
|
1064
|
+
} catch { /* parse error */ }
|
|
909
1065
|
};
|
|
910
1066
|
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
ws.
|
|
914
|
-
|
|
1067
|
+
cleanup = () => {
|
|
1068
|
+
if (ws.removeEventListener) ws.removeEventListener("message", handleMessage);
|
|
1069
|
+
else if (ws.off) ws.off("message", handleMessage);
|
|
1070
|
+
else if (ws.onmessage === handleMessage) ws.onmessage = null;
|
|
915
1071
|
};
|
|
916
1072
|
|
|
1073
|
+
if (ws.addEventListener) ws.addEventListener("message", handleMessage);
|
|
1074
|
+
else if (ws.on) ws.on("message", handleMessage);
|
|
1075
|
+
else ws.onmessage = handleMessage;
|
|
1076
|
+
|
|
917
1077
|
ws.send(JSON.stringify({ id, method, params }));
|
|
918
1078
|
});
|
|
919
1079
|
}
|