@reconcrap/boss-recommend-mcp 2.1.24 → 2.1.25
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/package.json +1 -1
- package/src/chat-runtime-config.js +7 -5
- package/src/core/browser/index.js +16 -9
- package/src/core/run/index.js +5 -4
- package/src/core/screening/index.js +291 -26
- package/src/domains/chat/action-journal.js +97 -4
- package/src/domains/recommend/actions.js +740 -36
- package/src/domains/recommend/colleague-contact.js +836 -90
- package/src/domains/recommend/detail.js +4029 -128
- package/src/domains/recommend/filters.js +46 -28
- package/src/domains/recommend/jobs.js +32 -3
- package/src/domains/recommend/location.js +93 -46
- package/src/domains/recommend/refresh.js +354 -69
- package/src/domains/recommend/run-service.js +2499 -484
- package/src/recommend-mcp.js +514 -92
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
clickPoint,
|
|
3
|
+
describeNode,
|
|
3
4
|
getFrameDocumentNodeId,
|
|
4
5
|
getAttributesMap,
|
|
5
6
|
getNodeBox,
|
|
@@ -166,7 +167,7 @@ export function classifyGreetControl({
|
|
|
166
167
|
const className = normalizeText(attributes.class || "");
|
|
167
168
|
const title = normalizeText(attributes.title || attributes["aria-label"] || "");
|
|
168
169
|
const combined = lowerText(className, title);
|
|
169
|
-
const continueChat = labelText
|
|
170
|
+
const continueChat = labelText === "继续沟通";
|
|
170
171
|
const greetQuota = parseGreetQuota(labelText || title);
|
|
171
172
|
const greetEntry = (
|
|
172
173
|
GREET_EXACT_LABEL_PATTERN.test(labelText)
|
|
@@ -196,9 +197,11 @@ async function readActionNode(client, {
|
|
|
196
197
|
nodeId,
|
|
197
198
|
kind
|
|
198
199
|
}) {
|
|
199
|
-
const [attributes, outerHTML] = await Promise.all([
|
|
200
|
+
const [attributes, outerHTML, described, describedRoot] = await Promise.all([
|
|
200
201
|
getAttributesMap(client, nodeId),
|
|
201
|
-
getOuterHTML(client, nodeId)
|
|
202
|
+
getOuterHTML(client, nodeId),
|
|
203
|
+
describeNode(client, nodeId),
|
|
204
|
+
describeNode(client, root.nodeId, { depth: 0, pierce: true })
|
|
202
205
|
]);
|
|
203
206
|
let box = null;
|
|
204
207
|
let visible = false;
|
|
@@ -213,8 +216,14 @@ async function readActionNode(client, {
|
|
|
213
216
|
kind,
|
|
214
217
|
root: root.name,
|
|
215
218
|
root_node_id: root.nodeId,
|
|
219
|
+
root_backend_node_id: Number.isInteger(Number(describedRoot?.backendNodeId))
|
|
220
|
+
? Number(describedRoot.backendNodeId)
|
|
221
|
+
: null,
|
|
216
222
|
selector,
|
|
217
223
|
node_id: nodeId,
|
|
224
|
+
backend_node_id: Number.isInteger(Number(described?.backendNodeId))
|
|
225
|
+
? Number(described.backendNodeId)
|
|
226
|
+
: null,
|
|
218
227
|
visible,
|
|
219
228
|
center: box?.center || null,
|
|
220
229
|
rect: box?.rect || null,
|
|
@@ -277,18 +286,24 @@ export function summarizeRecommendActionControls(controls = []) {
|
|
|
277
286
|
return {
|
|
278
287
|
favorite: favorite
|
|
279
288
|
? {
|
|
289
|
+
kind: "favorite",
|
|
280
290
|
found: true,
|
|
281
291
|
active: favorite.active,
|
|
282
292
|
disabled: favorite.disabled,
|
|
283
293
|
label: favorite.label,
|
|
284
294
|
selector: favorite.selector,
|
|
285
295
|
root: favorite.root,
|
|
296
|
+
root_node_id: favorite.root_node_id,
|
|
297
|
+
root_backend_node_id: favorite.root_backend_node_id,
|
|
286
298
|
node_id: favorite.node_id,
|
|
287
|
-
|
|
299
|
+
backend_node_id: favorite.backend_node_id,
|
|
300
|
+
center: favorite.center,
|
|
301
|
+
rect: favorite.rect
|
|
288
302
|
}
|
|
289
303
|
: { found: false },
|
|
290
304
|
greet: greet
|
|
291
305
|
? {
|
|
306
|
+
kind: "greet",
|
|
292
307
|
found: true,
|
|
293
308
|
available: greet.available,
|
|
294
309
|
continue_chat: greet.continue_chat,
|
|
@@ -297,8 +312,12 @@ export function summarizeRecommendActionControls(controls = []) {
|
|
|
297
312
|
greet_quota: greet.greet_quota || null,
|
|
298
313
|
selector: greet.selector,
|
|
299
314
|
root: greet.root,
|
|
315
|
+
root_node_id: greet.root_node_id,
|
|
316
|
+
root_backend_node_id: greet.root_backend_node_id,
|
|
300
317
|
node_id: greet.node_id,
|
|
301
|
-
|
|
318
|
+
backend_node_id: greet.backend_node_id,
|
|
319
|
+
center: greet.center,
|
|
320
|
+
rect: greet.rect
|
|
302
321
|
}
|
|
303
322
|
: { found: false },
|
|
304
323
|
counts: {
|
|
@@ -384,7 +403,8 @@ export async function waitForRecommendDetailActionControls(client, {
|
|
|
384
403
|
timeoutMs = 8000,
|
|
385
404
|
intervalMs = 350,
|
|
386
405
|
selectors = {},
|
|
387
|
-
requireAny = true
|
|
406
|
+
requireAny = true,
|
|
407
|
+
requireContinueChat = false
|
|
388
408
|
} = {}) {
|
|
389
409
|
const started = Date.now();
|
|
390
410
|
let lastDiscovery = null;
|
|
@@ -408,7 +428,11 @@ export async function waitForRecommendDetailActionControls(client, {
|
|
|
408
428
|
lastDiscovery.summary.favorite.found
|
|
409
429
|
|| lastDiscovery.summary.greet.found
|
|
410
430
|
);
|
|
411
|
-
|
|
431
|
+
const hasExactContinueChat = Boolean(
|
|
432
|
+
lastDiscovery.summary.greet?.continue_chat === true
|
|
433
|
+
&& normalizeText(lastDiscovery.summary.greet?.label) === "继续沟通"
|
|
434
|
+
);
|
|
435
|
+
if (requireContinueChat ? hasExactContinueChat : (!requireAny || hasControl)) {
|
|
412
436
|
return {
|
|
413
437
|
...lastDiscovery,
|
|
414
438
|
elapsed_ms: Date.now() - started,
|
|
@@ -431,8 +455,514 @@ export async function waitForRecommendDetailActionControls(client, {
|
|
|
431
455
|
};
|
|
432
456
|
}
|
|
433
457
|
|
|
458
|
+
async function verifyRecommendActionControlScope(client, control, { maxDepth = 160 } = {}) {
|
|
459
|
+
const nodeId = Number(control?.node_id);
|
|
460
|
+
const expectedBackendNodeId = Number(control?.backend_node_id);
|
|
461
|
+
const rootNodeId = Number(control?.root_node_id);
|
|
462
|
+
const expectedRootBackendNodeId = Number(control?.root_backend_node_id);
|
|
463
|
+
if (
|
|
464
|
+
!Number.isInteger(nodeId)
|
|
465
|
+
|| nodeId <= 0
|
|
466
|
+
|| !Number.isInteger(expectedBackendNodeId)
|
|
467
|
+
|| expectedBackendNodeId <= 0
|
|
468
|
+
|| !Number.isInteger(rootNodeId)
|
|
469
|
+
|| rootNodeId <= 0
|
|
470
|
+
|| !Number.isInteger(expectedRootBackendNodeId)
|
|
471
|
+
|| expectedRootBackendNodeId <= 0
|
|
472
|
+
|| rootNodeId === nodeId
|
|
473
|
+
) {
|
|
474
|
+
const error = new Error(
|
|
475
|
+
"Recommend action control exact detail-root frontend/backend identity is missing"
|
|
476
|
+
);
|
|
477
|
+
error.code = "RECOMMEND_ACTION_CONTROL_SCOPE_REQUIRED";
|
|
478
|
+
throw error;
|
|
479
|
+
}
|
|
480
|
+
const root = await describeNode(client, rootNodeId, { depth: 0, pierce: true });
|
|
481
|
+
if (Number(root?.backendNodeId) !== expectedRootBackendNodeId) {
|
|
482
|
+
const error = new Error("Recommend action control detail root identity changed");
|
|
483
|
+
error.code = "RECOMMEND_ACTION_CONTROL_SCOPE_MISMATCH";
|
|
484
|
+
error.expected_root_node_id = rootNodeId;
|
|
485
|
+
error.expected_root_backend_node_id = expectedRootBackendNodeId;
|
|
486
|
+
error.observed_root_backend_node_id = Number(root?.backendNodeId) || null;
|
|
487
|
+
throw error;
|
|
488
|
+
}
|
|
489
|
+
const seen = new Set();
|
|
490
|
+
let currentNodeId = nodeId;
|
|
491
|
+
for (let depth = 0; depth < maxDepth; depth += 1) {
|
|
492
|
+
if (seen.has(currentNodeId)) break;
|
|
493
|
+
seen.add(currentNodeId);
|
|
494
|
+
const described = await describeNode(client, currentNodeId, { depth: 0, pierce: true });
|
|
495
|
+
if (depth === 0 && Number(described?.backendNodeId) !== expectedBackendNodeId) {
|
|
496
|
+
const error = new Error("Recommend action control backend identity changed within detail scope");
|
|
497
|
+
error.code = "RECOMMEND_ACTION_CONTROL_IDENTITY_MISMATCH";
|
|
498
|
+
error.expected_backend_node_id = expectedBackendNodeId;
|
|
499
|
+
error.observed_backend_node_id = Number(described?.backendNodeId) || null;
|
|
500
|
+
throw error;
|
|
501
|
+
}
|
|
502
|
+
const parentNodeId = Number(described?.parentId);
|
|
503
|
+
if (parentNodeId === rootNodeId) {
|
|
504
|
+
return {
|
|
505
|
+
verified: true,
|
|
506
|
+
node_id: nodeId,
|
|
507
|
+
backend_node_id: expectedBackendNodeId,
|
|
508
|
+
root_node_id: rootNodeId,
|
|
509
|
+
root_backend_node_id: expectedRootBackendNodeId,
|
|
510
|
+
ancestry_depth: depth + 1
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
if (!Number.isInteger(parentNodeId) || parentNodeId <= 0) break;
|
|
514
|
+
currentNodeId = parentNodeId;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// DOM.pushNodesByBackendIdsToFrontend can restore the exact control/root
|
|
518
|
+
// frontend ids while describeNode still omits parentId for the pushed
|
|
519
|
+
// control. That is not evidence that the control left the detail root.
|
|
520
|
+
// Re-prove containment from the already backend-verified root itself. Prefer
|
|
521
|
+
// exact frontend membership, then tolerate a Chrome frontend alias only when
|
|
522
|
+
// exactly one descendant has the already verified immutable control backend.
|
|
523
|
+
// This is not selector/label rediscovery and cannot switch to another
|
|
524
|
+
// same-labelled control or a control outside the exact detail root.
|
|
525
|
+
const rootDescendantNodeIds = await querySelectorAll(client, rootNodeId, "*");
|
|
526
|
+
if (rootDescendantNodeIds.some((candidateNodeId) => Number(candidateNodeId) === nodeId)) {
|
|
527
|
+
return {
|
|
528
|
+
verified: true,
|
|
529
|
+
method: "root_scoped_exact_frontend_membership",
|
|
530
|
+
node_id: nodeId,
|
|
531
|
+
backend_node_id: expectedBackendNodeId,
|
|
532
|
+
root_node_id: rootNodeId,
|
|
533
|
+
root_backend_node_id: expectedRootBackendNodeId,
|
|
534
|
+
ancestry_depth: null,
|
|
535
|
+
root_scoped_descendant_count: rootDescendantNodeIds.length
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
const rootScopedBackendMatches = [];
|
|
539
|
+
for (const candidateNodeId of rootDescendantNodeIds) {
|
|
540
|
+
try {
|
|
541
|
+
const candidate = await describeNode(client, candidateNodeId, { depth: 0, pierce: true });
|
|
542
|
+
if (Number(candidate?.backendNodeId) === expectedBackendNodeId) {
|
|
543
|
+
rootScopedBackendMatches.push(Number(candidateNodeId));
|
|
544
|
+
}
|
|
545
|
+
} catch {
|
|
546
|
+
// Ignore unrelated stale descendants. The exact backend must still have
|
|
547
|
+
// one and only one readable member inside the exact root below.
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
if (rootScopedBackendMatches.length === 1) {
|
|
551
|
+
return {
|
|
552
|
+
verified: true,
|
|
553
|
+
method: "root_scoped_exact_backend_membership",
|
|
554
|
+
node_id: rootScopedBackendMatches[0],
|
|
555
|
+
backend_node_id: expectedBackendNodeId,
|
|
556
|
+
root_node_id: rootNodeId,
|
|
557
|
+
root_backend_node_id: expectedRootBackendNodeId,
|
|
558
|
+
ancestry_depth: null,
|
|
559
|
+
root_scoped_descendant_count: rootDescendantNodeIds.length,
|
|
560
|
+
root_scoped_backend_match_count: 1
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
const error = new Error(
|
|
564
|
+
"Recommend action control is no longer a descendant of the exact current detail root"
|
|
565
|
+
);
|
|
566
|
+
error.code = "RECOMMEND_ACTION_CONTROL_SCOPE_MISMATCH";
|
|
567
|
+
error.expected_root_node_id = rootNodeId;
|
|
568
|
+
error.expected_root_backend_node_id = expectedRootBackendNodeId;
|
|
569
|
+
error.observed_control_node_id = nodeId;
|
|
570
|
+
error.root_scoped_descendant_count = rootDescendantNodeIds.length;
|
|
571
|
+
error.root_scoped_backend_match_count = rootScopedBackendMatches.length;
|
|
572
|
+
error.cdp_method = "DOM.querySelectorAll";
|
|
573
|
+
error.cdp_node_id = rootNodeId;
|
|
574
|
+
error.cdp_param_keys = ["nodeId", "selector"];
|
|
575
|
+
throw error;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
async function rebindRecommendActionControlFrontendIds(client, control) {
|
|
579
|
+
const expectedBackendNodeId = Number(control?.backend_node_id);
|
|
580
|
+
const expectedRootBackendNodeId = Number(control?.root_backend_node_id);
|
|
581
|
+
if (
|
|
582
|
+
!Number.isInteger(expectedBackendNodeId)
|
|
583
|
+
|| expectedBackendNodeId <= 0
|
|
584
|
+
|| !Number.isInteger(expectedRootBackendNodeId)
|
|
585
|
+
|| expectedRootBackendNodeId <= 0
|
|
586
|
+
) {
|
|
587
|
+
const error = new Error(
|
|
588
|
+
"Recommend action control requires exact control/root backend identities for frontend rebind"
|
|
589
|
+
);
|
|
590
|
+
error.code = "RECOMMEND_ACTION_CONTROL_REBIND_REQUIRED";
|
|
591
|
+
throw error;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Candidate reproof deliberately calls DOM.getDocument. Chrome may then
|
|
595
|
+
// replace every frontend node id even though the immutable backend nodes are
|
|
596
|
+
// unchanged. Push only the two exact backend identities into the refreshed
|
|
597
|
+
// frontend namespace; never rediscover by label or reuse a cached center.
|
|
598
|
+
await client.DOM.getDocument({ depth: 0, pierce: true });
|
|
599
|
+
const pushed = await client.DOM.pushNodesByBackendIdsToFrontend({
|
|
600
|
+
backendNodeIds: [expectedBackendNodeId, expectedRootBackendNodeId]
|
|
601
|
+
});
|
|
602
|
+
const nodeId = Number(pushed?.nodeIds?.[0]);
|
|
603
|
+
const rootNodeId = Number(pushed?.nodeIds?.[1]);
|
|
604
|
+
if (
|
|
605
|
+
!Number.isInteger(nodeId)
|
|
606
|
+
|| nodeId <= 0
|
|
607
|
+
|| !Number.isInteger(rootNodeId)
|
|
608
|
+
|| rootNodeId <= 0
|
|
609
|
+
|| nodeId === rootNodeId
|
|
610
|
+
) {
|
|
611
|
+
const error = new Error(
|
|
612
|
+
"Recommend action control/root backend identities could not be rebound to live frontend nodes"
|
|
613
|
+
);
|
|
614
|
+
error.code = "RECOMMEND_ACTION_CONTROL_REBIND_FAILED";
|
|
615
|
+
error.expected_backend_node_id = expectedBackendNodeId;
|
|
616
|
+
error.expected_root_backend_node_id = expectedRootBackendNodeId;
|
|
617
|
+
error.observed_node_id = Number.isInteger(nodeId) && nodeId > 0 ? nodeId : null;
|
|
618
|
+
error.observed_root_node_id = Number.isInteger(rootNodeId) && rootNodeId > 0
|
|
619
|
+
? rootNodeId
|
|
620
|
+
: null;
|
|
621
|
+
error.cdp_method = "DOM.pushNodesByBackendIdsToFrontend";
|
|
622
|
+
error.cdp_param_keys = ["backendNodeIds"];
|
|
623
|
+
error.cdp_backend_node_id = expectedBackendNodeId;
|
|
624
|
+
throw error;
|
|
625
|
+
}
|
|
626
|
+
const [node, root] = await Promise.all([
|
|
627
|
+
describeNode(client, nodeId, { depth: 0, pierce: true }),
|
|
628
|
+
describeNode(client, rootNodeId, { depth: 0, pierce: true })
|
|
629
|
+
]);
|
|
630
|
+
if (
|
|
631
|
+
Number(node?.backendNodeId) !== expectedBackendNodeId
|
|
632
|
+
|| Number(root?.backendNodeId) !== expectedRootBackendNodeId
|
|
633
|
+
) {
|
|
634
|
+
const error = new Error(
|
|
635
|
+
"Recommend action control/root backend identities changed during frontend rebind"
|
|
636
|
+
);
|
|
637
|
+
error.code = "RECOMMEND_ACTION_CONTROL_REBIND_MISMATCH";
|
|
638
|
+
error.expected_backend_node_id = expectedBackendNodeId;
|
|
639
|
+
error.observed_backend_node_id = Number(node?.backendNodeId) || null;
|
|
640
|
+
error.expected_root_backend_node_id = expectedRootBackendNodeId;
|
|
641
|
+
error.observed_root_backend_node_id = Number(root?.backendNodeId) || null;
|
|
642
|
+
error.cdp_method = "DOM.describeNode";
|
|
643
|
+
error.cdp_param_keys = ["nodeId"];
|
|
644
|
+
error.cdp_node_id = nodeId;
|
|
645
|
+
error.cdp_backend_node_id = expectedBackendNodeId;
|
|
646
|
+
throw error;
|
|
647
|
+
}
|
|
648
|
+
return {
|
|
649
|
+
...control,
|
|
650
|
+
node_id: nodeId,
|
|
651
|
+
root_node_id: rootNodeId
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
export async function verifyRecommendActionControlIdentity(client, control, {
|
|
656
|
+
requireContinueChat = false,
|
|
657
|
+
requireGeometry = true,
|
|
658
|
+
allowScroll = true,
|
|
659
|
+
settleMs = 100
|
|
660
|
+
} = {}) {
|
|
661
|
+
const originalNodeId = Number(control?.node_id);
|
|
662
|
+
const expectedBackendNodeId = Number(control?.backend_node_id);
|
|
663
|
+
if (
|
|
664
|
+
!Number.isInteger(originalNodeId)
|
|
665
|
+
|| originalNodeId <= 0
|
|
666
|
+
|| !Number.isInteger(expectedBackendNodeId)
|
|
667
|
+
|| expectedBackendNodeId <= 0
|
|
668
|
+
) {
|
|
669
|
+
const error = new Error("Recommend action control exact frontend/backend identity is missing");
|
|
670
|
+
error.code = "RECOMMEND_ACTION_CONTROL_IDENTITY_REQUIRED";
|
|
671
|
+
throw error;
|
|
672
|
+
}
|
|
673
|
+
const activeControl = await rebindRecommendActionControlFrontendIds(client, control);
|
|
674
|
+
const nodeId = Number(activeControl.node_id);
|
|
675
|
+
const scopeBefore = await verifyRecommendActionControlScope(client, activeControl);
|
|
676
|
+
const described = await describeNode(client, nodeId);
|
|
677
|
+
if (Number(described?.backendNodeId) !== expectedBackendNodeId) {
|
|
678
|
+
const error = new Error("Recommend action control backend identity no longer matches discovery");
|
|
679
|
+
error.code = "RECOMMEND_ACTION_CONTROL_IDENTITY_MISMATCH";
|
|
680
|
+
error.expected_backend_node_id = expectedBackendNodeId;
|
|
681
|
+
error.observed_backend_node_id = Number(described?.backendNodeId) || null;
|
|
682
|
+
throw error;
|
|
683
|
+
}
|
|
684
|
+
if (allowScroll) {
|
|
685
|
+
await scrollNodeIntoView(client, nodeId);
|
|
686
|
+
if (settleMs > 0) await sleep(settleMs);
|
|
687
|
+
}
|
|
688
|
+
const rebound = await describeNode(client, nodeId);
|
|
689
|
+
if (Number(rebound?.backendNodeId) !== expectedBackendNodeId) {
|
|
690
|
+
const error = new Error(
|
|
691
|
+
allowScroll
|
|
692
|
+
? "Recommend action control backend identity changed after fresh scroll"
|
|
693
|
+
: "Recommend action control backend identity changed during no-scroll refresh"
|
|
694
|
+
);
|
|
695
|
+
error.code = "RECOMMEND_ACTION_CONTROL_IDENTITY_MISMATCH";
|
|
696
|
+
error.expected_backend_node_id = expectedBackendNodeId;
|
|
697
|
+
error.observed_backend_node_id = Number(rebound?.backendNodeId) || null;
|
|
698
|
+
throw error;
|
|
699
|
+
}
|
|
700
|
+
const scopeAfter = await verifyRecommendActionControlScope(client, activeControl);
|
|
701
|
+
const [attributes, outerHTML] = await Promise.all([
|
|
702
|
+
getAttributesMap(client, nodeId),
|
|
703
|
+
getOuterHTML(client, nodeId)
|
|
704
|
+
]);
|
|
705
|
+
const classification = activeControl?.kind === "favorite"
|
|
706
|
+
? classifyFavoriteControl({ outerHTML, attributes })
|
|
707
|
+
: classifyGreetControl({ outerHTML, attributes });
|
|
708
|
+
const expectedLabel = normalizeText(activeControl?.label || "");
|
|
709
|
+
const observedLabel = normalizeText(classification?.label || "");
|
|
710
|
+
const exactContinueChat = Boolean(
|
|
711
|
+
classification?.continue_chat === true
|
|
712
|
+
&& observedLabel === "继续沟通"
|
|
713
|
+
);
|
|
714
|
+
const exactExpectedState = requireContinueChat
|
|
715
|
+
? exactContinueChat
|
|
716
|
+
: activeControl?.kind === "greet"
|
|
717
|
+
? classification.available === true
|
|
718
|
+
&& classification.continue_chat === false
|
|
719
|
+
&& observedLabel === expectedLabel
|
|
720
|
+
: classification.matches === true && observedLabel === expectedLabel;
|
|
721
|
+
if (!expectedLabel || !exactExpectedState || classification.disabled === true) {
|
|
722
|
+
const error = new Error("Recommend action control exact visible label/state is not verified");
|
|
723
|
+
error.code = "RECOMMEND_ACTION_CONTROL_LABEL_MISMATCH";
|
|
724
|
+
error.expected_label = requireContinueChat ? "继续沟通" : expectedLabel || null;
|
|
725
|
+
error.observed_label = observedLabel || null;
|
|
726
|
+
throw error;
|
|
727
|
+
}
|
|
728
|
+
let geometry = null;
|
|
729
|
+
if (requireGeometry) {
|
|
730
|
+
const box = await getNodeBox(client, nodeId);
|
|
731
|
+
const valid = [box?.center?.x, box?.center?.y, box?.rect?.width, box?.rect?.height]
|
|
732
|
+
.every((value) => Number.isFinite(Number(value)))
|
|
733
|
+
&& Number(box.rect.width) > 2
|
|
734
|
+
&& Number(box.rect.height) > 2;
|
|
735
|
+
if (!valid) {
|
|
736
|
+
const error = new Error("Recommend action control fresh geometry is unreadable");
|
|
737
|
+
error.code = "RECOMMEND_ACTION_CONTROL_GEOMETRY_UNREADABLE";
|
|
738
|
+
throw error;
|
|
739
|
+
}
|
|
740
|
+
geometry = { center: box.center, rect: box.rect };
|
|
741
|
+
}
|
|
742
|
+
return {
|
|
743
|
+
verified: true,
|
|
744
|
+
node_id: nodeId,
|
|
745
|
+
backend_node_id: expectedBackendNodeId,
|
|
746
|
+
root: activeControl?.root || null,
|
|
747
|
+
root_node_id: scopeAfter.root_node_id,
|
|
748
|
+
root_backend_node_id: scopeAfter.root_backend_node_id,
|
|
749
|
+
root_ancestry_depth: scopeAfter.ancestry_depth,
|
|
750
|
+
root_scope_stable: scopeBefore.root_backend_node_id === scopeAfter.root_backend_node_id,
|
|
751
|
+
scroll_allowed: allowScroll === true,
|
|
752
|
+
settle_ms: allowScroll ? Math.max(0, Number(settleMs) || 0) : 0,
|
|
753
|
+
label: observedLabel,
|
|
754
|
+
continue_chat: exactContinueChat,
|
|
755
|
+
center: geometry?.center || null,
|
|
756
|
+
rect: geometry?.rect || null
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function positiveActionNodeId(value) {
|
|
761
|
+
const parsed = Number(value);
|
|
762
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : 0;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function resolveRecommendActionClickPointCandidates(rect) {
|
|
766
|
+
const left = Number(rect?.x);
|
|
767
|
+
const top = Number(rect?.y);
|
|
768
|
+
const width = Number(rect?.width);
|
|
769
|
+
const height = Number(rect?.height);
|
|
770
|
+
if (
|
|
771
|
+
!Number.isFinite(left)
|
|
772
|
+
|| !Number.isFinite(top)
|
|
773
|
+
|| !Number.isFinite(width)
|
|
774
|
+
|| !Number.isFinite(height)
|
|
775
|
+
|| width <= 2
|
|
776
|
+
|| height <= 2
|
|
777
|
+
) return [];
|
|
778
|
+
const candidates = [
|
|
779
|
+
{ name: "center", x: left + width * 0.5, y: top + height * 0.5 },
|
|
780
|
+
{ name: "left_inset", x: left + width * 0.25, y: top + height * 0.5 },
|
|
781
|
+
{ name: "right_inset", x: left + width * 0.75, y: top + height * 0.5 },
|
|
782
|
+
{ name: "upper_inset", x: left + width * 0.5, y: top + height * 0.35 },
|
|
783
|
+
{ name: "lower_inset", x: left + width * 0.5, y: top + height * 0.65 }
|
|
784
|
+
];
|
|
785
|
+
const seen = new Set();
|
|
786
|
+
return candidates.flatMap((point) => {
|
|
787
|
+
const rounded = {
|
|
788
|
+
name: point.name,
|
|
789
|
+
x: Math.round(point.x),
|
|
790
|
+
y: Math.round(point.y)
|
|
791
|
+
};
|
|
792
|
+
const key = `${rounded.x}:${rounded.y}`;
|
|
793
|
+
if (seen.has(key)) return [];
|
|
794
|
+
seen.add(key);
|
|
795
|
+
return [rounded];
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function createRecommendActionControlHitTestError(reason, evidence = null) {
|
|
800
|
+
const error = new Error(`Recommend action control hit-test proof failed: ${reason}`);
|
|
801
|
+
error.code = "RECOMMEND_ACTION_CONTROL_HIT_TEST_UNVERIFIED";
|
|
802
|
+
error.phase = "recommend:post-action-pre-input";
|
|
803
|
+
error.recommend_pre_input_aborted = true;
|
|
804
|
+
error.recommend_input_dispatched = false;
|
|
805
|
+
error.recommend_action_control_hit_test = evidence;
|
|
806
|
+
if (reason === "layout_metrics_unavailable" || reason === "layout_metrics_invalid") {
|
|
807
|
+
error.cdp_method = "Page.getLayoutMetrics";
|
|
808
|
+
error.cdp_param_keys = [];
|
|
809
|
+
} else if (reason === "control_descendants_unavailable") {
|
|
810
|
+
error.cdp_method = "DOM.querySelectorAll";
|
|
811
|
+
error.cdp_param_keys = ["nodeId", "selector"];
|
|
812
|
+
} else {
|
|
813
|
+
error.cdp_method = "DOM.getNodeForLocation";
|
|
814
|
+
error.cdp_param_keys = ["x", "y", "includeUserAgentShadowDOM"];
|
|
815
|
+
}
|
|
816
|
+
return error;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
async function readRecommendActionControlHitTestEvidence(client, control) {
|
|
820
|
+
const controlNodeId = positiveActionNodeId(control?.node_id);
|
|
821
|
+
const expectedControlBackendNodeId = positiveActionNodeId(control?.backend_node_id);
|
|
822
|
+
const expectedRootBackendNodeId = positiveActionNodeId(control?.root_backend_node_id);
|
|
823
|
+
const baseEvidence = {
|
|
824
|
+
verified: false,
|
|
825
|
+
exact_control_hit_verified: false,
|
|
826
|
+
control_node_id: controlNodeId || null,
|
|
827
|
+
control_backend_node_id: expectedControlBackendNodeId || null,
|
|
828
|
+
root_backend_node_id: expectedRootBackendNodeId || null,
|
|
829
|
+
viewport: null,
|
|
830
|
+
control_descendant_backend_count: 0,
|
|
831
|
+
selected: null,
|
|
832
|
+
attempts: []
|
|
833
|
+
};
|
|
834
|
+
if (typeof client?.Page?.getLayoutMetrics !== "function") {
|
|
835
|
+
throw createRecommendActionControlHitTestError("layout_metrics_unavailable", baseEvidence);
|
|
836
|
+
}
|
|
837
|
+
if (typeof client?.DOM?.querySelectorAll !== "function") {
|
|
838
|
+
throw createRecommendActionControlHitTestError("control_descendants_unavailable", baseEvidence);
|
|
839
|
+
}
|
|
840
|
+
if (typeof client?.DOM?.getNodeForLocation !== "function") {
|
|
841
|
+
throw createRecommendActionControlHitTestError("node_location_unavailable", baseEvidence);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
const metrics = await client.Page.getLayoutMetrics();
|
|
845
|
+
const viewport = metrics?.cssVisualViewport
|
|
846
|
+
|| metrics?.visualViewport
|
|
847
|
+
|| metrics?.cssLayoutViewport
|
|
848
|
+
|| metrics?.layoutViewport
|
|
849
|
+
|| null;
|
|
850
|
+
const viewportWidth = Number(viewport?.clientWidth || viewport?.width || 0);
|
|
851
|
+
const viewportHeight = Number(viewport?.clientHeight || viewport?.height || 0);
|
|
852
|
+
const viewportEvidence = {
|
|
853
|
+
width: viewportWidth,
|
|
854
|
+
height: viewportHeight,
|
|
855
|
+
page_x: Number(viewport?.pageX || 0),
|
|
856
|
+
page_y: Number(viewport?.pageY || 0),
|
|
857
|
+
scale: Number(viewport?.scale || 1),
|
|
858
|
+
source: metrics?.cssVisualViewport
|
|
859
|
+
? "cssVisualViewport"
|
|
860
|
+
: metrics?.visualViewport
|
|
861
|
+
? "visualViewport"
|
|
862
|
+
: metrics?.cssLayoutViewport
|
|
863
|
+
? "cssLayoutViewport"
|
|
864
|
+
: metrics?.layoutViewport
|
|
865
|
+
? "layoutViewport"
|
|
866
|
+
: null
|
|
867
|
+
};
|
|
868
|
+
baseEvidence.viewport = viewportEvidence;
|
|
869
|
+
if (
|
|
870
|
+
!Number.isFinite(viewportWidth)
|
|
871
|
+
|| viewportWidth <= 4
|
|
872
|
+
|| !Number.isFinite(viewportHeight)
|
|
873
|
+
|| viewportHeight <= 4
|
|
874
|
+
) {
|
|
875
|
+
throw createRecommendActionControlHitTestError("layout_metrics_invalid", baseEvidence);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
const descendantNodeIds = await querySelectorAll(client, controlNodeId, "*");
|
|
879
|
+
if (descendantNodeIds.length > 256) {
|
|
880
|
+
const evidence = {
|
|
881
|
+
...baseEvidence,
|
|
882
|
+
control_descendant_count: descendantNodeIds.length
|
|
883
|
+
};
|
|
884
|
+
throw createRecommendActionControlHitTestError("control_descendant_set_too_large", evidence);
|
|
885
|
+
}
|
|
886
|
+
const ownedBackendNodeIds = new Set([expectedControlBackendNodeId]);
|
|
887
|
+
for (const descendantNodeId of descendantNodeIds) {
|
|
888
|
+
const described = await describeNode(client, descendantNodeId, { depth: 0, pierce: true });
|
|
889
|
+
const backendNodeId = positiveActionNodeId(described?.backendNodeId);
|
|
890
|
+
if (backendNodeId) ownedBackendNodeIds.add(backendNodeId);
|
|
891
|
+
}
|
|
892
|
+
baseEvidence.control_descendant_backend_count = Math.max(0, ownedBackendNodeIds.size - 1);
|
|
893
|
+
|
|
894
|
+
const margin = 2;
|
|
895
|
+
const attempts = [];
|
|
896
|
+
for (const point of resolveRecommendActionClickPointCandidates(control?.rect)) {
|
|
897
|
+
const insideViewport = Boolean(
|
|
898
|
+
point.x >= margin
|
|
899
|
+
&& point.x <= viewportWidth - margin
|
|
900
|
+
&& point.y >= margin
|
|
901
|
+
&& point.y <= viewportHeight - margin
|
|
902
|
+
);
|
|
903
|
+
if (!insideViewport) {
|
|
904
|
+
attempts.push({
|
|
905
|
+
point,
|
|
906
|
+
inside_viewport: false,
|
|
907
|
+
exact_control_hit: false,
|
|
908
|
+
hit_node_id: null,
|
|
909
|
+
hit_backend_node_id: null,
|
|
910
|
+
hit_frame_id: null,
|
|
911
|
+
reason: "action_click_point_outside_viewport"
|
|
912
|
+
});
|
|
913
|
+
continue;
|
|
914
|
+
}
|
|
915
|
+
// This must remain the last CDP read before Input whenever it succeeds.
|
|
916
|
+
// Exact ownership is established only from backend IDs proven inside the
|
|
917
|
+
// freshly rebound control subtree; root-only or same-label hits fail.
|
|
918
|
+
const hit = await client.DOM.getNodeForLocation({
|
|
919
|
+
x: point.x,
|
|
920
|
+
y: point.y,
|
|
921
|
+
includeUserAgentShadowDOM: true
|
|
922
|
+
});
|
|
923
|
+
const hitNodeId = positiveActionNodeId(hit?.nodeId);
|
|
924
|
+
const hitBackendNodeId = positiveActionNodeId(hit?.backendNodeId);
|
|
925
|
+
const exactControlHit = Boolean(
|
|
926
|
+
hitBackendNodeId
|
|
927
|
+
&& ownedBackendNodeIds.has(hitBackendNodeId)
|
|
928
|
+
);
|
|
929
|
+
const attempt = {
|
|
930
|
+
point,
|
|
931
|
+
inside_viewport: true,
|
|
932
|
+
exact_control_hit: exactControlHit,
|
|
933
|
+
hit_node_id: hitNodeId || null,
|
|
934
|
+
hit_backend_node_id: hitBackendNodeId || null,
|
|
935
|
+
hit_frame_id: hit?.frameId || null,
|
|
936
|
+
reason: exactControlHit ? null : "action_click_point_not_owned_by_exact_control"
|
|
937
|
+
};
|
|
938
|
+
attempts.push(attempt);
|
|
939
|
+
if (exactControlHit) {
|
|
940
|
+
return {
|
|
941
|
+
...baseEvidence,
|
|
942
|
+
verified: true,
|
|
943
|
+
exact_control_hit_verified: true,
|
|
944
|
+
selected: point,
|
|
945
|
+
attempts
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
const evidence = {
|
|
950
|
+
...baseEvidence,
|
|
951
|
+
attempts
|
|
952
|
+
};
|
|
953
|
+
const reason = attempts.some((attempt) => attempt.inside_viewport)
|
|
954
|
+
? "action_click_point_not_owned_by_exact_control"
|
|
955
|
+
: "action_click_point_outside_viewport";
|
|
956
|
+
throw createRecommendActionControlHitTestError(reason, evidence);
|
|
957
|
+
}
|
|
958
|
+
|
|
434
959
|
export async function clickRecommendActionControl(client, control, {
|
|
435
|
-
allowDisabled = false
|
|
960
|
+
allowDisabled = false,
|
|
961
|
+
beforeRefresh = null,
|
|
962
|
+
beforeFinalRefresh = null,
|
|
963
|
+
beforeClick = null,
|
|
964
|
+
beforeInput = null,
|
|
965
|
+
immediatelyBeforeInput = null
|
|
436
966
|
} = {}) {
|
|
437
967
|
const greetQuota = control?.kind === "greet"
|
|
438
968
|
? assertGreetQuotaAvailable(control.greet_quota || control.label || "")
|
|
@@ -441,15 +971,102 @@ export async function clickRecommendActionControl(client, control, {
|
|
|
441
971
|
throw new Error(`Action control is disabled: ${control.kind}`);
|
|
442
972
|
}
|
|
443
973
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
if (
|
|
447
|
-
|
|
974
|
+
const originalNodeId = Number(control?.node_id);
|
|
975
|
+
const expectedBackendNodeId = Number(control?.backend_node_id);
|
|
976
|
+
if (
|
|
977
|
+
!Number.isInteger(originalNodeId)
|
|
978
|
+
|| originalNodeId <= 0
|
|
979
|
+
|| !Number.isInteger(expectedBackendNodeId)
|
|
980
|
+
|| expectedBackendNodeId <= 0
|
|
981
|
+
) {
|
|
982
|
+
const error = new Error(
|
|
983
|
+
"Recommend action control requires exact frontend/backend identity; cached centers are never clickable"
|
|
984
|
+
);
|
|
985
|
+
error.code = "RECOMMEND_ACTION_CONTROL_IDENTITY_REQUIRED";
|
|
986
|
+
error.phase = "recommend:post-action-control-refresh";
|
|
987
|
+
error.cached_center_ignored = Boolean(control?.center);
|
|
988
|
+
throw error;
|
|
989
|
+
}
|
|
990
|
+
if (beforeRefresh != null) {
|
|
991
|
+
if (typeof beforeRefresh !== "function") {
|
|
992
|
+
const error = new Error("Recommend action beforeRefresh must be a function");
|
|
993
|
+
error.code = "RECOMMEND_ACTION_BEFORE_REFRESH_INVALID";
|
|
994
|
+
throw error;
|
|
995
|
+
}
|
|
996
|
+
await beforeRefresh();
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
let clickCenter = null;
|
|
1000
|
+
let clickRect = null;
|
|
1001
|
+
let clickTargetProof = null;
|
|
1002
|
+
let activeControl = control;
|
|
1003
|
+
let nodeId = originalNodeId;
|
|
1004
|
+
{
|
|
1005
|
+
let refreshStep = "rebind_before_scroll";
|
|
448
1006
|
try {
|
|
449
|
-
await
|
|
1007
|
+
activeControl = await rebindRecommendActionControlFrontendIds(client, activeControl);
|
|
1008
|
+
nodeId = Number(activeControl.node_id);
|
|
1009
|
+
refreshStep = "describe_before_scroll";
|
|
1010
|
+
await verifyRecommendActionControlScope(client, activeControl);
|
|
1011
|
+
const beforeNode = await describeNode(client, nodeId);
|
|
1012
|
+
if (Number(beforeNode?.backendNodeId) !== expectedBackendNodeId) {
|
|
1013
|
+
const error = new Error("Recommend action control backend identity changed before click");
|
|
1014
|
+
error.code = "RECOMMEND_ACTION_CONTROL_IDENTITY_MISMATCH";
|
|
1015
|
+
throw error;
|
|
1016
|
+
}
|
|
1017
|
+
refreshStep = "scroll_into_view";
|
|
1018
|
+
await scrollNodeIntoView(client, nodeId);
|
|
450
1019
|
await sleep(150);
|
|
1020
|
+
if (beforeFinalRefresh != null) {
|
|
1021
|
+
if (typeof beforeFinalRefresh !== "function") {
|
|
1022
|
+
const error = new Error("Recommend action beforeFinalRefresh must be a function");
|
|
1023
|
+
error.code = "RECOMMEND_ACTION_BEFORE_FINAL_REFRESH_INVALID";
|
|
1024
|
+
throw error;
|
|
1025
|
+
}
|
|
1026
|
+
refreshStep = "before_final_refresh";
|
|
1027
|
+
await beforeFinalRefresh();
|
|
1028
|
+
}
|
|
1029
|
+
refreshStep = "rebind_after_final_refresh";
|
|
1030
|
+
activeControl = await rebindRecommendActionControlFrontendIds(client, activeControl);
|
|
1031
|
+
nodeId = Number(activeControl.node_id);
|
|
1032
|
+
refreshStep = "describe_after_scroll";
|
|
1033
|
+
const afterNode = await describeNode(client, nodeId);
|
|
1034
|
+
if (Number(afterNode?.backendNodeId) !== expectedBackendNodeId) {
|
|
1035
|
+
const error = new Error("Recommend action control backend identity changed after scroll");
|
|
1036
|
+
error.code = "RECOMMEND_ACTION_CONTROL_IDENTITY_MISMATCH";
|
|
1037
|
+
throw error;
|
|
1038
|
+
}
|
|
1039
|
+
refreshStep = "verify_detail_scope_after_scroll";
|
|
1040
|
+
await verifyRecommendActionControlScope(client, activeControl);
|
|
1041
|
+
refreshStep = "read_exact_label";
|
|
1042
|
+
const [freshAttributes, freshOuterHTML] = await Promise.all([
|
|
1043
|
+
getAttributesMap(client, nodeId),
|
|
1044
|
+
getOuterHTML(client, nodeId)
|
|
1045
|
+
]);
|
|
1046
|
+
const freshClassification = activeControl?.kind === "favorite"
|
|
1047
|
+
? classifyFavoriteControl({ outerHTML: freshOuterHTML, attributes: freshAttributes })
|
|
1048
|
+
: classifyGreetControl({ outerHTML: freshOuterHTML, attributes: freshAttributes });
|
|
1049
|
+
const expectedLabel = normalizeText(activeControl?.label || "");
|
|
1050
|
+
const freshLabel = normalizeText(freshClassification?.label || "");
|
|
1051
|
+
const exactKindState = activeControl?.kind === "greet"
|
|
1052
|
+
? activeControl?.continue_chat === true
|
|
1053
|
+
? freshClassification.continue_chat === true && freshLabel === "继续沟通"
|
|
1054
|
+
: freshClassification.available === true && freshClassification.continue_chat === false
|
|
1055
|
+
: freshClassification.matches === true;
|
|
1056
|
+
if (
|
|
1057
|
+
!expectedLabel
|
|
1058
|
+
|| freshLabel !== expectedLabel
|
|
1059
|
+
|| !exactKindState
|
|
1060
|
+
|| freshClassification.disabled === true
|
|
1061
|
+
) {
|
|
1062
|
+
const error = new Error("Recommend action control exact label/state changed before click");
|
|
1063
|
+
error.code = "RECOMMEND_ACTION_CONTROL_LABEL_MISMATCH";
|
|
1064
|
+
error.expected_label = expectedLabel || null;
|
|
1065
|
+
error.observed_label = freshLabel || null;
|
|
1066
|
+
throw error;
|
|
1067
|
+
}
|
|
451
1068
|
refreshStep = "read_box_model";
|
|
452
|
-
const box = await getNodeBox(client,
|
|
1069
|
+
const box = await getNodeBox(client, nodeId);
|
|
453
1070
|
const centerX = Number(box?.center?.x);
|
|
454
1071
|
const centerY = Number(box?.center?.y);
|
|
455
1072
|
const width = Number(box?.rect?.width);
|
|
@@ -463,13 +1080,15 @@ export async function clickRecommendActionControl(client, control, {
|
|
|
463
1080
|
|| height <= 2
|
|
464
1081
|
) {
|
|
465
1082
|
const error = new Error(
|
|
466
|
-
`Could not compute box model for recommend action control nodeId=${
|
|
1083
|
+
`Could not compute box model for recommend action control nodeId=${nodeId}`
|
|
467
1084
|
);
|
|
468
1085
|
error.code = "RECOMMEND_ACTION_CONTROL_GEOMETRY_UNREADABLE";
|
|
469
1086
|
throw error;
|
|
470
1087
|
}
|
|
471
1088
|
clickCenter = box.center;
|
|
472
1089
|
clickRect = box.rect;
|
|
1090
|
+
refreshStep = "verify_detail_scope_before_journal";
|
|
1091
|
+
await verifyRecommendActionControlScope(client, activeControl);
|
|
473
1092
|
} catch (error) {
|
|
474
1093
|
const failure = error instanceof Error
|
|
475
1094
|
? error
|
|
@@ -478,38 +1097,123 @@ export async function clickRecommendActionControl(client, control, {
|
|
|
478
1097
|
failure.phase = failure.phase || "recommend:post-action-control-refresh";
|
|
479
1098
|
failure.action_control_refresh_step = failure.action_control_refresh_step || refreshStep;
|
|
480
1099
|
failure.action_control = failure.action_control || {
|
|
481
|
-
kind:
|
|
482
|
-
label:
|
|
483
|
-
selector:
|
|
484
|
-
root:
|
|
485
|
-
|
|
1100
|
+
kind: activeControl?.kind || null,
|
|
1101
|
+
label: activeControl?.label || null,
|
|
1102
|
+
selector: activeControl?.selector || null,
|
|
1103
|
+
root: activeControl?.root || null,
|
|
1104
|
+
root_node_id: Number(activeControl?.root_node_id) || null,
|
|
1105
|
+
root_backend_node_id: Number(activeControl?.root_backend_node_id) || null,
|
|
1106
|
+
node_id: nodeId
|
|
486
1107
|
};
|
|
487
1108
|
failure.cached_center_ignored = Boolean(control?.center);
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
1109
|
+
const refreshMethod = {
|
|
1110
|
+
rebind_before_scroll: "DOM.pushNodesByBackendIdsToFrontend",
|
|
1111
|
+
describe_before_scroll: "DOM.describeNode",
|
|
1112
|
+
scroll_into_view: "DOM.scrollIntoViewIfNeeded",
|
|
1113
|
+
rebind_after_final_refresh: "DOM.pushNodesByBackendIdsToFrontend",
|
|
1114
|
+
describe_after_scroll: "DOM.describeNode",
|
|
1115
|
+
verify_detail_scope_after_scroll: "DOM.describeNode",
|
|
1116
|
+
read_exact_label: "DOM.getOuterHTML",
|
|
1117
|
+
read_box_model: "DOM.getBoxModel",
|
|
1118
|
+
verify_detail_scope_before_journal: "DOM.describeNode"
|
|
1119
|
+
}[refreshStep] || null;
|
|
1120
|
+
if (refreshMethod) failure.cdp_method = failure.cdp_method || refreshMethod;
|
|
1121
|
+
failure.cdp_node_id = failure.cdp_node_id || nodeId;
|
|
494
1122
|
failure.cdp_param_keys = Array.isArray(failure.cdp_param_keys)
|
|
495
1123
|
? failure.cdp_param_keys
|
|
496
|
-
:
|
|
1124
|
+
: refreshStep.startsWith("rebind_")
|
|
1125
|
+
? ["backendNodeIds"]
|
|
1126
|
+
: ["nodeId"];
|
|
497
1127
|
throw failure;
|
|
498
1128
|
}
|
|
499
1129
|
}
|
|
500
|
-
|
|
501
|
-
|
|
1130
|
+
try {
|
|
1131
|
+
if (beforeClick != null) {
|
|
1132
|
+
if (typeof beforeClick !== "function") {
|
|
1133
|
+
const error = new Error("Recommend action beforeClick must be a function");
|
|
1134
|
+
error.code = "RECOMMEND_ACTION_BEFORE_CLICK_INVALID";
|
|
1135
|
+
throw error;
|
|
1136
|
+
}
|
|
1137
|
+
await beforeClick({
|
|
1138
|
+
kind: activeControl.kind,
|
|
1139
|
+
label: activeControl.label,
|
|
1140
|
+
selector: activeControl.selector,
|
|
1141
|
+
root: activeControl.root,
|
|
1142
|
+
root_node_id: Number(activeControl?.root_node_id) || null,
|
|
1143
|
+
root_backend_node_id: Number(activeControl?.root_backend_node_id) || null,
|
|
1144
|
+
node_id: nodeId,
|
|
1145
|
+
backend_node_id: expectedBackendNodeId,
|
|
1146
|
+
center: clickCenter,
|
|
1147
|
+
rect: clickRect
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
if (beforeInput != null) {
|
|
1151
|
+
if (typeof beforeInput !== "function") {
|
|
1152
|
+
const error = new Error("Recommend action beforeInput must be a function");
|
|
1153
|
+
error.code = "RECOMMEND_ACTION_BEFORE_INPUT_INVALID";
|
|
1154
|
+
throw error;
|
|
1155
|
+
}
|
|
1156
|
+
await beforeInput();
|
|
1157
|
+
}
|
|
1158
|
+
const controlAfterFinalScroll = await verifyRecommendActionControlIdentity(client, activeControl, {
|
|
1159
|
+
requireContinueChat: activeControl?.continue_chat === true,
|
|
1160
|
+
requireGeometry: false,
|
|
1161
|
+
allowScroll: true,
|
|
1162
|
+
settleMs: 100
|
|
1163
|
+
});
|
|
1164
|
+
activeControl = { ...activeControl, ...controlAfterFinalScroll };
|
|
1165
|
+
if (immediatelyBeforeInput != null) {
|
|
1166
|
+
if (typeof immediatelyBeforeInput !== "function") {
|
|
1167
|
+
const error = new Error("Recommend action immediatelyBeforeInput must be a function");
|
|
1168
|
+
error.code = "RECOMMEND_ACTION_IMMEDIATELY_BEFORE_INPUT_INVALID";
|
|
1169
|
+
throw error;
|
|
1170
|
+
}
|
|
1171
|
+
await immediatelyBeforeInput(controlAfterFinalScroll);
|
|
1172
|
+
}
|
|
1173
|
+
// The candidate/root hook above must be non-scrolling. Re-read the exact
|
|
1174
|
+
// control/root/label/box after it with no scroll and no settle, then prove
|
|
1175
|
+
// a bounded point's topmost native hit belongs to this exact control.
|
|
1176
|
+
// A successful DOM.getNodeForLocation is the last CDP read before Input.
|
|
1177
|
+
const finalControl = await verifyRecommendActionControlIdentity(client, activeControl, {
|
|
1178
|
+
requireContinueChat: activeControl?.continue_chat === true,
|
|
1179
|
+
requireGeometry: true,
|
|
1180
|
+
allowScroll: false,
|
|
1181
|
+
settleMs: 0
|
|
1182
|
+
});
|
|
1183
|
+
clickCenter = finalControl.center;
|
|
1184
|
+
clickRect = finalControl.rect;
|
|
1185
|
+
activeControl = { ...activeControl, ...finalControl };
|
|
1186
|
+
clickTargetProof = await readRecommendActionControlHitTestEvidence(client, activeControl);
|
|
1187
|
+
clickCenter = {
|
|
1188
|
+
x: clickTargetProof.selected.x,
|
|
1189
|
+
y: clickTargetProof.selected.y
|
|
1190
|
+
};
|
|
1191
|
+
} catch (error) {
|
|
1192
|
+
const failure = error instanceof Error
|
|
1193
|
+
? error
|
|
1194
|
+
: new Error(String(error || "Recommend action pre-Input verification failed"));
|
|
1195
|
+
failure.recommend_pre_input_aborted = true;
|
|
1196
|
+
failure.recommend_input_dispatched = false;
|
|
1197
|
+
failure.phase = failure.phase || "recommend:post-action-pre-input";
|
|
1198
|
+
throw failure;
|
|
502
1199
|
}
|
|
503
|
-
await clickPoint(client, clickCenter.x, clickCenter.y
|
|
1200
|
+
await clickPoint(client, clickCenter.x, clickCenter.y, {
|
|
1201
|
+
humanRestEnabled: false,
|
|
1202
|
+
moveBeforePress: false
|
|
1203
|
+
});
|
|
504
1204
|
return {
|
|
505
1205
|
clicked: true,
|
|
506
|
-
kind:
|
|
507
|
-
label:
|
|
1206
|
+
kind: activeControl.kind,
|
|
1207
|
+
label: activeControl.label,
|
|
508
1208
|
greet_quota: greetQuota?.found ? greetQuota : null,
|
|
509
|
-
selector:
|
|
510
|
-
root:
|
|
511
|
-
|
|
1209
|
+
selector: activeControl.selector,
|
|
1210
|
+
root: activeControl.root,
|
|
1211
|
+
root_node_id: Number(activeControl?.root_node_id) || null,
|
|
1212
|
+
root_backend_node_id: Number(activeControl?.root_backend_node_id) || null,
|
|
1213
|
+
node_id: Number(activeControl?.node_id) || null,
|
|
1214
|
+
backend_node_id: expectedBackendNodeId,
|
|
512
1215
|
center: clickCenter,
|
|
513
|
-
rect: clickRect
|
|
1216
|
+
rect: clickRect,
|
|
1217
|
+
click_target_proof: clickTargetProof
|
|
514
1218
|
};
|
|
515
1219
|
}
|